Home
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