Working on case cards.

This commit is contained in:
Joshua Bemenderfer
2021-12-30 21:56:56 -05:00
parent 4e83fd3dbd
commit bbaef02acb
512 changed files with 847 additions and 1240952 deletions

View File

@@ -20,12 +20,14 @@
</template>
<script setup lang='ts'>
import cache from '@/data/cache.js'
const parameters = cache.parameters
const { parameters, unknown } = defineProps({
parameters: Object,
unknown: Boolean
})
const counties = [
'All',
'-- All --',
unknown ? '-- Unknown --' : null,
'Appling',
'Atkinson',
'Bacon',
@@ -186,5 +188,5 @@ const counties = [
'Wilkes',
'Wilkinson',
'Worth'
]
].filter(county => !!county)
</script>

View File

@@ -0,0 +1,137 @@
<template>
<StatCard>
<template v-slot:heading>Today's Confirmed Cases</template>
<template v-slot:value v-if="chips.today_cases != null">
{{chips.today_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}}%)
</span> compared to yesterday
</template>
</StatCard>
<StatCard>
<template v-slot:heading>Today's Cases per 10,000 Residents</template>
<template v-slot:value v-if="chips.today_cases_per_capita != null">
~{{chips.today_cases_per_capita.toLocaleString()}}
</template>
<template v-slot:meta v-if="chips.population != null">
<span class="text-indigo-500 font-semibold">{{chips.today_cases.toLocaleString()}}</span> out of <span class="text-indigo-500 font-semibold">{{chips.population.toLocaleString()}}</span> residents
</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>
<template v-slot:meta v-if="chips.total_case_change != null">
<span class="font-semibold text-indigo-500">
{{Math.abs(chips.total_case_change).toLocaleString()}} ({{chips.total_case_change_percent}}%)
</span> more than yesterday
</template>
</StatCard>
<StatCard>
<template v-slot:heading>Total Cases per 10,000 Residents</template>
<template v-slot:value v-if="chips.total_cases_per_capita != null">
~{{chips.total_cases_per_capita.toLocaleString()}}
</template>
<template v-slot:meta v-if="chips.total_cases != null && chips.population != null">
<span class="text-indigo-500 font-semibold">{{chips.total_cases.toLocaleString()}}</span> out of <span class="text-indigo-500 font-semibold">{{chips.population.toLocaleString()}}</span> residents
</template>
</StatCard>
</template>
<script setup>
import { reactive, computed } from 'vue'
import { col } from '@/components/charts/util'
import store from '@/components/charts/state/cases/store.js'
const data = store.data
const chips = reactive({
population: computed(() => {
if (!data.value) return null
const r = data.value.rows.at(-1)
return col(data, r, 'population')
}),
today_cases: computed(() => {
if (!data.value) return null
const r = data.value.rows.at(-1)
return col(data, r, 'cases')
}),
today_case_change: computed(() => {
if (!data.value) return null
const today = col(data, data.value.rows.at(-1), 'cases')
const yesterday = col(data, data.value.rows.at(-2), 'cases')
return today - yesterday
}),
today_case_change_percent: computed(() => {
if (!data.value) return null
const today = col(data, data.value.rows.at(-1), 'cases')
const yesterday = col(data, data.value.rows.at(-2), 'cases')
const change = today - yesterday
const percent = change > 0
? ((change / yesterday) * 100)
: ((change / today) * 100)
return Math.abs(percent.toFixed(1))
}),
today_cases_per_capita: computed(() => {
if (!data.value) return null
const r = data.value.rows.at(-1)
return Math.round(col(data, r, 'cases_per_capita') * 10000)
}),
total_cases: computed(() => {
if (!data.value) return null
const r = data.value.rows.at(-1)
return col(data, r, 'total_cases')
}),
total_case_change: computed(() => {
if (!data.value) return null
const today = col(data, data.value.rows.at(-1), 'total_cases')
const yesterday = today - col(data, data.value.rows.at(-1), 'cases')
return today - yesterday
}),
total_case_change_percent: computed(() => {
if (!data.value) return null
const today = col(data, data.value.rows.at(-1), 'total_cases')
const yesterday = today - col(data, data.value.rows.at(-1), 'cases')
const change = today - yesterday
const percent = change > 0
? ((change / yesterday) * 100)
: ((change / today) * 100)
return Math.abs(percent.toFixed(1))
}),
total_cases_per_capita: computed(() => {
if (!data.value) return null
const r = data.value.rows.at(-1)
return Math.round(col(data, r, 'total_cases_per_capita') * 10000)
})
})
</script>

