Skip to content

Commit 6e8dac5

Browse files
authored
Merge pull request #1182 from cmu-delphi/release/v3.2.2
Release v3.2.2
2 parents d5373bd + 1d26d0a commit 6e8dac5

File tree

10 files changed

+277
-56
lines changed

10 files changed

+277
-56
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "www-covidcast",
3-
"version": "3.2.1",
3+
"version": "3.2.2",
44
"private": true,
55
"license": "MIT",
66
"description": "",

scripts/generateDescriptions.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,14 @@ function convertDescriptions(code) {
114114
casesOrDeathSignals: parseNestedOrString,
115115
mapTitleText: parseNestedOrString,
116116
unitShort: (v) => v || '',
117+
overrides: (v) =>
118+
parseObject(v, {
119+
county: parseObject,
120+
state: parseObject,
121+
nation: parseObject,
122+
hhs: parseObject,
123+
msa: parseObject,
124+
}),
117125
ageStratifications: parseArray,
118126
});
119127
});

src/data/fetchTriple.ts

Lines changed: 123 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,8 @@ function toGeoPair(
4242
function toSourceSignalPair<S extends { id: string; signal: string; valueScaleFactor?: number }>(
4343
transfer: (keyof EpiDataJSONRow)[],
4444
mixinValues: Partial<EpiDataRow>,
45-
sensor: S | readonly S[],
45+
sensor: readonly S[],
4646
) {
47-
if (!isArray(sensor)) {
48-
mixinValues.source = sensor.id;
49-
mixinValues.signal = sensor.signal;
50-
return {
51-
factor: sensor.valueScaleFactor ?? 1,
52-
sourceSignalPairs: SourceSignalPair.from(sensor),
53-
};
54-
}
5547
const grouped = groupBySource(sensor);
5648

5749
let factor: number | ((row: EpiDataRow) => number);
@@ -97,8 +89,58 @@ function toSourceSignalPair<S extends { id: string; signal: string; valueScaleFa
9789
};
9890
}
9991

92+
function resolveBackwardOverrides(
93+
rows: EpiDataRow[],
94+
overrides: { level: RegionLevel; fromId: string; fromSignal: string; toId: string; toSignal: string }[],
95+
): EpiDataRow[] {
96+
if (overrides.length === 0) {
97+
return rows;
98+
}
99+
function toKey(id: string, signal: string, level: RegionLevel) {
100+
return `${id}@${signal}@${level}`;
101+
}
102+
const over = new Map(overrides.map((o) => [toKey(o.toId, o.toSignal, o.level), o]));
103+
for (const row of rows) {
104+
const key = toKey(row.source, row.signal, row.geo_type);
105+
const signalOverride = over.get(key);
106+
if (signalOverride) {
107+
row.source = signalOverride.fromId;
108+
row.signal = signalOverride.fromSignal;
109+
}
110+
}
111+
return rows;
112+
}
113+
114+
function mapOverrides(
115+
overrides: { level: RegionLevel; fromId: string; fromSignal: string; toId: string; toSignal: string }[],
116+
typeSensors: readonly { id: string; signal: string; valueScaleFactor?: number }[],
117+
) {
118+
if (overrides.length === 0) {
119+
return typeSensors;
120+
}
121+
return typeSensors.map((d) => {
122+
for (const o of overrides) {
123+
if (o.fromId === d.id && o.fromSignal === d.signal) {
124+
return {
125+
id: o.toId,
126+
signal: o.toSignal,
127+
valueScaleFactor: d.valueScaleFactor,
128+
};
129+
}
130+
}
131+
return d;
132+
});
133+
}
134+
100135
export default function fetchTriple<
101-
S extends { id: string; signal: string; format: Sensor['format']; isWeeklySignal: boolean },
136+
S extends {
137+
id: string;
138+
signal: string;
139+
format: Sensor['format'];
140+
isWeeklySignal: boolean;
141+
overrides?: Sensor['overrides'];
142+
valueScaleFactor?: number;
143+
},
102144
>(
103145
sensor: S | readonly S[],
104146
region: Region | RegionLevel | readonly Region[],
@@ -120,6 +162,31 @@ export default function fetchTriple<
120162
return asOf;
121163
}
122164

165+
function resolveForwardOverrides(geoPairs: GeoPair | GeoPair[], typeSensors: readonly S[]) {
166+
const levels = Array.from(
167+
new Set<RegionLevel>(Array.isArray(geoPairs) ? geoPairs.map((d) => d.level) : [geoPairs.level]),
168+
);
169+
const overrides: { level: RegionLevel; fromId: string; fromSignal: string; toId: string; toSignal: string }[] = [];
170+
for (const sensor of typeSensors) {
171+
if (!sensor.overrides) {
172+
continue;
173+
}
174+
for (const level of levels) {
175+
if (sensor.overrides[level] != null) {
176+
// override
177+
overrides.push({
178+
level,
179+
fromId: sensor.id,
180+
fromSignal: sensor.signal,
181+
toId: sensor.overrides[level]!.id,
182+
toSignal: sensor.overrides[level]!.signal,
183+
});
184+
}
185+
}
186+
}
187+
return { overrides, levels };
188+
}
189+
123190
function fetchImpl(
124191
type: 'day' | 'week',
125192
geoPairs: GeoPair | GeoPair[],
@@ -128,7 +195,6 @@ export default function fetchTriple<
128195
typedMixinValues: Partial<EpiDataRow>,
129196
) {
130197
typedMixinValues.time_type = type;
131-
const { sourceSignalPairs, factor } = toSourceSignalPair(typedTransfer, typedMixinValues, typeSensors);
132198
if (date instanceof Date) {
133199
// single level and single date
134200
typedMixinValues.time_value = type === 'day' ? toTimeValue(date) : toTimeWeekValue(date);
@@ -137,9 +203,52 @@ export default function fetchTriple<
137203
} else {
138204
typedTransfer.push('time_value');
139205
}
140-
return callAPI(type, sourceSignalPairs, geoPairs, new TimePair(type, date), typedTransfer, {
141-
asOf: fixAsOf(),
142-
}).then((rows) => parseData(rows, typedMixinValues, factor));
206+
const timePair = new TimePair(type, date);
207+
208+
const { overrides, levels } = resolveForwardOverrides(geoPairs, typeSensors);
209+
210+
if (overrides.length === 0 || levels.length === 1) {
211+
// simple case: none or direct replacement
212+
const mappedSensors = mapOverrides(overrides, typeSensors);
213+
const { sourceSignalPairs, factor } = toSourceSignalPair(typedTransfer, typedMixinValues, mappedSensors);
214+
return callAPI(type, sourceSignalPairs, geoPairs, timePair, typedTransfer, {
215+
asOf: fixAsOf(),
216+
}).then((rows) => resolveBackwardOverrides(parseData(rows, typedMixinValues, factor), overrides));
217+
}
218+
219+
// multiple calls one for each mapped level
220+
const mappedLevels = Array.from(new Set(overrides.map((d) => d.level)));
221+
const calls: Promise<EpiDataRow[]>[] = [];
222+
const geo = Array.isArray(geoPairs) ? geoPairs : [geoPairs];
223+
for (const mappedLevel of mappedLevels) {
224+
// compute subset of what needs to be mapped and can be transferred at once
225+
const levelOverrides = overrides.filter((d) => d.level === mappedLevel);
226+
const levelGeo = geo.filter((d) => d.level === mappedLevel);
227+
228+
const mappedSensors = mapOverrides(levelOverrides, typeSensors);
229+
const levelTransfer = typedTransfer.slice();
230+
const levelMixins = { ...typedMixinValues };
231+
const { sourceSignalPairs, factor } = toSourceSignalPair(levelTransfer, levelMixins, mappedSensors);
232+
calls.push(
233+
callAPI(type, sourceSignalPairs, levelGeo, timePair, levelTransfer, {
234+
asOf: fixAsOf(),
235+
}).then((rows) => resolveBackwardOverrides(parseData(rows, levelMixins, factor), levelOverrides)),
236+
);
237+
}
238+
const unmappedLevels = levels.filter((d) => !mappedLevels.includes(d));
239+
if (unmappedLevels.length > 0) {
240+
// compute subset of what needs to be mapped and can be transferred at once
241+
const levelGeo = geo.filter((d) => unmappedLevels.includes(d.level));
242+
const levelTransfer = typedTransfer.slice();
243+
const levelMixins = { ...typedMixinValues };
244+
const { sourceSignalPairs, factor } = toSourceSignalPair(levelTransfer, levelMixins, typeSensors);
245+
calls.push(
246+
callAPI(type, sourceSignalPairs, levelGeo, timePair, levelTransfer, {
247+
asOf: fixAsOf(),
248+
}).then((rows) => parseData(rows, levelMixins, factor)),
249+
);
250+
}
251+
return Promise.all(calls).then((r) => ([] as EpiDataRow[]).concat(...r));
143252
}
144253

145254
const [day, week] = splitDailyWeekly(sensor);

src/data/sensor.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ export interface Sensor {
8888

8989
readonly formatSpecifier: string;
9090
formatValue(v?: number | null, enforceSign?: boolean): string;
91+
92+
readonly overrides?: Partial<Record<RegionLevel, { id: string; signal: string }>>;
9193
}
9294

9395
function determineHighValuesAre(sensor: {

src/data/trend.ts

Lines changed: 117 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { EpiDataRow } from './fetchData';
22
import type { Sensor } from '../stores/constants';
33
import { callTrendAPI, EpiDataTrendRow, FieldSpec } from './api';
44
import { GeoPair, SourceSignalPair } from './apimodel';
5-
import type { Region } from './regions';
5+
import type { Region, RegionLevel } from './regions';
66
import { splitDailyWeekly } from './sensor';
77
import type { TimeFrame } from './TimeFrame';
88
import { parseAPITime, toTimeValue } from './utils';
@@ -119,32 +119,131 @@ export function asSensorTrend(
119119
return t;
120120
}
121121

122-
export function fetchTrend(
123-
signal: Sensor | Sensor[],
124-
region: Region | Region[],
122+
export function fetchTrendSR(
123+
signal: Sensor,
124+
region: Region,
125125
date: Date,
126126
window: TimeFrame,
127-
fields?: FieldSpec<EpiDataTrendRow>,
128127
): Promise<EpiDataTrendRow[]> {
129-
const geo = Array.isArray(region) ? GeoPair.fromArray(region) : GeoPair.from(region);
130-
if (!Array.isArray(signal)) {
128+
const geo = GeoPair.from(region);
129+
let source = SourceSignalPair.from(signal);
130+
if (signal.overrides && signal.overrides[region.level]) {
131+
// need to map but no need to unmap since not transferred
132+
source = SourceSignalPair.from(signal.overrides[region.level]!);
133+
}
134+
return callTrendAPI(
135+
signal.isWeeklySignal ? 'week' : 'day',
136+
source,
137+
geo,
138+
date,
139+
window,
140+
signal.isWeeklySignal ? 1 : 7,
141+
{ exclude: ['geo_type', 'geo_value', 'signal_signal', 'signal_source'] },
142+
);
143+
}
144+
145+
export function fetchTrendR(
146+
signal: Sensor,
147+
regions: Region[],
148+
date: Date,
149+
window: TimeFrame,
150+
): Promise<EpiDataTrendRow[]> {
151+
const calls: Promise<EpiDataTrendRow[]>[] = [];
152+
// for each mapped level
153+
for (const level of Object.keys(signal.overrides || {})) {
154+
const levelRegions = regions.filter((d) => d.level === level);
155+
if (levelRegions.length === 0) {
156+
continue;
157+
}
158+
calls.push(
159+
callTrendAPI(
160+
signal.isWeeklySignal ? 'week' : 'day',
161+
SourceSignalPair.from(signal.overrides![level as RegionLevel]!),
162+
GeoPair.fromArray(levelRegions),
163+
date,
164+
window,
165+
signal.isWeeklySignal ? 1 : 7,
166+
{
167+
exclude: ['signal_signal', 'signal_source'],
168+
},
169+
),
170+
);
171+
}
172+
// all not mapped ones
173+
const rest = regions.filter((d) => !signal.overrides || signal.overrides[d.level] == null);
174+
if (rest.length > 0) {
175+
calls.push(
176+
callTrendAPI(
177+
signal.isWeeklySignal ? 'week' : 'day',
178+
SourceSignalPair.from(signal),
179+
GeoPair.fromArray(rest),
180+
date,
181+
window,
182+
signal.isWeeklySignal ? 1 : 7,
183+
{
184+
exclude: ['signal_signal', 'signal_source'],
185+
},
186+
),
187+
);
188+
}
189+
190+
if (calls.length === 1) {
191+
return calls[0];
192+
}
193+
return Promise.all(calls).then((r) => ([] as EpiDataTrendRow[]).concat(...r));
194+
}
195+
196+
export function fetchTrendS(
197+
signal: Sensor[],
198+
region: Region,
199+
date: Date,
200+
window: TimeFrame,
201+
): Promise<EpiDataTrendRow[]> {
202+
const geo = GeoPair.from(region);
203+
const fields: FieldSpec<EpiDataTrendRow> = { exclude: ['geo_type', 'geo_value'] };
204+
205+
function fetchMultiSignals(type: 'day' | 'week', sensors: Sensor[]) {
206+
if (sensors.length === 0) {
207+
return [];
208+
}
209+
const lookup = new Map<string, Sensor>();
210+
const mapped = sensors.map((s) => {
211+
const override = s.overrides?.[region.level];
212+
if (override) {
213+
lookup.set(`${override.id}@${override.signal}`, s);
214+
// map forward
215+
return override;
216+
}
217+
return s;
218+
});
131219
return callTrendAPI(
132-
signal.isWeeklySignal ? 'week' : 'day',
133-
SourceSignalPair.from(signal),
220+
type,
221+
SourceSignalPair.fromArray(mapped),
134222
geo,
135223
date,
136224
window,
137-
signal.isWeeklySignal ? 1 : 7,
225+
type == 'week' ? 1 : 7,
138226
fields,
139-
);
227+
).then((rows) => {
228+
if (lookup.size === 0) {
229+
return rows;
230+
}
231+
// map back
232+
for (const row of rows) {
233+
const key = `${row.signal_source}@${row.signal_signal}`;
234+
const base = lookup.get(key);
235+
if (base) {
236+
row.signal_source = base.id;
237+
row.signal_signal = base.signal;
238+
}
239+
}
240+
return rows;
241+
});
140242
}
141-
return Promise.all(
142-
splitDailyWeekly(signal).map(({ type, sensors }) =>
143-
sensors.length === 0
144-
? []
145-
: callTrendAPI(type, SourceSignalPair.fromArray(sensors), geo, date, window, type == 'week' ? 1 : 7, fields),
146-
),
147-
).then((r) => ([] as EpiDataTrendRow[]).concat(...r));
243+
244+
return Promise.all(splitDailyWeekly(signal).map(({ type, sensors }) => fetchMultiSignals(type, sensors))).then((r) =>
245+
([] as EpiDataTrendRow[]).concat(...r),
246+
);
148247
}
149248

150249
export function computeLatest(

0 commit comments

Comments
 (0)