Posted on 5/8/2020
Tags:
Programming
COVID-19 has loomed over us for many months. For weeks, I watched the spread using graphs on
politico.com.
A few days went by without updates so I investigated recreating the graphs myself. Here’s my process:
1. I noticed that the website links to its data source. Fortunately, it comes from an open website with a convenient API:
https://covidtracking.com/api
2. I knew I could use the
Scriptable app on iOS to write some JavaScript to process and transform the data
3. Scriptable can draw some native UI but it’s often too limited. Instead, I chose to generate some HTML it can display in a web view.
4. I did some quick searches for simple ways to graph data on web pages. When I
searched, one of the top hits was
canvasJS.
5. I found a
sample graph that looked similar to what I needed
6. To quickly check that this would work, I created a script in Scriptable to generate the HTML and show a web view, pasting in their example:
let html = `
<!DOCTYPE html>
<html>
<head>
<script>
window.onload = function() {
var chart = new CanvasJS.Chart("chartContainer", {
animationEnabled: true,
title: {
text: "Hourly Average CPU Utilization"
},
axisX: {
title: "Time"
},
axisY: {
title: "Percentage",
suffix: "%"
},
data: [{
type: "line",
name: "CPU Utilization",
connectNullData: true,
//nullDataLineDashType: "solid",
xValueType: "dateTime",
xValueFormatString: "DD MMM hh:mm TT",
yValueFormatString: "#,##0.##\"%\"",
dataPoints: [
{ x: 1501102673000, y: 22.836 },
{ x: 1501106273000, y: 23.220 },
{ x: 1501109873000, y: 23.594 },
{ x: 1501113473000, y: 24.596 },
{ x: 1501117073000, y: 31.947 },
{ x: 1501120673000, y: 31.142 }
]
}]
});
chart.render();
}
</script>
</head>
<body>
<div id="chartContainer" style="height: 300px; width: 100%;"></div>
<script src="https://canvasjs.com/assets/script/canvasjs.min.js"></script>
</body>
</html>
`
let webView = new WebView()
webView.loadHTML(html)
webView.present(true)
7. That worked! I then figured out how to request data from covidtracking.com — conveniently hosted in JSON format. I used console.log to sanity check the result.
let dailyDataRequest = new Request("https://covidtracking.com/api/v1/us/daily.json")
let dailyData = await dailyDataRequest.loadJSON()
console.log("Row 0: " + dailyData[0].positive)
8. From there, I parsed the data, transformed it, and added more charts. I included some charts that the original website didn’t have that I found interesting.
// Show graphs that match: https://www.politico.com/interactives/2020/coronavirus-testing-by-state-chart-of-new-cases/
// Data from: https://covidtracking.com/api
// Examples the charts are based on:
// https://canvasjs.com/javascript-charts/null-data-chart/
// https://canvasjs.com/javascript-charts/multi-series-chart/
// https://canvasjs.com/javascript-charts/stacked-column-chart/
// https://canvasjs.com/javascript-charts/stacked-bar-chart/
// https://canvasjs.com/javascript-charts/stacked-bar-100-chart/
let firstInterestingDate = 20200301
function dataPointsFromDailyDataJSON(json, key) {
// { x: new Date(2017,6,24), y: 31 },
var result = ""
for (let row of json) {
let value = row[key]
let date = row["date"]
// Data is uninteresting before March 1st: 20200301
if (date < firstInterestingDate) {
continue
}
let match = (date+"").match(/(\d{4})(\d{2})(\d{2})/)
let year = parseInt(match[1])
let month = parseInt(match[2])
let day = parseInt(match[3])
result += "{ x: new Date("+year+","+(month-1)+","+day+"), y: " + value + " },\n"
}
return result
}
function deltaDataPointsFromDailyDataJSON(json, key) {
// { x: new Date(2017,6,24), y: 31 },
var result = ""
// json is sorted newest to oldest so we need to iterate backwards
var index = json.length-1
var prevValue = json[index][key]
while (index >= 0) {
let row = json[index]
index--
let value = row[key]
let date = row["date"]
let delta = value - prevValue
prevValue = value
// Data is uninteresting before March 1st: 20200301
if (date < firstInterestingDate) {
continue
}
let match = (date+"").match(/(\d{4})(\d{2})(\d{2})/)
let year = parseInt(match[1])
let month = parseInt(match[2])
let day = parseInt(match[3])
result += "{ x: new Date("+year+","+(month-1)+","+day+"), y: " + delta + " },\n"
}
return result
}
var chartCount = 0
function chartTestsPositivesDeathsForState(json, state) {
// Filter the data to the correct state; null means US
let filteredJson = (state == null) ? json : json.filter(row => row["state"] == state)
let currentTotalTests = filteredJson[0]["totalTestResults"]
let totalTestsDataPoints = dataPointsFromDailyDataJSON(filteredJson, "totalTestResults")
let currentPositive = filteredJson[0]["positive"]
let positiveDataPoints = dataPointsFromDailyDataJSON(filteredJson, "positive")
let currentDeath = filteredJson[0]["death"]
let deathsDataPoints = dataPointsFromDailyDataJSON(filteredJson, "death")
let chartName = "chart" + (state == null ? "US" : state) + chartCount++
let chartJS = `
var ${chartName} = new CanvasJS.Chart("${chartName}", {
animationEnabled: false,
title:{
text: "Covid-19 in ${state == null ? "the United States" : state}",
fontSize: 25,
},
axisX: {
valueFormatString: "MMM DD"
},
axisY: {
title: "Count",
includeZero: true,
},
legend:{
cursor: "pointer",
fontSize: 16,
itemclick: toggleDataSeries
},
toolTip:{
shared: true
},
data: [{
name: "Total Tests (${currentTotalTests.toLocaleString()})",
type: "line",
lineColor: "gray",
color: "gray",
showInLegend: true,
dataPoints: [
${totalTestsDataPoints}
]
},
{
name: "Positive (${currentPositive.toLocaleString()})",
type: "line",
lineColor: "orange",
color: "orange",
showInLegend: true,
dataPoints: [
${positiveDataPoints}
]
},
{
name: "Deaths (${currentDeath.toLocaleString()})",
type: "line",
lineColor: "red",
color: "red",
showInLegend: true,
dataPoints: [
${deathsDataPoints}
]
}]
});
${chartName}.render();
`
return { chartJS: chartJS, chartName: chartName };
}
function chartDeltas(json, state) {
// Filter the data to the correct state; null means US
let filteredJson = (state == null) ? json : json.filter(row => row["state"] == state)
let currentTestsDeltaToday = filteredJson[0]["totalTestResults"] - filteredJson[1]["totalTestResults"]
let currentTestsDeltas = deltaDataPointsFromDailyDataJSON(filteredJson, "totalTestResults")
let positiveDeltaToday = filteredJson[0]["positive"] - filteredJson[1]["positive"]
let positiveDeltas = deltaDataPointsFromDailyDataJSON(filteredJson, "positive")
let deathsDeltaToday = filteredJson[0]["death"] - filteredJson[1]["death"]
let deathsDeltas = deltaDataPointsFromDailyDataJSON(filteredJson, "death")
let chartName = "chartDeltas" + (state == null ? "US" : state) + chartCount++
let chartJS = `
var ${chartName} = new CanvasJS.Chart("${chartName}", {
animationEnabled: false,
title:{
text: "Day-over-Day Change${state == null ? "" : " in " + state}",
fontSize: 25,
},
axisX: {
valueFormatString: "MMM DD"
},
axisY: {
title: "Count",
includeZero: true,
},
legend:{
cursor: "pointer",
fontSize: 16,
itemclick: toggleDataSeries
},
toolTip:{
shared: true
},
data: [{
name: "Total Tests Δ (${currentTestsDeltaToday.toLocaleString()})",
type: "line",
lineColor: "gray",
color: "gray",
showInLegend: true,
dataPoints: [
${currentTestsDeltas}
]
},
{
name: "Positive Δ (${positiveDeltaToday.toLocaleString()})",
type: "line",
lineColor: "orange",
color: "orange",
showInLegend: true,
dataPoints: [
${positiveDeltas}
]
},
{
name: "Deaths Δ (${deathsDeltaToday.toLocaleString()})",
type: "line",
lineColor: "red",
color: "red",
showInLegend: true,
dataPoints: [
${deathsDeltas}
]
}]
});
${chartName}.render();
`
return { chartJS: chartJS, chartName: chartName };
}
function sortedStateNames(json) {
// For the entry with today's date, sort the state names from highest positive count to lowest
let todaysDate = json[0].date
let todaysData = json.filter(row => row.date == todaysDate)
todaysData.sort((a, b) => b.positive - a.positive)
return todaysData.map(row => row.state)
}
function statesTestsBarChart(json) {
let todaysDate = json[0].date
let todaysData = json.filter(row => row.date == todaysDate)
todaysData.sort((a, b) => b.positive - a.positive)
let positiveDataPoints = todaysData.map(row => "{ y: "+row.positive+", label: '"+row.state+"' }")
let negativeDataPoints = todaysData.map(row => "{ y: "+(row.totalTestResults-row.positive)+", label: '"+row.state+"' }")
let chartName = "chartTestsBarChart" + chartCount++
let chartJS = `
var ${chartName} = new CanvasJS.Chart("${chartName}", {
animationEnabled: false,
title:{
text: "State Testing",
fontSize: 25,
},
axisX: {
title: "State",
interval: 1,
labelFontSize: 12,
},
axisY:{
title: "Count",
},
data: [{
type: "stackedColumn",
showInLegend: true,
color: "orange",
name: "Positive",
dataPoints: [
${positiveDataPoints.join(",")}
]
},
{
type: "stackedColumn",
showInLegend: true,
name: "Negative",
color: "gray",
dataPoints: [
${negativeDataPoints.join(",")}
]
},
]
});
${chartName}.render();
`
return { chartJS: chartJS, chartName: chartName };
}
var html = `
<!DOCTYPE HTML>
<html>
<head>
<script>
window.onload = function () {
`
var chartNames = []
// https://covidtracking.com/api/v1/us/daily.json
let dailyDataRequest = new Request("https://covidtracking.com/api/v1/us/daily.json")
let dailyData = await dailyDataRequest.loadJSON()
// US chart for tests given, positive tests, deaths
let usChart = chartTestsPositivesDeathsForState(dailyData, null)
html += usChart.chartJS
chartNames.push(usChart.chartName)
// US Deltas
let usDeltasChart = chartDeltas(dailyData, null)
html += usDeltasChart.chartJS
chartNames.push(usDeltasChart.chartName)
// Per-state tests, positive, deaths charts sorted by most positive
// https://covidtracking.com/api/v1/states/daily.json
let stateDataRequest = new Request("https://covidtracking.com/api/v1/states/daily.json")
let stateData = await stateDataRequest.loadJSON()
// States tests stacked bar chart
let testsBarChart = statesTestsBarChart(stateData)
html += testsBarChart.chartJS
chartNames.push(testsBarChart.chartName)
// California
let caChart = chartTestsPositivesDeathsForState(stateData, "CA")
html += caChart.chartJS
chartNames.push(caChart.chartName)
let caDeltasChart = chartDeltas(stateData, "CA")
html += caDeltasChart.chartJS
chartNames.push(caDeltasChart.chartName)
// Sorted state data
let sortedStates = sortedStateNames(stateData)
for (let state of sortedStates) {
let stateChart = chartTestsPositivesDeathsForState(stateData, state)
html += stateChart.chartJS
chartNames.push(stateChart.chartName)
}
html += `
function toggleDataSeries(e){
if (typeof(e.dataSeries.visible) === "undefined" || e.dataSeries.visible) {
e.dataSeries.visible = false;
}
else{
e.dataSeries.visible = true;
}
chart.render();
}
}
</script>
</head>
<body>
`
var index = 0
for (let chartName of chartNames) {
var width = "100%" // US data, tests for all states
if (index >= 5) { // sorted states
width = "33%"
} else if (index >= 3) { // California
width = "50%"
}
html += '<div id="'+chartName+'" style="height: 500px; width: '+width+'; display: inline-block;"></div>'
if (index == 1 || index == 2 || (index % 3) == 1) {
html += "<br /><br />"
}
index++
}
html += `
<script src="https://canvasjs.com/assets/script/canvasjs.min.js"></script>
</body>
</html>
`
let webView = new WebView()
webView.loadHTML(html)
webView.present(true)
Resources:
-
Backup of the canvasJS library
-
Snapshot of US Daily data
-
Snapshot of States Daily data