View File

@@ -0,0 +1,9 @@
<template>
<SliceSelector :parameters="parameters" :unknown="true"></SliceSelector>
</template>
<script setup lang='ts'>
import store from '@/components/charts/state/cases/store.js'
const parameters = store.parameters
</script>

View File

@@ -0,0 +1,68 @@
<template>
<Card>
<h2 class="mb-6 text-xl text-indigo-900 flex justify-between items-center">
Daily PCR Tests Performed
<select v-model="chart" class="text-base m-0">
<option value="calendar">Calendar</option>
<option value="line">Line Chart</option>
</select>
</h2>
<JSCharting v-if="chart === 'calendar'" :options="calendarOptions"></JSCharting>
<JSCharting v-if="chart === 'line'" :options="areaOptions"></JSCharting>
</Card>
</template>
<script setup>
import { ref, computed } from 'vue'
import { col, colors } from '@/components/charts/util'
import store from '@/components/charts/state/testing/store.js'
const chart = ref('calendar')
const column = 'pcr_performed'
const parameters = store.parameters
const data = store.data
const rows = computed(() => {
if (!data.value || !parameters.value) return []
return data.value.rows
.filter(r => {
return col(data, r, 'report_date') >= parameters.value.start && col(data, r, 'report_date') <= parameters.value.end
})
})
const calendarOptions = computed(() => {
return {
type: 'calendar month solid',
palette: { colors },
calendar_initial: parameters.value ? parameters.value.end : null,
data: rows.value.map(r => ([
`${col(data, r, 'report_date')} 12:00:00`,
col(data, r, column)
]))
}
})
const areaOptions = computed(() => {
return {
type: 'lineSpline',
legend_visible: false,
xAxis: { crosshair_enabled: true, scale_type: 'time' },
palette: { colors },
defaultSeries: {
shape_opacity: 0.55,
color: colors[Math.round(colors.length / 2)],
defaultPoint_marker_visible: false
},
series: [
{
points: rows.value.map(r => ([
`${col(data, r, 'report_date')} 12:00:00`,
col(data, r, column)
]))
}
]
}
})
</script>

View File

@@ -0,0 +1,34 @@
import { reactive, ref, watch } from 'vue'
function formatDate(d) {
return [
d.getFullYear(),
('0' + (d.getMonth() + 1)).slice(-2),
('0' + d.getDate()).slice(-2)
].join('-');
}
const today = new Date()
const end = new Date()
const start = new Date(today.valueOf() - 2592000000)
const store = {
parameters: ref({
county: '-- All --',
start: formatDate(start),
end: formatDate(end)
}),
data: ref(null)
}
async function refreshData() {
store.data.value = await fetch(`/data/state/cases/${store.parameters.value.county}.json`).then(res => res.json())
}
watch(() => store.parameters.value.county, () => {
refreshData()
})
refreshData()
export default store

View File

