Add summary section to index.

This commit is contained in:
Joshua Bemenderfer
2022-01-01 21:50:26 -05:00
parent 94018b70ec
commit b3289258e3
27 changed files with 593 additions and 88 deletions

7
LICENSE.md Normal file
View File

@@ -0,0 +1,7 @@
Copyright 2022 Joshua Bemenderfer <josh@thederf.com>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1 +1,95 @@
# TODO: Write
# About
This site exists to provide a simplified, targeted view into information provided by the [Georgia Department of Health (DPH)](https://dph.georgia.gov/covid-19-daily-status-report) on the COVID-19 pandemic.
While the DPH provides an excellent range of datasets and visualizations, how to understand their utility and interpret them is left as an exercise to the reader.<br/>
Here I've attempted to provide explanations of the utility of certain statistics as well as provide some new visualizations not currently provided by the Department of Health dashboard.
## Disclaimer
The data provided on this site is based on information made available by the Georgia Department of Health at the time of reporting.
It may at times lack accuracy due to unreported, delayed, or improperly processed information.
Reporting time and quality for certain statistics may vary depending on the processes of individual organizations and jurisdictions.
This site is manually built from daily DPH reports. I will attempt to have new data incorporated by 5:00 PM on weekdays. Updates may be delayed due to personal circumstances.
**For case reports**, at-home rapid tests are not accounted for. Case numbers may be much higher than reported if [test positivity](https://ga-covid.thederf.com/overall/testing) is unusually high.
## The following additional reports are planned
- **What is the overall trend in vaccinations?** - [Data available here](https://experience.arcgis.com/experience/3d8eea39f5c1443db1743a4cb8948a9c) as XLSX, pending processing.
- **Who is most at risk by ethnicity and sex?** - Data available in daily reports, pending processing.
- **Who is most at risk by vaccination status?** - Weekly reports on breakthrough cases and deaths [are available here](https://dph.georgia.gov/covid-19-breakthrough-reports), but challenging to process.
- **How many hospital resources are available?** - [Data available here](https://www.arcgis.com/apps/opsdashboard/index.html#/47c1cee4d02542bea35bc3324d6cf5e3) but requires querying ArcGIS directly.
- **How many hospital patients have COVID?** - [Data available here](https://www.arcgis.com/apps/opsdashboard/index.html#/e40c39564f724af7bfe8fd5d88deadb6) but requires querying ArcGIS directly.
**Please note:** *This is a citizen science project and is not affiliated with or endorsed in any way by the State of Georgia.*
## Data Sources
- Georgia Department of Health, COVID-19 Daily Status Report - [https://ga-covid19.ondemand.sas.com/docs/ga_covid_data.zip](https://ga-covid19.ondemand.sas.com/docs/ga_covid_data.zip)
- **Update Frequency** - The dataset above is updated each working day around 2:50 PM. I load it onto this site and rebuild shortly thereafter.
- **Additional Data** - Some reports on this site may derive additional time-based information by comparing totals from multiple daily reports.
- David Eldersveld, TopoJSON Collection, Georgia Counties TopoJSON - [https://github.com/deldersveld/topojson/blob/master/countries/us-states/GA-13-georgia-counties.json](https://github.com/deldersveld/topojson/blob/master/countries/us-states/GA-13-georgia-counties.json)
- Tweaked and used for rendering county maps.
## Developing
Feel free to build it yourself, take a look around, validate the reports, and [suggest improvements](#contact).<br/>
*Note, the code is still very much in the "make it work" stage, and has much room for being simplified, abstracted, and cleaned up. Ignore the fact that I'm using TypeScript as if it were un-typed JavaScript.*
### Stack
- [îles](https://iles-docs.netlify.app/) - Generates static pages from MDX files and Vue components, with optional component-level client-side hydration.
- Excellent for getting a site built with a modern tech stack and minimal extraneous config. Still in beta with some rough edges, but I greatly enjoyed working with it.
- [Vue 3](https://v3.vuejs.org/) - Used for layout and client-side components such as charts.
- [JSCharting](https://jscharting.com/) - Used for charts. Straightforward to get up and running with, but provides a vast array of data visualization options.
- [TailwindCSS](https://tailwindcss.com/) - Because people keep telling me to try it. I get the appeal. It made style tweaking incredibly fast and fluid, but it definitely results in messy markup.
### Building & Running
1. Clone repository using [git](https://git-scm.com/)
```bash
$ git clone https://git.thederf.com/thederf/ga-covid.thederf.com.git
```
2. Install dependencies using [npm](https://www.npmjs.com/) or your preferred [node.js](https://nodejs.org) package manager of choice.
```bash
$ cd ga-covid.thederf.com/
$ npm install
```
3. (Optional) Rebuild the data json files.
```bash
npm run process:data
```
4. Run a local server for development
```bash
npm run dev
```
5. Build static site
```bash
npm run build
```
### File Tree
```txt
data/ - Scripts for comverting the raw CSV source files into browser-friendly JSON, removing extraneous data points, and segmenting data into separate files.
- parser.js - Data parser entrypoint.
parser/... - Scripts for producing json files for specific reports.
raw/
- YYYY-MM-DD.zip - Raw Georgia DPH datasets from the link above, by date downloaded.
Including historical datasets allows for building time-series reports for data provided exclusively in daily counts.
...
public/ - Static assets.
maps/ - GeoJSON map files for rendering maps.
data/ - Generated JSON files prepared for browser loading by the parser above.
...
src/
assets/ - CSS & Image assets processed by Vite
components/ - Vue components for rendering client-interactive parts of the website.
charts/ - Components that handle chart & chip rendering.
layouts/ - Vue components for laying out the site. Primarily server-rendered.
pages/ - MDX files for site pages.
...
...
```

13
components.d.ts vendored
View File

@@ -10,16 +10,27 @@ declare module 'vue' {
ChipsDeaths: typeof import('./src/components/charts/overall/deaths/ChipsDeaths.vue')['default']
ChipsHospitalizations: typeof import('./src/components/charts/overall/hospitalizations/ChipsHospitalizations.vue')['default']
ChipsRiskAge: typeof import('./src/components/charts/risk/age/ChipsRiskAge.vue')['default']
ChipsRiskHealthConditions: typeof import('./src/components/charts/risk/health-conditions/ChipsRiskHealthConditions.vue')['default']
ChipsSummary: typeof import('./src/components/charts/summary/ChipsSummary.vue')['default']
ChipsTesting: typeof import('./src/components/charts/overall/testing/ChipsTesting.vue')['default']
County_time_series: typeof import('./src/components/charts/state/cases/county_time_series.js')['default']
County_time_series_store: typeof import('./src/components/charts/state/cases/county_time_series_store.js')['default']
IconHealthiconsClinicalFOutline: typeof import('~icons/healthicons/clinical-f-outline')['default']
IconHealthiconsHospitalized: typeof import('~icons/healthicons/hospitalized')['default']
IconHealthiconsHospitalizedOutline: typeof import('~icons/healthicons/hospitalized-outline')['default']
IconHealthiconsVentilator: typeof import('~icons/healthicons/ventilator')['default']
IconHealthiconsVentilatorAl: typeof import('~icons/healthicons/ventilator-al')['default']
IconHealthiconsVentilatorAlt: typeof import('~icons/healthicons/ventilator-alt')['default']
IconHealthiconsVentilatorOutline: typeof import('~icons/healthicons/ventilator-outline')['default']
IconMdiGenderMaleFemale: typeof import('~icons/mdi/gender-male-female')['default']
IconMdiGraveStone: typeof import('~icons/mdi/grave-stone')['default']
IconMdiHospitalBoxOutline: typeof import('~icons/mdi/hospital-box-outline')['default']
IconMdiHumanCane: typeof import('~icons/mdi/human-cane')['default']
IconMdiPercentOutline: typeof import('~icons/mdi/percent-outline')['default']
IconMdiPerson: typeof import('~icons/mdi/person')['default']
IconMdiSyringe: typeof import('~icons/mdi/syringe')['default']
IconMdiTestTube: typeof import('~icons/mdi/test-tube')['default']
IconMdiUser: typeof import('~icons/mdi/user')['default']
IconMdiVirusOutline: typeof import('~icons/mdi/virus-outline')['default']
IconMdiWeb: typeof import('~icons/mdi/web')['default']
Island: typeof import('./node_modules/iles/dist/client/app/components/Island.vue')['default']
@@ -37,7 +48,7 @@ declare module 'vue' {
RiskAgeParameters: typeof import('./src/components/charts/risk/age/RiskAgeParameters.vue')['default']
SliceSelector: typeof import('./src/components/SliceSelector.vue')['default']
StatCard: typeof import('./src/components/cards/StatCard.vue')['default']
Store: typeof import('./src/components/charts/overall/cases/store.js')['default']
Store: typeof import('./src/components/charts/summary/store.js')['default']
Store_combined: typeof import('./src/components/charts/overall/cases/store_combined.js')['default']
TestingDataSetup: typeof import('./src/components/pages/state/TestingDataSetup.vue')['default']
TestingParameters: typeof import('./src/components/charts/state/testing/TestingParameters.vue')['default']

View File

@@ -11,6 +11,8 @@ import OverallDeaths from './parser/overall/deaths.js'
import RiskAge from './parser/risk/age.js'
import RiskHealthConditions from './parser/risk/health-conditions.js'
import Summary from './parser/summary.js'
async function main() {
const sources = await fg(['./data/raw/*.zip'])
sources.sort()
@@ -29,6 +31,8 @@ async function main() {
await RiskAge(zips)
await RiskHealthConditions(zips)
await Summary()
}
main()

41
data/parser/summary.js Normal file
View File

@@ -0,0 +1,41 @@
import mkdirp from 'mkdirp'
import path from 'path'
import fsp from 'fs/promises'
async function readJSON(file) {
return JSON.parse(await fsp.readFile(file, 'utf-8'))
}
export default async function process () {
const testing = await readJSON('./public/data/overall/testing/by-county/-- All --.json')
const cases = await readJSON('./public/data/overall/cases/by-county/-- All --.json')
const hospitalizations = await readJSON('./public/data/overall/hospitalizations/by-county/-- All --.json')
const deaths = await readJSON('./public/data/overall/deaths/by-county/-- All --.json')
const summary = {
last_report_date: testing.rows.at(-1)[0],
testing: {
headers: testing.headers,
current: testing.rows.at(-1),
prev: testing.rows.at(-2),
},
cases: {
headers: cases.headers,
current: cases.rows.at(-1),
prev: cases.rows.at(-2),
},
hospitalizations: {
headers: hospitalizations.headers,
current: hospitalizations.rows.at(-1),
prev: hospitalizations.rows.at(-2),
},
deaths: {
headers: deaths.headers,
current: deaths.rows.at(-1),
prev: deaths.rows.at(-2),
}
}
await fsp.writeFile('./public/data/summary.json', JSON.stringify(summary))
}

View File

@@ -1,6 +1,7 @@
{
"name": "ga-covid-charts",
"private": true,
"license": "MIT",
"version": "0.0.0",
"scripts": {
"dev": "iles dev",

1
public/data/summary.json Normal file
View File

@@ -0,0 +1 @@
{"last_report_date":"2021-12-31","testing":{"headers":["report_date","pcr_performed","pcr_positive","antigen_performed","antigen_positive","combined_performed","combined_positive","seven_day_percent_positive","combined_performed_running_total"],"current":["2021-12-31",54958,21204,27064,6629,82022,27833,32.1,18485118],"prev":["2021-12-30",57566,21851,31395,7521,88961,29372,30,18403096]},"cases":{"headers":["report_date","population","cases","cases_per_capita","cases_14_days","case_rate_14_days","total_cases","total_cases_per_capita"],"current":["2021-12-31",10833472,17641,0.0016283791567467936,null,null,1839082,0.16975924246631183],"prev":["2021-12-30",10833472,17923,0.0016544095927879816,null,null,1814762,0.16751434812403632]},"hospitalizations":{"headers":["report_date","population","hospitalizations","hospitalizations_per_capita","hospitalizations_last_14_days","hospitalizations_last_14_days_per_capita"],"current":["2021-12-31",10833472,248,0.00002289201467451986,2747,0.0002535659851246212],"prev":["2021-12-30",10833472,283,0.000026122742551972257,2590,0.00023907386293147754]},"deaths":{"headers":["report_date","population","deaths","deaths_per_capita","deaths_14_days","death_rate_14_days","total_deaths","total_deaths_per_capita"],"current":["2021-12-31",10833472,18,0.0000016615171941183769,340,0.00003138421366668045,26425,0.0024391995474765614],"prev":["2021-12-30",10833472,49,0.000004523019028433359,407,0.00003756874988923219,26407,0.002437538030282443]}}

View File

@@ -1,5 +1,5 @@
<template>
<div :class="['bg-white shadow rounded-lg p-4 lg:p-8 max-w-none', prose ? 'prose lg:prose-base prose-headings:font-light prose-headings:text-violet-900 prose-h1:mb-0' : '']">
<div :class="['bg-white shadow rounded-lg p-4 lg:p-8 max-w-none', prose ? 'prose lg:prose-base prose-headings:font-light prose-headings:text-violet-900 prose-h1:mb-0 prose-a:text-violet-700 prose-a:font-normal' : '']">
<slot></slot>
</div>
</template>

View File

@@ -1,26 +1,26 @@
<template>
<StatCard>
<template v-slot:heading>Confirmed Cases {{last_report_date ? humanDate(last_report_date) : ''}}</template>
<template v-slot:value v-if="chips.today_cases != null">
{{chips.today_cases.toLocaleString()}}
<template v-slot:value v-if="chips.current_cases != null">
{{chips.current_cases.toLocaleString()}}
</template>
<template v-slot:meta v-if="chips.today_case_change != null">
<span v-if="chips.today_case_change > 0">Up </span>
<span v-if="chips.today_case_change < 0">Down </span>
<span :class="{'font-semibold': true, 'text-red-600': chips.today_case_change > 0, 'text-green-600': chips.today_case_change < 0}">
{{Math.abs(chips.today_case_change).toLocaleString()}} ({{chips.today_case_change_percent}}%)
<template v-slot:meta v-if="chips.current_case_change != null">
<span v-if="chips.current_case_change > 0">Up </span>
<span v-if="chips.current_case_change < 0">Down </span>
<span :class="{'font-semibold': true, 'text-red-600': chips.current_case_change > 0, 'text-green-600': chips.current_case_change < 0}">
{{Math.abs(chips.current_case_change).toLocaleString()}} ({{chips.current_case_change_percent}}%)
</span> compared to previous day
</template>
</StatCard>
<StatCard>
<template v-slot:heading>Cases per 10,000 Residents {{last_report_date ? humanDate(last_report_date) : ''}}</template>
<template v-slot:value v-if="chips.today_cases_per_capita != null">
<span v-if="chips.population">~{{chips.today_cases_per_capita.toLocaleString()}}</span>
<template v-slot:value v-if="chips.current_cases_per_capita != null">
<span v-if="chips.population">~{{chips.current_cases_per_capita.toLocaleString()}}</span>
<span v-else>No Data</span>
</template>
<template v-slot:meta v-if="chips.population">
<span class="text-violet-500 font-semibold">{{chips.today_cases.toLocaleString()}}</span> out of <span class="text-violet-500 font-semibold">{{chips.population.toLocaleString()}}</span> residents
<span class="text-violet-500 font-semibold">{{chips.current_cases.toLocaleString()}}</span> out of <span class="text-violet-500 font-semibold">{{chips.population.toLocaleString()}}</span> residents
</template>
</StatCard>
@@ -68,14 +68,14 @@ const chips = reactive({
return col(data, r, 'population')
}),
today_cases: computed(() => {
current_cases: computed(() => {
if (!data.value) return null
const r = data.value.rows.at(-1)
return col(data, r, 'cases')
}),
today_case_change: computed(() => {
current_case_change: computed(() => {
if (!data.value) return null
const today = col(data, data.value.rows.at(-1), 'cases')
@@ -83,7 +83,7 @@ const chips = reactive({
return today - yesterday
}),
today_case_change_percent: computed(() => {
current_case_change_percent: computed(() => {
if (!data.value) return null
const today = col(data, data.value.rows.at(-1), 'cases')
@@ -100,7 +100,7 @@ const chips = reactive({
return Math.abs(percent.toFixed(1))
}),
today_cases_per_capita: computed(() => {
current_cases_per_capita: computed(() => {
if (!data.value) return null
const r = data.value.rows.at(-1)

View File

@@ -28,8 +28,6 @@ watch(() => store.parameters.value.county, () => {
refreshData()
})
if (globalThis.window) {
refreshData()
}
if (globalThis.window) refreshData()
export default store

View File

@@ -8,8 +8,6 @@ async function refreshData() {
store.data.value = await fetch(`/data/overall/cases/combined.json`).then(res => res.json())
}
if (globalThis.window) {
refreshData()
}
if (globalThis.window) refreshData()
export default store

View File

@@ -28,8 +28,6 @@ watch(() => store.parameters.value.county, () => {
refreshData()
})
if (globalThis.window) {
refreshData()
}
if (globalThis.window) refreshData()
export default store

View File

@@ -8,8 +8,6 @@ async function refreshData() {
store.data.value = await fetch(`/data/overall/deaths/combined.json`).then(res => res.json())
}
if (globalThis.window) {
refreshData()
}
if (globalThis.window) refreshData()
export default store

View File

@@ -28,8 +28,6 @@ watch(() => store.parameters.value.county, () => {
refreshData()
})
if (globalThis.window) {
refreshData()
}
if (globalThis.window) refreshData()
export default store

View File

@@ -5,11 +5,9 @@ const store = {
}
async function refreshData() {
store.data.value = await fetch(`/data/state/hospitalizations/combined.json`).then(res => res.json())
store.data.value = await fetch(`/data/overall/hospitalizations/combined.json`).then(res => res.json())
}
if (globalThis.window) {
refreshData()
}
if (globalThis.window) refreshData()
export default store

View File

@@ -30,7 +30,7 @@
</StatCard>
<StatCard>
<template v-slot:heading>7-Day Percent Positive</template>
<template v-slot:heading>7-Day Test Positivity</template>
<template v-slot:value v-if="chips.percent_positive">
<span :class="{
'text-green-600': chips.percent_positive < 2,
@@ -41,7 +41,7 @@
{{chips.percent_positive}}<span class="font-light">%</span>
</span>
</template>
<template v-slot:meta v-if="chips.positive">
<template v-slot:meta v-if="chips.percent_positive">
<div class="inline-flex flex-wrap text-xs -mx-1">
<div class="flex-1">
<div class="px-1 text-green-600 font-medium whitespace-nowrap">Low: &lt; 2%</div>

View File

@@ -22,8 +22,6 @@ watch(() => store.parameters.value.county, () => {
refreshData()
})
if (globalThis.window) {
refreshData()
}
if (globalThis.window) refreshData()
export default store

View File

@@ -23,8 +23,6 @@ async function refreshData() {
}
}
if (globalThis.window) {
refreshData()
}
if (globalThis.window) refreshData()
export default store

View File

@@ -8,8 +8,6 @@ async function refreshData() {
store.data.value = await fetch(`/data/risk/health-conditions/health-conditions.json`).then(res => res.json())
}
if (globalThis.window) {
refreshData()
}
if (globalThis.window) refreshData()
export default store

View File

@@ -0,0 +1,216 @@
<template>
<StatCard>
<template v-slot:heading>Tests Reported {{last_report_date ? humanDate(last_report_date) : ''}}</template>
<template v-slot:value v-if="chips.current_testing">
{{chips.current_testing.toLocaleString()}}
</template>
<template v-slot:meta v-if="chips.current_testing">
PCR + Antigen
</template>
<template v-slot:meta v-if="chips.current_testing_change != null">
<span v-if="chips.current_testing_change > 0">Up </span>
<span v-if="chips.current_testing_change < 0">Down </span>
<span class="font-semibold text-violet-600">
{{Math.abs(chips.current_testing_change).toLocaleString()}} ({{chips.current_testing_change_percent}}%)
</span> compared to previous day
</template>
</StatCard>
<StatCard>
<template v-slot:heading>7-Day Test Positivity</template>
<template v-slot:value v-if="chips.percent_positive">
<span :class="{
'text-green-600': chips.percent_positive < 2,
'text-yellow-500': chips.percent_positive >= 2 && chips.percent_positive < 5,
'text-orange-500': chips.percent_positive >= 5 && chips.percent_positive < 20,
'text-red-800': chips.percent_positive > 20
}">
{{chips.percent_positive}}<span class="font-light">%</span>
</span>
</template>
<template v-slot:meta v-if="chips.percent_positive">
<div class="inline-flex flex-wrap text-xs -mx-1">
<div class="flex-1">
<div class="px-1 text-green-600 font-medium whitespace-nowrap">Low: &lt; 2%</div>
<div class="px-1 text-yellow-600 font-medium whitespace-nowrap">Moderate &lt; 5%</div>
</div>
<div class="flex-1">
<div class="px-1 text-orange-600 font-medium whitespace-nowrap">High &lt; 20%</div>
<div class="px-1 text-red-700 font-medium whitespace-nowrap">Very high &gt; 20%</div>
</div>
</div>
<a class="link block mt-2 text-xs" href="https://www.who.int/publications/i/item/considerations-in-adjusting-public-health-and-social-measures-in-the-context-of-covid-19-interim-guidanceLow">
Source, see p. 18
</a>
</template>
</StatCard>
<StatCard>
<template v-slot:heading>Confirmed Cases {{last_report_date ? humanDate(last_report_date) : ''}}</template>
<template v-slot:value v-if="chips.current_cases != null">
{{chips.current_cases.toLocaleString()}}
</template>
<template v-slot:meta v-if="chips.current_case_change != null">
<span v-if="chips.current_case_change > 0">Up </span>
<span v-if="chips.current_case_change < 0">Down </span>
<span :class="{'font-semibold': true, 'text-red-600': chips.current_case_change > 0, 'text-green-600': chips.current_case_change < 0}">
{{Math.abs(chips.current_case_change).toLocaleString()}} ({{chips.current_case_change_percent}}%)
</span> compared to previous day
</template>
</StatCard>
<StatCard>
<template v-slot:heading>Confirmed Deaths {{last_report_date ? humanDate(last_report_date) : ''}}</template>
<template v-slot:value v-if="chips.current_deaths != null">
{{chips.current_deaths.toLocaleString()}}
</template>
<template v-slot:meta v-if="chips.current_death_change != null">
<span v-if="chips.current_death_change > 0">Up </span>
<span v-if="chips.current_death_change < 0">Down </span>
<span :class="{'font-semibold': true, 'text-red-600': chips.current_death_change > 0, 'text-green-600': chips.current_death_change < 0}">
{{Math.abs(chips.current_death_change).toLocaleString()}} ({{chips.current_death_change_percent}}%)
</span> compared to previous day
</template>
</StatCard>
<StatCard>
<template v-slot:heading>Total Confirmed Cases</template>
<template v-slot:value v-if="chips.total_cases != null">
{{chips.total_cases.toLocaleString()}}
</template>
</StatCard>
<StatCard>
<template v-slot:heading>Total Confirmed Deaths</template>
<template v-slot:value v-if="chips.total_deaths != null">
{{chips.total_deaths.toLocaleString()}}
</template>
</StatCard>
</template>
<script setup>
import { reactive, computed } from 'vue'
import { col, humanDate } from '@/components/charts/util'
import store from '@/components/charts/summary/store.js'
const data = store.data
const last_report_date = computed(() => {
if (!data.value) return null
return data.value.last_report_date
})
const chips = reactive({
current_testing: computed(() => {
if (!data.value) return null
return col(data.value.testing, data.value.testing.current, 'combined_performed')
}),
current_testing_change: computed(() => {
if (!data.value) return null
const today = col(data.value.testing, data.value.testing.current, 'combined_performed')
const yesterday = col(data.value.testing, data.value.testing.prev, 'combined_performed')
return today - yesterday
}),
current_testing_change_percent: computed(() => {
if (!data.value) return null
const today = col(data.value.testing, data.value.testing.current, 'combined_performed')
const yesterday = col(data.value.testing, data.value.testing.prev, 'combined_performed')
const change = today - yesterday
const percent = change > 0
? ((change / yesterday) * 100)
: ((change / today) * 100)
if (Math.abs(percent) === Infinity) return 100
if (isNaN(percent)) return 0
return Math.abs(percent.toFixed(1))
}),
percent_positive: computed(() => {
if (!data.value) return null
return col(data.value.testing, data.value.testing.current, 'seven_day_percent_positive')
}),
current_cases: computed(() => {
if (!data.value) return null
return col(data.value.cases, data.value.cases.current, 'cases')
}),
current_case_change: computed(() => {
if (!data.value) return null
const today = col(data.value.cases, data.value.cases.current, 'cases')
const yesterday = col(data.value.cases, data.value.cases.prev, 'cases')
return today - yesterday
}),
current_case_change_percent: computed(() => {
if (!data.value) return null
const today = col(data.value.cases, data.value.cases.current, 'cases')
const yesterday = col(data.value.cases, data.value.cases.prev, 'cases')
const change = today - yesterday
const percent = change > 0
? ((change / yesterday) * 100)
: ((change / today) * 100)
if (Math.abs(percent) === Infinity) return 100
if (isNaN(percent)) return 0
return Math.abs(percent.toFixed(1))
}),
current_deaths: computed(() => {
if (!data.value) return null
return col(data.value.deaths, data.value.deaths.current, 'deaths')
}),
current_death_change: computed(() => {
if (!data.value) return null
const today = col(data.value.deaths, data.value.deaths.current, 'deaths')
const yesterday = col(data.value.deaths, data.value.deaths.prev, 'deaths')
return today - yesterday
}),
current_death_change_percent: computed(() => {
if (!data.value) return null
const today = col(data.value.deaths, data.value.deaths.current, 'deaths')
const yesterday = col(data.value.deaths, data.value.deaths.prev, 'deaths')
const change = today - yesterday
const percent = change > 0
? ((change / yesterday) * 100)
: ((change / today) * 100)
if (Math.abs(percent) === Infinity) return 100
if (isNaN(percent)) return 0
return Math.abs(percent.toFixed(1))
}),
total_cases: computed(() => {
if (!data.value) return null
return col(data.value.cases, data.value.cases.current, 'total_cases')
}),
total_deaths: computed(() => {
if (!data.value) return null
return col(data.value.deaths, data.value.deaths.current, 'total_deaths')
})
})
</script>

View File

@@ -0,0 +1,13 @@
import { reactive, ref, watch } from 'vue'
const store = {
data: ref(null)
}
async function refreshData() {
store.data.value = await fetch(`/data/summary.json`).then(res => res.json())
}
if (globalThis.window) refreshData()
export default store

View File

@@ -1,7 +1,7 @@
export const colors = ['#f5f3ff','#ede9fe','#ddd6fe','#c4b5fd','#a78bfa','#8b5cf6','#7c3aed','#6d28d9','#5b21b6','#4c1d95']
export function col(data, row, column) {
const index = data.value.headers.indexOf(column)
const index = data.headers ? data.headers.indexOf(column) : data.value.headers.indexOf(column)
if (index === -1) return null
return row[index]
}

View File

@@ -2,7 +2,7 @@
<ul class="space-y-2 pb-2">
<li>
<router-link :to="{path: '/'}" class="text-base text-violet-900 font-bold flex flex-col rounded-lg p-2 px-3 hover:bg-violet-100 group" active-class="bg-violet-800 text-white hover:bg-violet-800">
Introduction
Home
</router-link>
</li>
<li>
@@ -28,35 +28,16 @@
<icon-mdi-grave-stone class="mr-2"></icon-mdi-grave-stone> deaths?
</router-link>
</li>
<li class="flex items-center justify-between">
<a href="#" class="text-base rounded-lg flex items-center p-2 mr-3 group opacity-50">
<icon-mdi-syringe class="mr-2"></icon-mdi-syringe> vaccinations?
</a>
<span class="rounded-lg bg-violet-700 text-white text-sm px-2 py-1 mr-3">Planned</span>
</li>
</ul>
</li>
<!-- <li>
<span class="flex font-bold px-3 py-1 text-violet-900">Which counties have the...</span>
<ul class="space-y-1 py-2 ml-3">
<li>
<router-link :to="{path: '/cases'}" class="text-base rounded-lg flex items-center p-2 mr-3 hover:bg-violet-100 group" active-class="bg-violet-800 text-white hover:bg-violet-800">
<icon-mdi-percent-outline class="mr-2"></icon-mdi-percent-outline> highest positivity rate?
</router-link>
</li>
<li>
<router-link :to="{path: '/cases'}" class="text-base rounded-lg flex items-center p-2 mr-3 hover:bg-violet-100 group" active-class="bg-violet-800 text-white hover:bg-violet-800">
<icon-mdi-virus-outline class="mr-2"></icon-mdi-virus-outline> most cases by population?
</router-link>
</li>
<li>
<router-link :to="{path: '/cases'}" class="text-base rounded-lg flex items-center p-2 mr-3 hover:bg-violet-100 group" active-class="bg-violet-800 text-white hover:bg-violet-800">
<icon-mdi-hospital-box-outline class="mr-2"></icon-mdi-hospital-box-outline> most hospitalizations by population?
</router-link>
</li>
<li>
<router-link :to="{path: '/cases'}" class="text-base rounded-lg flex items-center p-2 mr-3 hover:bg-violet-100 group" active-class="bg-violet-800 text-white hover:bg-violet-800">
<icon-mdi-grave-stone class="mr-2"></icon-mdi-grave-stone> most deaths by population?
</router-link>
</li>
</ul>
</li> -->
<li>
<span class="flex font-bold px-3 py-1 text-violet-900">Who is most at-risk...</span>
<span class="flex font-bold px-3 py-1 text-violet-900">Who is most at risk...</span>
<ul class="space-y-1 py-2 ml-3">
<li>
<router-link :to="{path: '/risk/age'}" class="text-base rounded-lg flex items-center p-2 mr-3 hover:bg-violet-100 group" active-class="bg-violet-800 text-white hover:bg-violet-800">
@@ -69,18 +50,36 @@
</router-link>
</li>
<li class="flex items-center justify-between">
<!-- <router-link :to="{path: '/risk/gender'}" class="text-base rounded-lg flex items-center p-2 mr-3 hover:bg-violet-100 group" active-class="bg-violet-800 text-white hover:bg-violet-800"> -->
<!-- <router-link :to="{path: '/risk/ethnicity'}" class="text-base rounded-lg flex items-center p-2 mr-3 hover:bg-violet-100 group" active-class="bg-violet-800 text-white hover:bg-violet-800"> -->
<a href="#" class="text-base rounded-lg flex items-center p-2 mr-3 group opacity-50">
<icon-mdi-gender-male-female class="mr-2"></icon-mdi-gender-male-female> by gender?
<icon-mdi-user class="mr-2"></icon-mdi-user> by ethnicity and sex?
</a>
<span class="rounded-lg bg-violet-700 text-white text-sm px-2 py-1 mr-3">Soon</span>
<span class="rounded-lg bg-violet-700 text-white text-sm px-2 py-1 mr-3">Planned</span>
</li>
<li class="flex items-center justify-between">
<!-- <router-link :to="{path: '/risk/ethnicity'}" class="text-base rounded-lg flex items-center p-2 mr-3 hover:bg-violet-100 group" active-class="bg-violet-800 text-white hover:bg-violet-800"> -->
<a href="#" class="text-base rounded-lg flex items-center p-2 mr-3 group opacity-50">
<icon-mdi-web class="mr-2"></icon-mdi-web> by ethnicity?
<icon-mdi-syringe class="mr-2"></icon-mdi-syringe> by vaccination status?
</a>
<span class="rounded-lg bg-violet-700 text-white text-sm px-2 py-1 mr-3">Soon</span>
<span class="rounded-lg bg-violet-700 text-white text-sm px-2 py-1 mr-3">Planned</span>
</li>
</ul>
</li>
<li>
<span class="flex font-bold px-3 py-1 text-violet-900">How many hospital...</span>
<ul class="space-y-1 py-2 ml-3">
<li class="flex items-center justify-between">
<a href="#" class="text-base rounded-lg flex items-center p-2 mr-3 group opacity-50">
<icon-healthicons-ventilator class="mr-2"></icon-healthicons-ventilator> resources are available?
</a>
<span class="rounded-lg bg-violet-700 text-white text-sm px-2 py-1 mr-3">Planned</span>
</li>
<li class="flex items-center justify-between">
<a href="#" class="text-base rounded-lg flex items-center p-2 mr-3 group opacity-50">
<icon-healthicons-hospitalized class="mr-2"></icon-healthicons-hospitalized> patients have COVID?
</a>
<span class="rounded-lg bg-violet-700 text-white text-sm px-2 py-1 mr-3">Planned</span>
</li>
</ul>
</li>

View File

@@ -14,9 +14,9 @@
<strong>Note:</strong> This is a citizen science project by <a href="https://thederf.com" class="link">Joshua Bemenderfer</a> and not affiliated with or endorsed in any way by the State of Georgia.
</p>
<ul class="flex items-center mt-4 lg:mt-0">
<li class="text-center"><a href="/sources" class="font-normal text-violet-900 hover:bg-violet-100 rounded-lg p-2 px-3 block">Sources</a></li>
<li class="text-center"><a href="/about" class="font-normal text-violet-900 hover:bg-violet-100 rounded-lg p-2 px-3 block">About</a></li>
<li class="text-center"><a href="mailto:josh@thederf.com" target="_blank" class="font-normal text-violet-900 hover:bg-violet-100 rounded-lg p-2 px-3 block">Contact</a></li>
<li class="text-center"><a href="/#sources" class="font-normal text-violet-900 hover:bg-violet-100 rounded-lg p-2 px-3 block">Sources</a></li>
<li class="text-center"><a href="/#about" class="font-normal text-violet-900 hover:bg-violet-100 rounded-lg p-2 px-3 block">About</a></li>
<li class="text-center"><a href="/#contact" class="font-normal text-violet-900 hover:bg-violet-100 rounded-lg p-2 px-3 block">Contact</a></li>
</ul>
</div>
</article>

View File

@@ -1,8 +1,143 @@
---
title: Introduction
title: Home / About / Sources
---
<Card prose={true}>
# Introduction
<div id="about"></div>
# About
This site exists to provide a simplified, targeted view into information provided by the [Georgia Department of Health (DPH)](https://dph.georgia.gov/covid-19-daily-status-report) on the COVID-19 pandemic.
While the DPH provides an excellent range of datasets and visualizations, how to understand their utility and interpret them is left as an exercise to the reader.<br/>
Here I've attempted to provide explanations of the utility of certain statistics as well as provide some new visualizations not currently provided by the Department of Health dashboard.
### Disclaimer
The data provided on this site is based on information made available by the Georgia Department of Health at the time of reporting.
It may at times lack accuracy due to unreported, delayed, or improperly processed information.
Reporting time and quality for certain statistics may vary depending on the processes of individual organizations and jurisdictions.
This site is manually built from daily DPH reports. I will attempt to have new data incorporated by 5:00 PM on weekdays. Updates may be delayed due to personal circumstances.
**For case reports**, at-home rapid tests are not accounted for. Case numbers may be much higher than reported if [test positivity](/overall/testing) is unusually high.
### The following additional reports are planned
- **What is the overall trend in vaccinations?** - [Data available here](https://experience.arcgis.com/experience/3d8eea39f5c1443db1743a4cb8948a9c) as XLSX, pending processing.
- **Who is most at risk by ethnicity and sex?** - Data available in daily reports, pending processing.
- **Who is most at risk by vaccination status?** - Weekly reports on breakthrough cases and deaths [are available here](https://dph.georgia.gov/covid-19-breakthrough-reports), but challenging to process.
- **How many hospital resources are available?** - [Data available here](https://www.arcgis.com/apps/opsdashboard/index.html#/47c1cee4d02542bea35bc3324d6cf5e3) but requires querying ArcGIS directly.
- **How many hospital patients have COVID?** - [Data available here](https://www.arcgis.com/apps/opsdashboard/index.html#/e40c39564f724af7bfe8fd5d88deadb6) but requires querying ArcGIS directly.
**Please note:** *This is a citizen science project and is not affiliated with or endorsed in any way by the State of Georgia.*
</Card>
<Card class="bg-violet-900">
<div id="summary"></div>
<div class="prose prose-invert lg:prose-base prose-headings:font-light prose-a:text-violet-700 prose-a:font-normal mb-2">
# Summary of Current Overall Trends
</div>
<p class="text-white prose prose-invert lg:prose-base mb-4">
For more details, see reporting pages for:
<div class="grid gap-2 grid-cols-2 sm:grid-cols-4 -mt-2">
<RouterLink to={{path: '/overall/testing'}} class="text-center font-normal no-underline text-white bg-violet-700 hover:bg-violet-500 rounded-lg p-2 px-3 block w-full">Testing</RouterLink>
<RouterLink to={{path: '/overall/testing'}} class="text-center font-normal no-underline text-white bg-violet-700 hover:bg-violet-500 rounded-lg p-2 px-3 block w-full">Cases</RouterLink>
<RouterLink to={{path: '/overall/testing'}} class="text-center font-normal no-underline text-white bg-violet-700 hover:bg-violet-500 rounded-lg p-2 px-3 block w-full">Hospitalizations</RouterLink>
<RouterLink to={{path: '/overall/testing'}} class="text-center font-normal no-underline text-white bg-violet-700 hover:bg-violet-500 rounded-lg p-2 px-3 block w-full">Deaths</RouterLink>
</div>
</p>
<div class="grid gap-2 lg:gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3">
<MapCases client:load class="sm:col-span-2 xl:col-span-1 xl:row-span-3"/>
<ChipsSummary client:load/>
</div>
</Card>
<Card prose={true}>
<div id="sources"></div>
# Sources
### Data Sources
- Georgia Department of Health, COVID-19 Daily Status Report - [https://ga-covid19.ondemand.sas.com/docs/ga_covid_data.zip](https://ga-covid19.ondemand.sas.com/docs/ga_covid_data.zip)
- **Update Frequency** - The dataset above is updated each working day around 2:50 PM. I load it onto this site and rebuild shortly thereafter.
- **Additional Data** - Some reports on this site may derive additional time-based information by comparing totals from multiple daily reports.
- David Eldersveld, TopoJSON Collection, Georgia Counties TopoJSON - [https://github.com/deldersveld/topojson/blob/master/countries/us-states/GA-13-georgia-counties.json](https://github.com/deldersveld/topojson/blob/master/countries/us-states/GA-13-georgia-counties.json)
- Tweaked and used for rendering county maps.
### Site Source Code
The source code for this website can be found at [https://git.thederf.com/thederf/ga-covid.thederf.com](https://git.thederf.com/thederf/ga-covid.thederf.com).
Feel free to build it yourself, take a look around, validate the reports, and [suggest improvements](#contact).<br/>
*Note, the code is still very much in the "make it work" stage, and has much room for being simplified, abstracted, and cleaned up.*
#### Licensing
The source code and content of this site is provided under the [MIT license](https://opensource.org/licenses/MIT).<br/>
Datasets, libraries, and other assets used on this site are distributed or used according to the licenses of the original copyright holders to the best of my knowledge.
#### Stack
- [îles](https://iles-docs.netlify.app/) - Generates static pages from MDX files and Vue components, with optional component-level client-side hydration.
- Excellent for getting a site built with a modern tech stack and minimal extraneous config. Still in beta with some rough edges, but I greatly enjoyed working with it.
- [Vue 3](https://v3.vuejs.org/) - Used for layout and client-side components such as charts.
- [JSCharting](https://jscharting.com/) - Used for charts. Straightforward to get up and running with, but provides a vast array of data visualization options.
- [TailwindCSS](https://tailwindcss.com/) - Because people keep telling me to try it. I get the appeal. It made style tweaking incredibly fast and fluid, but it definitely results in messy markup.
#### Building & Running
1. Clone repository using [git](https://git-scm.com/)
```bash
$ git clone https://git.thederf.com/thederf/ga-covid.thederf.com.git
```
2. Install dependencies using [npm](https://www.npmjs.com/) or your preferred [node.js](https://nodejs.org) package manager of choice.
```bash
$ cd ga-covid.thederf.com/
$ npm install
```
3. (Optional) Rebuild the data json files.
```bash
npm run process:data
```
4. Run a local server for development
```bash
npm run dev
```
5. Build static site
```bash
npm run build
```
#### File Tree
```txt
data/ - Scripts for comverting the raw CSV source files into browser-friendly JSON, removing extraneous data points, and segmenting data into separate files.
- parser.js - Data parser entrypoint.
parser/... - Scripts for producing json files for specific reports.
raw/
- YYYY-MM-DD.zip - Raw Georgia DPH datasets from the link above, by date downloaded.
Including historical datasets allows for building time-series reports for data provided exclusively in daily counts.
...
public/ - Static assets.
maps/ - GeoJSON map files for rendering maps.
data/ - Generated JSON files prepared for browser loading by the parser above.
...
src/
assets/ - CSS & Image assets processed by Vite
components/ - Vue components for rendering client-interactive parts of the website.
charts/ - Components that handle chart & chip rendering.
layouts/ - Vue components for laying out the site. Primarily server-rendered.
pages/ - MDX files for site pages.
...
...
```
</Card>
<Card prose={true}>
<div id="contact"></div>
# Contact
If you have comments, concerns, requests, or recommendations, feel free to send me an email at
[ga-covid@thederf.com](mailto:ga-covid@thederf.com).
</Card>

View File

@@ -14,7 +14,8 @@ title: What is the overall trend in testing?
**For example:**<br/>
If you get **10** positive results from **100** tests, the disease is probably spreading more widely than would be indicated if there were **50** positive results from **1,000** tests.
This discrepancy is often apparent on Mondays, where tests and cases from over the weekend might wind up in Monday's report.
**Test Positivity** can serve as a rough proxy for inferring how many cases go unreported.<br/>
If a high percentage of reported tests are coming back positive, odds are a significant number of cases are going unreported, especially when at-home tests are available.
</Card>
<ParametersTesting client:load/>

View File

@@ -3,13 +3,13 @@ title: Who is most at risk by health condition?
---
<Card class="col-span-1" prose={true}>
# Who is most at risk by condition?
# Who is most at risk by health condition?
## What is this report useful for?
This report provides charts indicating the number of cases and deaths for a number of tracked medical conditions.
You may find it useful for assessing the relative risk of COVID to yourself or others.
You may find it useful for assessing the relative risk of contracting COVID to yourself or others.
</Card>
<div class="grid gap-2 lg:gap-4 grid-cols-1 xl:grid-cols-4">