|
<!DOCTYPE html> |
|
<html> |
|
|
|
<head> |
|
<title>COVID-19 Perspective Workspace</title> |
|
<meta name="viewport" |
|
content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" /> |
|
|
|
<script src="https://perspective-covid.herokuapp.com/static/perspective-workspace/umd/perspective-workspace.js"> |
|
</script> |
|
<script |
|
src="https://perspective-covid.herokuapp.com/static/perspective-viewer-datagrid/umd/perspective-viewer-datagrid.js"> |
|
</script> |
|
<script src="https://perspective-covid.herokuapp.com/static/perspective-viewer-d3fc/umd/perspective-viewer-d3fc.js"> |
|
</script> |
|
<script src="https://perspective-covid.herokuapp.com/static/perspective/umd/perspective.js"></script> |
|
<link rel="stylesheet" |
|
href="https://perspective-covid.herokuapp.com/static/perspective-workspace/umd/material.css" /> |
|
|
|
<style> |
|
@import url(https://fonts.googleapis.com/css?family=Inconsolata); |
|
|
|
td, tr { |
|
font-family: "Inconsolata", monospace; |
|
font-size: 15px !important; |
|
} |
|
body { |
|
display: flex; |
|
flex-direction: column; |
|
position: absolute; |
|
top: 0; |
|
left: 0; |
|
right: 0; |
|
bottom: 0; |
|
margin: 0; |
|
padding: 0; |
|
overflow: hidden; |
|
} |
|
</style> |
|
</head> |
|
|
|
<body> |
|
<perspective-workspace id="workspace"></perspective-workspace> |
|
|
|
<script> |
|
const worker = perspective.shared_worker(); |
|
const state_data = async websocket => { |
|
// Get a proxy for a view named "state_data_source", registered on |
|
// the server with a reciprocal call to `host_view()`. |
|
// No data is transferred, `view` is a virtual handle for data on |
|
// the server. |
|
const view = websocket.open_view("state_data_source"); |
|
|
|
// Create a `table` from this, owned by the local WebWorker. |
|
// Data is transferred from `view` to the local WebWorker, both |
|
// the current state and all future updates, as Arrows. |
|
return worker.table(view); |
|
}; |
|
|
|
const county_data = async websocket => { |
|
// Get a proxy for a view named "state_data_source", registered on |
|
// the server with a reciprocal call to `host_view()`. |
|
// No data is transferred, `view` is a virtual handle for data on |
|
// the server. |
|
const view = websocket.open_view("county_data_source"); |
|
|
|
// Create a `table` from this, owned by the local WebWorker. |
|
// Data is transferred from `view` to the local WebWorker, both |
|
// the current state and all future updates, as Arrows. |
|
return worker.table(view); |
|
}; |
|
|
|
window.addEventListener("load", async () => { |
|
const websocket = perspective.websocket( |
|
"wss://perspective-covid.herokuapp.com/ws" |
|
); |
|
|
|
window.workspace.addTable("state", state_data(websocket)); |
|
window.workspace.addTable("county", county_data(websocket)); |
|
|
|
const line_config = window.getPlugin("d3_y_line"); |
|
line_config.max_cells = 250000; |
|
line_config.max_columns = 7500; |
|
|
|
await window.workspace.restore({ |
|
sizes: [0.23643658663883088, 0.7635634133611691], |
|
detail: { |
|
main: { |
|
type: "split-area", |
|
orientation: "horizontal", |
|
children: [ |
|
{ |
|
type: "tab-area", |
|
widgets: [ |
|
"CountyCasesWidget" |
|
], |
|
currentIndex: 0 |
|
}, |
|
{ |
|
type: "split-area", |
|
orientation: "vertical", |
|
children: [{ |
|
type: "tab-area", |
|
widgets: ["CountyScatterWidget"], |
|
currentIndex: 0 |
|
}, |
|
{ |
|
type: "tab-area", |
|
widgets: ["CountyPopulationWidget"], |
|
currentIndex: 0 |
|
} |
|
], |
|
sizes: [0.5, 0.5] |
|
}, |
|
], |
|
sizes: [0.5, 0.5] |
|
} |
|
}, |
|
master: { |
|
widgets: ["MasterStateWidget", "MasterDateWidget"] |
|
}, |
|
mode: "globalFilters", |
|
viewers: { |
|
MasterStateWidget: { |
|
plugin: "datagrid", |
|
name: "Cases & Deaths by State", |
|
table: "state", |
|
"computed-columns": [ |
|
"'New Deaths' % 'New Cases' as 'Fatality Rate (%)'" |
|
], |
|
columns: [ |
|
"Fatality Rate (%)", |
|
"New Cases", |
|
"New Deaths" |
|
], |
|
"row-pivots": ["State Name"], |
|
aggregates: { |
|
"Cumulative Cases": "high", |
|
"Cumulative Deaths": "high", |
|
"Fatality Rate (%)": "avg", |
|
"Date": "dominant" |
|
}, |
|
sort: [ |
|
["Cumulative Cases", "desc"] |
|
] |
|
}, |
|
MasterDateWidget: { |
|
plugin: "datagrid", |
|
name: "Cases & Deaths by Date", |
|
table: "state", |
|
"computed-columns": [ |
|
"'New Deaths' % 'New Cases' as 'Fatality Rate (%)'" |
|
], |
|
columns: [ |
|
"Fatality Rate (%)", |
|
"New Cases", |
|
"New Deaths" |
|
], |
|
sort: [ |
|
["Date", "desc"] |
|
], |
|
"row-pivots": ["Date"], |
|
aggregates: { |
|
"Cumulative Cases": "high", |
|
"Cumulative Deaths": "high", |
|
"Fatality Rate (%)": "avg", |
|
Date: "dominant" |
|
} |
|
}, |
|
CountyCasesWidget: { |
|
plugin: "d3_y_line", |
|
name: "Cases since March 1st", |
|
table: "county", |
|
columns: ["Cumulative Cases"], |
|
"row-pivots": ["Date"], |
|
"column-pivots": ["Location"], |
|
filters: [["Date", ">", "03/01/2020"]], |
|
aggregates: { |
|
"Date": "dominant", |
|
"Cumulative Cases": "high", |
|
"Cumulative Deaths": "high", |
|
"Population (2018 Estimate)": "high", |
|
"Unemployment Rate % (2018 Estimate)": "high", |
|
"Unemployed (2018 Estimate)": "high", |
|
"Employed (2018 Estimate)": "high", |
|
"Civilian Labor Force (2018 Estimate)": "high", |
|
"Median Household Income (2018 Estimate)": "high", |
|
}, |
|
"computed-columns": [ |
|
'concat_comma("County", "State") as "Location"' |
|
], |
|
plugin_config: { |
|
legend: { |
|
left: "80px", |
|
top: "15px" |
|
} |
|
} |
|
}, |
|
CountyScatterWidget: { |
|
plugin: "d3_xy_scatter", |
|
name: "Cases & Deaths Scatter Plot (by county)", |
|
table: "county", |
|
columns: ["New Cases", "New Deaths"], |
|
"row-pivots": ["Location"], |
|
aggregates: { |
|
"Date": "dominant", |
|
"Cumulative Cases": "high", |
|
"Cumulative Deaths": "high", |
|
"Population (2018 Estimate)": "high", |
|
"Unemployment Rate % (2018 Estimate)": "high", |
|
"Unemployed (2018 Estimate)": "high", |
|
"Employed (2018 Estimate)": "high", |
|
"Civilian Labor Force (2018 Estimate)": "high", |
|
"Median Household Income (2018 Estimate)": "high", |
|
}, |
|
"computed-columns": [ |
|
'concat_comma("County", "State") as "Location"' |
|
] |
|
}, |
|
CountyPopulationWidget: { |
|
name: "Cases/Deaths with county-level population, income, unemployment data", |
|
table: "county", |
|
plugin: "datagrid", |
|
columns: [ |
|
"Cases % Population", |
|
"New Cases", |
|
"New Deaths", |
|
"Population (2018 Estimate)", |
|
"Unemployed (2018 Estimate)", |
|
"Employed (2018 Estimate)", |
|
"Civilian Labor Force (2018 Estimate)", |
|
"Median Household Income (2018 Estimate)", |
|
], |
|
sort: [["Population (2018 Estimate)", "desc"]], |
|
aggregates: { |
|
"Cases % Population": "avg", |
|
"Date": "dominant", |
|
"Cumulative Cases": "high", |
|
"Cumulative Deaths": "high", |
|
"Population (2018 Estimate)": "high", |
|
"Unemployment Rate % (2018 Estimate)": "high", |
|
"Unemployed (2018 Estimate)": "high", |
|
"Employed (2018 Estimate)": "high", |
|
"Civilian Labor Force (2018 Estimate)": "high", |
|
"Median Household Income (2018 Estimate)": "high", |
|
}, |
|
"row-pivots": ["Location"], |
|
"computed-columns": [ |
|
'concat_comma("County", "State") as "Location"', |
|
"'New Cases' % 'Population (2018 Estimate)' as 'Cases % Population'" |
|
] |
|
}, |
|
} |
|
}); |
|
|
|
// Custom formatting for the datagrid - display percentages with a `%` |
|
for (const viewer of document |
|
.getElementById("workspace") |
|
.querySelectorAll("perspective-viewer")) { |
|
viewer.addEventListener( |
|
"perspective-datagrid-after-update", |
|
event => { |
|
const datagrid = event.detail; |
|
for (const td of datagrid.get_tds()) { |
|
const metadata = datagrid.get_meta(td); |
|
if ( |
|
metadata.column.includes("%") && |
|
typeof metadata.value === "number" |
|
) { |
|
const pct = metadata.value |
|
.toFixed(4) |
|
.toString(); |
|
td.textContent = pct + "%"; |
|
} else if (metadata.column === "Median Household Income (2018 Estimate)") { |
|
td.textContent = "$" + metadata.value; |
|
} else { |
|
td.style.display = "table-cell"; |
|
} |
|
} |
|
} |
|
); |
|
} |
|
}); |
|
</script> |
|
</body> |
|
|
|
</html> |