loading...
Cover image for COVID-19 SPC: Statistical Process Control Charts

COVID-19 SPC: Statistical Process Control Charts

richardeschloss profile image Richard Schloss ・4 min read

Intro

Just launched (05/03/2020 at 2 AM): I created a web app that helps answer the question "How well is YOUR region doing with regards to COVID-19"? While there are already many great dashboards out there, I just wanted something far simpler: two charts and that's it. Trends of cases and deaths, which can be filtered by state, and further by region. I also wanted just the data. No opinions, no ads, no bloat, no auto-play videos on the side. Just the data. Unaltered, unbiased. I know people are capable of forming their own opinions.

Usage

The web app is currently hosted on Netlify at https://covid19-spc.netlify.app and using it is extremely simple with only two controls: a dropdown for the state and another for the region. Deleting the region or selecting a different state resets the charts to the "state level":

Alt Text

How it works

The web app takes advantage of the new fetch feature released in Nuxt 2.12, and also uses npm package vue-highcharts to make setting chart data from a Vue prop straightforward. Normally with the highcharts API, you would have to keep calling .setData({}) to update the series data, but with vue-highcharts, just provide the data; the setData() calls are handled for you.

So, for this particular app, there are two main components: 1) TrendChart.vue, and 2) ComboSelect.vue.

Trend Chart component

In this component, we define the props "dates" (as epoch time), "trendData" (the data points we want to work with). Then the computed props will change when those props change, with one important computed prop being the "chartOptions" provided to the highcharts component:

computed: {
  dailyIncreases() {
    return Stats.dailyIncreases(this.trendData)
  },
  ...
  chartOptions() {
    return {
      ...
      xAxis: {
          type: 'datetime'
        },
        series: [
          {
            name: 'Daily Increases',
            color: 'purple',
            data: this.dailyIncreases.map((v, idx) => [this.dates[idx], v])
          },
          ....
        ]
      }
    }
  }
}

This way, when we want to use the component, it's extremely simple:

pages/index.vue:

<trend-chart :dates="casesInfo.dates" :trendData="casesInfo.cases" />
<trend-chart :dates="deathsInfo.dates" :trendData="casesInfo.deaths" />

Combo Select component

This component takes advantage of the HTML datalist component, which allows an input text box to be tied to a list of options:

 <div>
    <div>
      <input
        v-model="selectedItem"
        :placeholder="placeholder"
        :list="id"
        class="form-control"
      />
    </div>
    <datalist :id="id">
      <option v-for="item in items" :key="item" :value="item" />
    </datalist>
  </div>

When we use this component, we want "selectedItem" to actually be a bound property in the parent that uses it; i.e., the parent will set it's v-model to "selectedItem". So to get the bound behavior, we need to set "value" as a prop in the component, and make "selectedItem" a computed prop with a defined getter and setter. Also, since the datalist input needs to be linked to a unique id, we have to make that a property too.

Inside "components/ComboSelect.vue":

props: {
  id: { type: String, default: () => {},
  value: { type: String, default: () => '' },
  items: { type: Array, default: () => [] },
  placeholder: { type: String, default: () => '' }
},
computed: {
  get() {
    return this.value // value is a prop 
  },
  set(val) {
    if (val && this.items.includes(val)) {
      // When the input changes, and it's an item in the datalist
      // emit "itemSelected" event
      this.$emit('itemSelected', val)
    } else {
      // Otherwise, just send "input" event
      this.$emit('input', val)
    }
  }
}

Then, in the page that uses the component, it is extremely simple to re-use:

<combo-select 
  v-model="selectedState" 
  :id="stateSelect" 
  :items="states" 
  :placeholder="Select State"
  @itemSelected="stateSelected" /> 
<combo-select
  v-model="selectedRegion"
  :id="regionSelect" 
  :items="regions" 
  placeholder="Select Region"
  @input="regionInput"
  @itemSelected="regionSelected" /> 

In the above snippet, we listen for "regionInput" because when it becomes empty, we can reset the charts back to the state view. An empty string will never trigger the "itemSelected" event.

The main page and "fetch"

In Nuxt 2.12, a new fetch has been introduced that allows fetching to be done either on the server or client side, setting the "fetchOnServer" boolean. The new fetch also exposes $fetchState which can tell us "pending" status of the fetch request, as well as the fetch timestamp. The pending boolean gets set to false when the fetch method completes (i.e., it's promises resolve).

So, this means we can control the displayed "Fetching data..." text like this now:

<span v-show="$fetchState.pending">
  (Fetching data...)
</span>

And our script would simply be:

fetch() {
  const urls = [...] // Array of sources
  const p = urls.map(Csv.fetch) // Array of promises to fetch the csv files
  Promise.all(p).then(parse) // When all those promise resolve, parse the data
}

Full disclosures

The website uses localStorage, but only to remember your dropdown selections so that you don't have to keep selecting state and region on page refresh. This information does not get sent back to me or third-parties. I don't want that information and I don't want to write the code for that.

The website uses an ".app" domain, since Netlify moved sites to that. This may or may not be the reason why some websites may incorrectly flag this as spam. Rest assured, it's not. In fact, all the source code is available on my Github repo and I encourage people with any concerns to check the code before navigating to the website. In just day 1, we already have a few people cloning it.

Also, the web app uses data provided by John's Hopkins University, sourced from their Github. To my knowledge, I believe I am adhering to their terms of use.

Posted on by:

richardeschloss profile

Richard Schloss

@richardeschloss

My goal is to be efficient and effective (#EnE) by writing less code that accomplishes more. Wannabe minimalist.

Discussion

pic
Editor guide