Skip to content

Commit dccf5f9

Browse files
authored
Merge pull request #893 from cmu-delphi/sgratzl/annotationlog
Annotation Overview Table
2 parents 58baec7 + 9ef294b commit dccf5f9

File tree

12 files changed

+355
-147
lines changed

12 files changed

+355
-147
lines changed

src/index.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@
3535
<li data-mode="indicator-status" class="nav-dropdown-child">
3636
<a href="/indicator-status">Indicator Status Overview</a>
3737
</li>
38+
<li data-mode="data-anomalies" class="nav-dropdown-child">
39+
<a href="/data-anomalies">Data Anomalies</a>
40+
</li>
3841
<li data-mode="export" class="nav-dropdown-child"><a href="/export">Export Data</a></li>
3942
<li class="uk-nav-divider"></li>
4043
<li data-mode="lab" class="nav-dropdown-child"><a href="/lab">Lab</a></li>
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
<script>
2+
import { annotationManager, getLevelInfo } from '../../stores';
3+
import SortColumnIndicator from '../mobile/components/SortColumnIndicator.svelte';
4+
import { SortHelper } from '../mobile/components/tableUtils';
5+
import ExternalLinkIcon from '!raw-loader!@fortawesome/fontawesome-free/svgs/solid/external-link-alt.svg';
6+
import { getDataSource, CASES_SOURCE, DEATH_SOURCE } from '../../stores/dataSourceLookup';
7+
import { formatDateISO } from '../../formats';
8+
import chevronRightIcon from '!raw-loader!@fortawesome/fontawesome-free/svgs/solid/chevron-right.svg';
9+
import { getInfoByName } from '../../maps';
10+
import { isCasesSignal, isDeathSignal } from '../../data';
11+
12+
/**
13+
* @param {import ('../../data/annotations').Annotation} d
14+
*/
15+
function resolveDataSource(d) {
16+
if (d.source !== 'indicator-combination') {
17+
return getDataSource(d.source);
18+
}
19+
const signals = [...d.signals];
20+
if (signals.every((d) => isCasesSignal(d))) {
21+
return getDataSource(CASES_SOURCE);
22+
}
23+
if (signals.every((d) => isDeathSignal(d))) {
24+
return getDataSource(DEATH_SOURCE);
25+
}
26+
return getDataSource(d.source);
27+
}
28+
/**
29+
*
30+
* @param {import ('../../data/annotations').Annotation} d
31+
*/
32+
function simplifyAnnotation(d, i) {
33+
return {
34+
i,
35+
annotation: d,
36+
problem: d.problem,
37+
// in case just of cases/death replace with custom data source name
38+
source: resolveDataSource(d),
39+
reference: d.reference,
40+
dateRange: `${formatDateISO(d.dates[0])} - ${formatDateISO(d.dates[1])}`,
41+
};
42+
}
43+
44+
const sort = new SortHelper('dateRange', true, 'dateRange');
45+
$: data = $annotationManager.annotations.map(simplifyAnnotation);
46+
$: sortedRows = data.sort($sort.comparator);
47+
48+
let details = -1;
49+
50+
function signals(signals) {
51+
return signals === '*' ? 'All Signals' : [...signals].join(', ');
52+
}
53+
function region(regions) {
54+
const mapped = regions
55+
.map((d) => {
56+
if (d.level === 'nation') {
57+
return 'Whole USA';
58+
}
59+
if (d.level === 'state') {
60+
return d.ids === '*' ? 'All US States' : [...d.ids].map((d) => d.toUpperCase()).join(', ');
61+
}
62+
return null;
63+
})
64+
.filter((d) => d != null);
65+
if (mapped.length < regions.length) {
66+
mapped.push('...');
67+
}
68+
return mapped.join(', ');
69+
}
70+
function regionLong(regions) {
71+
return regions
72+
.map((d) => {
73+
if (d.level === 'nation') {
74+
return 'Whole USA';
75+
}
76+
if (d.level === 'state') {
77+
return d.ids === '*'
78+
? 'All US States'
79+
: Array.from(d.ids, (d) => getInfoByName(d))
80+
.map((d) => d.displayName)
81+
.join(', ');
82+
}
83+
if (d.level === 'county') {
84+
return d.ids === '*'
85+
? 'All US Counties'
86+
: Array.from(d.ids, (d) => getInfoByName(d))
87+
.map((d) => d.displayName)
88+
.join(', ');
89+
}
90+
const level = getLevelInfo(d.level);
91+
return d.ids === '*' ? `All ${level.labelPlural}` : `${level.label}: ${[...d.ids].join(', ')}`;
92+
})
93+
.join(', ');
94+
}
95+
</script>
96+
97+
<table class="mobile-table">
98+
<thead>
99+
<tr>
100+
<th />
101+
<th class="mobile-th">Data Source</th>
102+
<th class="mobile-th">Problem</th>
103+
<th class="mobile-th">Date Range</th>
104+
<th class="mobile-th">Regions</th>
105+
<th class="mobile-th">Source</th>
106+
</tr>
107+
<tr>
108+
<th />
109+
<th class="sort-indicator uk-text-center">
110+
<SortColumnIndicator label="Data Source" {sort} prop="source" />
111+
</th>
112+
<th class="sort-indicator uk-text-center">
113+
<SortColumnIndicator label="Problem" {sort} prop="problem" />
114+
</th>
115+
<th class="sort-indicator uk-text-center">
116+
<SortColumnIndicator label="Date Range" {sort} prop="dateRange" />
117+
</th>
118+
<th class="sort-indicator uk-text-center" />
119+
<th class="sort-indicator uk-text-center" />
120+
</tr>
121+
</thead>
122+
<tbody>
123+
{#each sortedRows as r}
124+
<tr>
125+
<td>
126+
<button
127+
type="button"
128+
title="Toggle Details"
129+
on:click={() => (details = details === r.i ? -1 : r.i)}
130+
class="toggle-details"
131+
class:open={details === r.i}
132+
>
133+
{@html chevronRightIcon}
134+
</button>
135+
</td><td>{r.source}</td>
136+
<td>{r.problem}</td>
137+
<td>
138+
<span class="uk-text-nowrap">{formatDateISO(r.annotation.dates[0])}</span>
139+
-
140+
<span class="uk-text-nowrap">{formatDateISO(r.annotation.dates[1])}</span>
141+
</td>
142+
<td>{region(r.annotation.regions)}</td>
143+
<td>
144+
{#if r.reference}
145+
<a href={r.refernce} class="uk-link-text details-link">
146+
{@html ExternalLinkIcon}
147+
</a>
148+
{/if}
149+
</td>
150+
</tr>
151+
{#if details === r.i}
152+
<tr>
153+
<td />
154+
<td colspan="5">
155+
<p>
156+
{r.annotation.explanation}
157+
</p>
158+
<p><strong>Affected Signals:</strong> {signals(r.annotation.signals)}</p>
159+
<p><strong>Affected Regions:</strong> {regionLong(r.annotation.regions)}</p>
160+
</td>
161+
</tr>
162+
{/if}
163+
{/each}
164+
</tbody>
165+
</table>
166+
167+
<style>
168+
.toggle-details {
169+
border: none;
170+
background: none;
171+
padding: 0;
172+
width: 0.8em;
173+
cursor: pointer;
174+
transition: transform 0.25s linear;
175+
}
176+
177+
.open {
178+
transform: rotate(90deg);
179+
}
180+
181+
.toggle-details > :global(svg) {
182+
width: 1em;
183+
}
184+
185+
.details-link {
186+
width: 1em;
187+
display: inline-block;
188+
fill: currentColor;
189+
}
190+
</style>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<script>
2+
import AnnotationTable from './AnnotationTable.svelte';
3+
import '../mobile/common.css';
4+
</script>
5+
6+
<div class="root">
7+
<div class="mobile-header-line-bg">
8+
<div class="mobile-header-line">
9+
<h2>Data <span>Anomalies</span></h2>
10+
</div>
11+
</div>
12+
<div class="uk-container content-grid">
13+
<div class="grid-3-11">
14+
<AnnotationTable />
15+
</div>
16+
</div>
17+
</div>
18+
19+
<style>
20+
.root {
21+
position: relative;
22+
flex: 1 1 0;
23+
font-size: 0.875rem;
24+
line-height: 1.5rem;
25+
}
26+
.content-grid {
27+
grid-row-gap: 0;
28+
}
29+
</style>

src/modes/index.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,18 @@ const modes = [
7070
(r) => r.default,
7171
),
7272
},
73+
{
74+
id: 'data-anomalies',
75+
label: 'Data Anomalies',
76+
component: () =>
77+
import(/* webpackChunkName: 'm-data-anomalies' */ './data-anomalies/DataAnomalies.svelte').then((r) => r.default),
78+
},
7379
];
7480

7581
export default modes;
7682

7783
/**
78-
* @type {Record<'summary'|'timelapse'|'top10'|'export'|'single'|'survey-results'|'lab'|'classic'|'indicator'|'landing'|'indicator-status', Mode>}
84+
* @type {Record<'summary'|'timelapse'|'top10'|'export'|'single'|'survey-results'|'lab'|'classic'|'indicator'|'landing'|'indicator-status'|'data-anomalies', Mode>}
7985
*/
8086
export const modeByID = {};
8187
modes.forEach((mode) => (modeByID[mode.id] = mode));

src/modes/indicator-status/IndicatorStatusTable.svelte

Lines changed: 9 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,21 @@
11
<script>
2-
import SortColumnIndicator from '../mobile/SortColumnIndicator.svelte';
2+
import SortColumnIndicator from '../mobile/components/SortColumnIndicator.svelte';
33
import FancyHeader from '../mobile/FancyHeader.svelte';
44
import { formatDateISO, formatDateShortNumbers, formatFraction } from '../../formats';
55
import DownloadMenu from '../mobile/components/DownloadMenu.svelte';
66
import Vega from '../../components/Vega.svelte';
77
import { generateSparkLine } from '../../specs/lineSpec';
88
import { createEventDispatcher } from 'svelte';
99
import chevronRightIcon from '!raw-loader!@fortawesome/fontawesome-free/svgs/solid/chevron-right.svg';
10+
import { SortHelper } from '../mobile/components/tableUtils';
1011
1112
const dispatch = createEventDispatcher();
1213
/**
1314
* @type {Date}
1415
*/
1516
export let date;
1617
17-
let sortCriteria = 'name';
18-
let sortDirectionDesc = false;
19-
20-
function bySortCriteria(sortCriteria, sortDirectionDesc) {
21-
const less = sortDirectionDesc ? 1 : -1;
22-
23-
function clean(a) {
24-
// normalize NaN to null
25-
return typeof a === 'number' && Number.isNaN(a) ? null : a;
26-
}
27-
return (a, b) => {
28-
const av = clean(a[sortCriteria]);
29-
const bv = clean(b[sortCriteria]);
30-
if ((av == null) !== (bv == null)) {
31-
return av == null ? 1 : -1;
32-
}
33-
if (av !== bv) {
34-
return av < bv ? less : -less;
35-
}
36-
if (a.name !== b.name) {
37-
return a.name < b.name ? less : -less;
38-
}
39-
return 0;
40-
};
41-
}
42-
43-
function sortClick(prop, defaultSortDesc = false) {
44-
if (sortCriteria === prop) {
45-
sortDirectionDesc = !sortDirectionDesc;
46-
return;
47-
}
48-
sortCriteria = prop;
49-
sortDirectionDesc = defaultSortDesc;
50-
}
51-
52-
$: comparator = bySortCriteria(sortCriteria, sortDirectionDesc);
53-
18+
const sort = new SortHelper('name', false, 'name');
5419
/**
5520
* @type {Promise<import('./data').ExtendedStatus[]>}
5621
*/
@@ -68,6 +33,7 @@
6833
$: {
6934
loading = true;
7035
sortedData = [];
36+
const comparator = $sort.comparator;
7137
data.then((rows) => {
7238
sortedData = rows.slice().sort(comparator);
7339
loading = false;
@@ -121,44 +87,19 @@
12187
</tr>
12288
<tr>
12389
<th class="sort-indicator uk-text-center">
124-
<SortColumnIndicator
125-
label="Name"
126-
on:click={() => sortClick('name')}
127-
sorted={sortCriteria === 'name'}
128-
desc={sortDirectionDesc}
129-
/>
90+
<SortColumnIndicator label="Name" {sort} prop="name" />
13091
</th>
13192
<th class="sort-indicator">
132-
<SortColumnIndicator
133-
label="Latest Issue Date"
134-
on:click={() => sortClick('latest_issue')}
135-
sorted={sortCriteria === 'latest_issue'}
136-
desc={sortDirectionDesc}
137-
/>
93+
<SortColumnIndicator label="Latest Issue Date" {sort} prop="latest_issue" />
13894
</th>
13995
<th class="sort-indicator">
140-
<SortColumnIndicator
141-
label="Latest Data Date"
142-
on:click={() => sortClick('latest_time_value')}
143-
sorted={sortCriteria === 'latest_time_value'}
144-
desc={sortDirectionDesc}
145-
/>
96+
<SortColumnIndicator label="Latest Data Date" {sort} prop="latest_time_value" />
14697
</th>
14798
<th class="sort-indicator">
148-
<SortColumnIndicator
149-
label="Lag"
150-
on:click={() => sortClick('latest_lag')}
151-
sorted={sortCriteria === 'latest_lag'}
152-
desc={sortDirectionDesc}
153-
/>
99+
<SortColumnIndicator label="Lag" {sort} prop="latest_lag" />
154100
</th>
155101
<th class="sort-indicator">
156-
<SortColumnIndicator
157-
label="Latest Coverage"
158-
on:click={() => sortClick('latest_coverage')}
159-
sorted={sortCriteria === 'latest_coverage'}
160-
desc={sortDirectionDesc}
161-
/>
102+
<SortColumnIndicator label="Latest Coverage" {sort} prop="latest_coverage" />
162103
</th>
163104
<th class="sort-indicator" />
164105
<th class="sort-indicator" />

0 commit comments

Comments
 (0)