@@ -62,9 +62,9 @@
<script setup>
import { reactive, computed } from 'vue'
import { col } from '@/components/charts/util'
import cache from '@/data/cache.js'
import store from '@/components/charts/state/testing/store.js'
const data = cache.data
const data = store.data
const chips = reactive({
total_tests: computed(() => {

View File

@@ -0,0 +1,9 @@
<template>
<SliceSelector :parameters="parameters"></SliceSelector>
</template>
<script setup lang='ts'>
import store from '@/components/charts/state/testing/store.js'
const parameters = store.parameters
</script>

View File

@@ -16,16 +16,16 @@
<script setup>
import { ref, computed } from 'vue'
import { col, colors } from '@/components/charts/util'
import cache from '@/data/cache.js'
import store from '@/components/charts/state/testing/store.js'
const chart = ref('calendar')
const column = 'antigen_positive'
const parameters = cache.parameters
const data = cache.data
const parameters = store.parameters
const data = store.data
const rows = computed(() => {
if (!data.value) return []
if (!data.value || !parameters.value) return []
return data.value.rows
.filter(r => {
return col(data, r, 'report_date') >= parameters.value.start && col(data, r, 'report_date') <= parameters.value.end
@@ -36,7 +36,7 @@ const calendarOptions = computed(() => {
return {
type: 'calendar month solid',
palette: { colors },
calendar_initial: parameters.value.end,
calendar_initial: parameters.value ? parameters.value.end : null,
data: rows.value.map(r => ([
`${col(data, r, 'report_date')} 12:00:00`,
col(data, r, column)

View File

@@ -15,16 +15,16 @@
<script setup>
import { ref, computed } from 'vue'
import { col, colors } from '@/components/charts/util'
import cache from '@/data/cache.js'
import store from '@/components/charts/state/testing/store.js'
const chart = ref('calendar')
const column = 'antigen_performed'
const parameters = cache.parameters
const data = cache.data
const parameters = store.parameters
const data = store.data
const rows = computed(() => {
if (!data.value) return []
if (!data.value || !parameters.value) return []
return data.value.rows
.filter(r => {
return col(data, r, 'report_date') >= parameters.value.start && col(data, r, 'report_date') <= parameters.value.end
@@ -35,7 +35,7 @@ const calendarOptions = computed(() => {
return {
type: 'calendar month solid',
palette: { colors },
calendar_initial: parameters.value.end,
calendar_initial: parameters.value ? parameters.value.end : null,
data: rows.value.map(r => ([
`${col(data, r, 'report_date')} 12:00:00`,
col(data, r, column)

View File

@@ -16,16 +16,16 @@
<script setup>
import { ref, computed } from 'vue'
import { col, colors } from '@/components/charts/util'
import cache from '@/data/cache.js'
import store from '@/components/charts/state/testing/store.js'
const chart = ref('calendar')
const column = 'pcr_positive'
const parameters = cache.parameters
const data = cache.data
const parameters = store.parameters
const data = store.data
const rows = computed(() => {
if (!data.value) return []
if (!data.value || !parameters.value) return []
return data.value.rows
.filter(r => {
return col(data, r, 'report_date') >= parameters.value.start && col(data, r, 'report_date') <= parameters.value.end
@@ -36,7 +36,7 @@ const calendarOptions = computed(() => {
return {
type: 'calendar month solid',
palette: { colors },
calendar_initial: parameters.value.end,
calendar_initial: parameters.value ? parameters.value.end : null,
data: rows.value.map(r => ([
`${col(data, r, 'report_date')} 12:00:00`,
col(data, r, column)

View File

@@ -16,16 +16,16 @@
<script setup>
import { ref, computed } from 'vue'
import { col, colors } from '@/components/charts/util'
import cache from '@/data/cache.js'
import store from '@/components/charts/state/testing/store.js'
const chart = ref('calendar')
const column = 'pcr_performed'
const parameters = cache.parameters
const data = cache.data
const parameters = store.parameters
const data = store.data
const rows = computed(() => {
if (!data.value) return []
if (!data.value || !parameters.value) return []
return data.value.rows
.filter(r => {
return col(data, r, 'report_date') >= parameters.value.start && col(data, r, 'report_date') <= parameters.value.end
@@ -36,7 +36,7 @@ const calendarOptions = computed(() => {
return {
type: 'calendar month solid',
palette: { colors },
calendar_initial: parameters.value.end,
calendar_initial: parameters.value ? parameters.value.end : null,
data: rows.value.map(r => ([
`${col(data, r, 'report_date')} 12:00:00`,
col(data, r, column)

View File

@@ -0,0 +1,36 @@
import { reactive, ref, watch } from 'vue'
function formatDate(d) {
return [
d.getFullYear(),
('0' + (d.getMonth() + 1)).slice(-2),
('0' + d.getDate()).slice(-2)
].join('-');
}
const today = new Date()
const end = new Date()
const start = new Date(today.valueOf() - 2592000000)
const store = {
parameters: ref({
county: '-- All --',
start: formatDate(start),
end: formatDate(end)
}),
data: ref(null)
}
async function refreshData() {
store.data.value = await fetch(`/data/state/testing/${store.parameters.value.county}.json`).then(res => res.json())
}
watch(() => store.parameters.value.county, () => {
refreshData()
})
if (globalThis.window) {
refreshData()
}
export default store

View File

@@ -1,39 +0,0 @@
<template></template>
<script setup>
import { watch, onMounted } from 'vue'
import cache from '@/data/cache.js'
const parameters = cache.parameters
const data = cache.data
function formatDate(d) {
return [
d.getFullYear(),
('0' + (d.getMonth() + 1)).slice(-2),
('0' + d.getDate()).slice(-2)
].join('-');
}
const today = new Date()
const end = new Date()
const start = new Date(today.valueOf() - 2592000000)
console.log(start, end)
parameters.value = {
county: 'All',
start: formatDate(start),
end: formatDate(end),
}
async function refreshData() {
data.value = await fetch(`/data/testing-trend/${parameters.value.county}.json`).then(res => res.json())
}
watch(() => parameters.value.county, () => {
refreshData()
})
onMounted(() => refreshData())
</script>

View File

@@ -1,8 +0,0 @@
import { reactive, ref, watch } from 'vue'
const cache = {
parameters: ref(null),
data: ref(null)
}
export default cache

View File

@@ -14,7 +14,7 @@
</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">
<router-link :to="{path: '/state/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> cases?
</router-link>
</li>

View File

@@ -0,0 +1,40 @@
<template>
<nav class="bg-white fixed w-full z-30 shadow-md lg:shadow-none">
<div class="px-3 py-3 lg:px-5 lg:pl-3 h-16">
<div class="flex items-center h-full">
<a class="flex h-full pr-5 font-bold text-violet-900 items-center" href="/">
<img src="@/assets/ga-state-flag.svg" class="block h-full object-contain py-2 mr-2"/>
TheDerf's Georgia COVID-19 Charts
</a>
<div class="flex-1"></div>
<button @click="toggleSidebar()" aria-expanded="true" aria-controls="sidebar" class="lg:hidden ml-2 text-gray-600 hover:text-gray-900 cursor-pointer p-2 hover:bg-gray-100 focus:bg-gray-100 focus:ring-2 focus:ring-gray-100 rounded">
<svg :class="['w-6 h-6', isOpen ? 'hidden' : '']" class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h6a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd"></path>
</svg>
<svg :class="['w-6 h-6', isOpen ? '' : 'hidden']" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
</svg>
</button>
<div class="flex-1 hidden lg:-block"></div>
</div>
</div>
</nav>
<aside ref="sidebar" :class="[
'fixed z-20 min-h-full max-h-screen lg:max-h-full pt-16 left-0 flex lg:flex flex-shrink-0 flex-col w-full lg:w-96 transition-transform origin-top duration-75 lg:relative lg:scale-y-100',
isOpen ? 'scale-y-100' : 'scale-y-0'
]" aria-label="Sidebar">
<div class="flex-1 flex flex-col overflow-y-auto px-3 bg-white space-y-4">
<slot></slot>
</div>
</aside>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const isOpen = ref(false)
function toggleSidebar() {
isOpen.value = !isOpen.value
}
</script>

View File

@@ -1,37 +1,9 @@
<template>
<!-- This is an example component -->
<div class="flex min-h-screen">
<nav class="bg-white fixed w-full z-30 shadow-md lg:shadow-none">
<div class="px-3 py-3 lg:px-5 lg:pl-3 h-16">
<div class="flex items-center h-full">
<a class="flex h-full pr-5 font-bold text-violet-900 items-center" href="/">
<img src="@/assets/ga-state-flag.svg" class="block h-full object-contain py-2 mr-2"/>
Georgia COVID-19 Charts
</a>
<div class="flex-1"></div>
<button @click="toggleSidebar()" aria-expanded="true" aria-controls="sidebar" class="lg:hidden ml-2 text-gray-600 hover:text-gray-900 cursor-pointer p-2 hover:bg-gray-100 focus:bg-gray-100 focus:ring-2 focus:ring-gray-100 rounded">
<svg :class="['w-6 h-6', isOpen ? 'hidden' : '']" class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h6a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd"></path>
</svg>
<svg :class="['w-6 h-6', isOpen ? '' : 'hidden']" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
</svg>
</button>
<div class="flex-1"></div>
<em class="font-light hidden lg:block">
This is a citizen science project and not affiliated with or endorsed in any way by the State of Georgia.<br/>
</em>
</div>
</div>
</nav>
<aside ref="sidebar" :class="[
'fixed z-20 min-h-full max-h-screen lg:max-h-full pt-16 left-0 flex lg:flex flex-shrink-0 flex-col w-full lg:w-96 transition-transform origin-top duration-75 lg:relative lg:scale-y-100',
isOpen ? 'scale-y-100' : 'scale-y-0'
]" aria-label="Sidebar">
<div class="flex-1 flex flex-col overflow-y-auto px-3 bg-white space-y-4">
<Navigation></Navigation>
</div>
</aside>
<Wrapper client:idle>
<Navigation></Navigation>
</Wrapper>
<main class="w-full min-h-full relative overflow-y-auto mt-16 shadow-inner bg-violet-100 rounded-t-xl">
<div class="p-2 lg:p-6">
<article class="w-full space-y-2 lg:space-y-4 flex flex-col h-full">
@@ -53,15 +25,10 @@
</div>
</template>
<script setup lang="ts">
<script setup>
import Wrapper from './components/Wrapper.vue'
import Navigation from './components/Navigation.vue'
import { ref } from 'vue'
const isOpen = ref(false)
function toggleSidebar() {
isOpen.value = !isOpen.value
}
</script>
<style>

32
src/pages/state/cases.mdx Normal file
View File

@@ -0,0 +1,32 @@
---
title: What is the statewide trend in testing?
---
<Card prose={true}>
# What is the statewide trend in cases?
## What is this report useful for?
The number of new cases reported doesn't directly indicate how rapidly the disease is spreading.
By checking how many people are getting tested, you can get an idea of whether the current case load indicates major community spread or
if there is simply an unusually large number of people getting tested for one reason or another.
**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.
</Card>
<ParametersCases client:load/>
<div class="grid gap-4 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
<ChipsCases client:load/>
</div>
<div class="grid gap-4 grid-cols-1 xl:grid-cols-2">
<TrendPCRTests client:load/>
<TrendPCRPositive client:load/>
<TrendAntigenTests client:load/>
<TrendAntigenPositive client:load/>
</div>

View File

@@ -17,9 +17,7 @@ title: What is the statewide trend in testing?
This discrepancy is often apparent on Mondays, where tests and cases from over the weekend might wind up in Monday's report.
</Card>
<TestingDataSetup client:load></TestingDataSetup>
<SliceSelector client:load/>
<ParametersTesting client:load/>
<div class="grid gap-4 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
<ChipsTesting client:load/>