Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions src/api/EpiData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export function loadDataSet(
fixedParams: Record<string, unknown>,
userParams: Record<string, unknown>,
columns: string[],
api_key = '',
): Promise<DataGroup | null> {
const duplicates = get(expandedDataGroups).filter((d) => d.title == title);
if (duplicates.length > 0) {
Expand All @@ -124,7 +125,11 @@ export function loadDataSet(
)
.then(() => null);
}
const url = new URL(ENDPOINT + `/${endpoint}/`);
let url_string = ENDPOINT + `/${endpoint}/`;
if (api_key !== '') {
url_string += `?api_key=${api_key}`;
}
const url = new URL(url_string);
const params = cleanParams(userParams);
Object.entries(fixedParams).forEach(([key, value]) => {
url.searchParams.set(key, String(value));
Expand All @@ -150,10 +155,14 @@ export function loadDataSet(
});
}

export function fetchCOVIDcastMeta(): Promise<
{ geo_type: string; signal: string; data_source: string; time_type?: string }[]
> {
const url = new URL(ENDPOINT + `/covidcast_meta/`);
export function fetchCOVIDcastMeta(
api_key: string,
): Promise<{ geo_type: string; signal: string; data_source: string; time_type?: string }[]> {
let url_string = ENDPOINT + `/covidcast_meta/`;
if (api_key !== '') {
url_string += `?api_key=${api_key}`;
}
const url = new URL(url_string);
url.searchParams.set('format', 'json');
return fetchImpl<{ geo_type: string; signal: string; data_source: string; time_type?: string }[]>(url).catch(
(error) => {
Expand Down Expand Up @@ -183,12 +192,14 @@ export function importCOVIDcast({
geo_value,
signal,
time_type = 'day',
api_key,
}: {
data_source: string;
signal: string;
time_type?: string;
geo_type: string;
geo_value: string;
api_key: string;
}): Promise<DataGroup | null> {
const title = `[API] COVIDcast: ${data_source}:${signal} (${geo_type}:${geo_value})`;
return loadDataSet(
Expand All @@ -203,6 +214,7 @@ export function importCOVIDcast({
},
{ data_source, signal, time_type, geo_type, geo_value },
['value', 'stderr', 'sample_size'],
api_key,
);
}

Expand Down
89 changes: 69 additions & 20 deletions src/components/dialogs/dataSources/COVIDcast.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@
export let id: string;
let api_key = '';
let data_source = '';
let signal = '';
let geo_type = '';
let geo_value = '';
let form_key = '';
let valid_key = true;
let dataSources: (LabelValue & { signals: string[] })[] = [];
let geoTypes: string[] = [];
Expand All @@ -23,38 +26,77 @@
}
}
onMount(() => {
fetchCOVIDcastMeta().then((res) => {
geoTypes = [...new Set(res.map((d) => d.geo_type))];
const byDataSource = new Map<string, LabelValue & { signals: string[] }>();
for (const row of res) {
const ds = byDataSource.get(row.data_source);
if (!ds) {
byDataSource.set(row.data_source, {
label: row.data_source,
value: row.data_source,
signals: [row.signal],
});
} else if (!ds.signals.includes(row.signal)) {
ds.signals.push(row.signal);
// Helper function; delay invoking "fn" until "ms" milliseconds have passed
const debounce = (fn: Function, ms = 500) => {
let timeoutId: ReturnType<typeof setTimeout>;
return function (this: any, ...args: any[]) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), ms);
};
};
function fetchMetadata() {
fetchCOVIDcastMeta(form_key).then((res) => {
if (res.length == 0) {
valid_key = form_key == ''; // mark key as valid if it's empty, otherwise invalid
} else {
valid_key = true;
api_key = form_key; // API key is valid -> use it to fetch data later on
geoTypes = [...new Set(res.map((d) => d.geo_type))];
const byDataSource = new Map<string, LabelValue & { signals: string[] }>();
for (const row of res) {
const ds = byDataSource.get(row.data_source);
if (!ds) {
byDataSource.set(row.data_source, {
label: row.data_source,
value: row.data_source,
signals: [row.signal],
});
} else if (!ds.signals.includes(row.signal)) {
ds.signals.push(row.signal);
}
}
byDataSource.forEach((entry) => {
entry.signals.sort();
});
dataSources = [...byDataSource.values()].sort((a, b) => a.value.localeCompare(b.value));
}
byDataSource.forEach((entry) => {
entry.signals.sort();
});
dataSources = [...byDataSource.values()].sort((a, b) => a.value.localeCompare(b.value));
});
}
onMount(() => {
fetchMetadata();
});
export function importDataSet() {
return fetchCOVIDcastMeta().then((res) => {
return fetchCOVIDcastMeta(api_key).then((res) => {
const meta = res.filter((row) => row.data_source === data_source && row.signal === signal);
const time_type = meta[0].time_type;
return importCOVIDcast({ data_source, signal, geo_type, geo_value, time_type });
return importCOVIDcast({ data_source, signal, geo_type, geo_value, time_type, api_key });
});
}
</script>

<div>
<label class="uk-form-label" for="{id}-apikey">
<a href="https://cmu-delphi.github.io/delphi-epidata/api/api_keys.html">API Key</a> (optional)
</label>
<div class="uk-form-controls">
<input
id="{id}-apikey"
type="text"
class="uk-input"
class:uk-form-danger={!valid_key}
name="api_key"
required={false}
bind:value={form_key}
on:input={debounce(() => fetchMetadata(), 500)}
/>
{#if !valid_key}
<div class="invalid">API key is invalid - ignoring</div>
{/if}
</div>
</div>
<SelectField id="{id}-r" label="Data Source" name="data_source" bind:value={data_source} options={dataSources} />
<SelectField id="{id}-r" label="Data Signal" name="signal" bind:value={signal} options={dataSignals} />
<SelectField id="{id}-gt" label="Geographic Type" bind:value={geo_type} name="geo_type" options={geoTypes} />
Expand All @@ -65,3 +107,10 @@
name="geo_values"
placeholder="e.g., PA or 42003"
/>

<style>
.invalid {
color: red;
font-size: 0.75rem;
}
</style>