Source

src/hooks/useGetLineChartData.js

import { useEffect, useState } from 'react'
import { useSelector } from 'react-redux'
import { findIn, findAllDefaults } from '../utils'
import dataDateRanges from '../config/dataDateRanges'
import { paramsSelectors } from '../stores/paramsStore'
import { dataSelectors } from '../stores/dataStore'
const { selectGeojsonData } = dataSelectors;
const {
  selectCurrentData,
  selectDatasets,
  selectDataParams,
  selectTables,
  selectSelectionKeys
} = paramsSelectors
/**
 * Async function for fetch relevant data
 *
 * See useGetLineChartData comments for schemas
 * @category Utils/fetchers
 * 
 * @param {Object} props
 * @param {string} props.currentGeojson Current map data
 * @param {string} props.currentTimeseriesDataset Current timeseries data
 * @param {number[]} props.selectionKeys List of ID keys (optional)
 * @param {number} props.totalPopulation Total population for normalization
 * @returns {TimeSeriesData} Chart data, maximums, and relevant metadata, as
 *   below
 */
async function fetchTimeSeries({
  currentGeojson,
  currentTimeseriesDataset,
  selectionKeys,
  totalPopulation,
}) {
  const keysToFetch = selectionKeys.length
    ? [currentTimeseriesDataset, ...selectionKeys]
    : [currentTimeseriesDataset]
  const timeseriesData = await Promise.allSettled(
    keysToFetch.map((key, idx) =>
      fetch(`${process.env.PUBLIC_URL}/timeseries/${key}.json`).then((r) =>
        r.json()
      )
    )
  )
  

  let chartData = []
  for (let i = 0; i < keysToFetch.length; i++) {
    const id = keysToFetch[i]
    const data =
      i === 0
        ? timeseriesData[i].value
        : timeseriesData[i].value?.[currentTimeseriesDataset]
    if (i > 0 && data === undefined) continue 
    if (i === 0) {
      const pop = totalPopulation
      for (let j = 0; j < data.dates.length; j++) {
        const delta1 = j === 0 ? 0 : 1
        const delta2 = j < 6 ? j : 7
        chartData.push({
          date: data.dates[j],
          sum: data.sumData[j],
          sum100k: (data.sumData[j] / pop) * 100000,
          daily: data.sumData[j] - data.sumData[j - delta1],
          daily100k:
            ((data.sumData[j] - data.sumData[j - delta1]) / pop) * 100000,
          weekly: (data.sumData[j] - data.sumData[j - delta2]) / 7,
          weekly100k:
            (((data.sumData[j] - data.sumData[j - delta2]) / pop) * 100000) / 7,
        })
      }
    } else {
      const pop = currentGeojson[id].population
      for (let j = 0; j < data.length; j++) {
        const delta1 = j === 0 ? 0 : 1
        const delta2 = j < 6 ? j : 7
        chartData[j] = {
          ...chartData[j],
          keySum: chartData[j]?.keySum || 0 + data[j],
          keySum100k: chartData[j]?.keySum100k || 0 + (data[j] / pop) * 100000,
          keyDaily: chartData[j]?.keyDaily || 0 + (data[j] - data[j - delta1]),
          keyDaily100k:
            chartData[j]?.keyDaily100k ||
            0 + ((data[j] - data[j - delta1]) / pop) * 100000,
          keyWeekly:
            chartData[j]?.keyWeekly || 0 + (data[j] - data[j - delta2]) / 7,
          keyWeekly100k:
            chartData[j]?.keyWeekly100k ||
            0 + (((data[j] - data[j - delta2]) / pop) * 100000) / 7,
          [`${id}Sum`]: data[j],
          [`${id}Sum100k`]: (data[j] / pop) * 100000,
          [`${id}Daily`]: data[j] - data[j - delta1],
          [`${id}Daily100k`]: ((data[j] - data[j - delta1]) / pop) * 100000,
          [`${id}Weekly`]: (data[j] - data[j - delta2]) / 7,
          [`${id}Weekly100k`]:
            (((data[j] - data[j - delta2]) / pop) * 100000) / 7,
        }
      }
    }
  }

  let maximums = {
    sum: chartData.slice(-1)[0].sum,
    sum100k: chartData.slice(-1)[0].sum100k,
    idSum: Math.max(
      ...keysToFetch.slice(1).map((id) => chartData.slice(-1)[0][`${id}Sum`])
    ),
    idSum100k: Math.max(
      ...keysToFetch
        .slice(1)
        .map((id) => chartData.slice(-1)[0][`${id}Sum100k`])
    ),
    keySum: chartData.slice(-1)[0].keySum,
    keySum100k: chartData.slice(-1)[0].keySum100k,
  }
  for (let i = 0; i < chartData.length; i++) {
    maximums.daily = Math.max(chartData[i].daily, maximums.daily || 0)
    maximums.daily100k = Math.max(
      chartData[i].daily100k,
      maximums.daily100k || 0
    )
    maximums.weekly = Math.max(chartData[i].weekly, maximums.weekly || 0)
    maximums.weekly100k = Math.max(
      chartData[i].weekly100k,
      maximums.weekly100k || 0
    )
    maximums.keyDaily = Math.max(chartData[i].keyDaily, maximums.keyDaily || 0)
    maximums.keyDaily100k = Math.max(
      chartData[i].keyDaily100k,
      maximums.keyDaily100k || 0
    )
    maximums.keyWeekly = Math.max(
      chartData[i].keyWeekly,
      maximums.keyWeekly || 0
    )
    maximums.keyWeekly100k = Math.max(
      chartData[i].keyWeekly100k,
      maximums.keyWeekly100k || 0
    )
    maximums.idDaily = Math.max(
      ...keysToFetch.slice(1).map((id) => chartData[i][`${id}Daily`]),
      maximums.idDaily || 0
    )
    maximums.idDaily100k = Math.max(
      ...keysToFetch.slice(1).map((id) => chartData[i][`${id}Daily100k`]),
      maximums.idDaily100k || 0
    )
    maximums.idWeekly = Math.max(
      ...keysToFetch.slice(1).map((id) => chartData[i][`${id}Weekly`]),
      maximums.idWeekly || 0
    )
    maximums.idWeekly100k = Math.max(
      ...keysToFetch.slice(1).map((id) => chartData[i][`${id}Weekly100`]),
      maximums.idWeekly100k || 0
    )
  }

  return {
    maximums,
    chartData,
  }
}

/**
 * Hook to get line chart data given a particular variable and GEOID or GEOIDS
 * By default, this will provide national data. If keys are provided, properties
 * for the total of all keys will be provided as keySum, keyDaily... And
 * properties for will be provided for each key as {key}Sum, {key}Daily, eg.
 * "01001Sum", "01001Daily"
 *
 * @category Hooks
 * @param {Object} props
 * @param {string} props.table - 'cases' | 'deaths' | 'vaccination' The table to
 *   fetch data from
 * @param {string[]} props.geoid The county or state GEOID to fetch data for
 * @returns {LineChartData} Chart data, maximums, and relevant metadata
 * @subcategory Data
 */
function useGetLineChartData({ table = 'cases', geoid = [] }) {
  const [data, setData] = useState({
    maximums: {},
    chartData: [],
  })

  // pieces of redux state
  const currentData = useSelector(selectCurrentData)
  const datasets = useSelector(selectDatasets)
  const dataParams = useSelector(selectDataParams)
  const tables = useSelector(selectTables)
  const stateKeys = useSelector(selectSelectionKeys)
  const selectionKeys = geoid.length ? geoid : stateKeys

  // current state data params
  const currDataset = findIn(datasets, 'file', currentData)
  const currTables = [
    ...Object.values(currDataset.tables).map((tableId) =>
      findIn(tables, 'id', tableId)
    ),
    ...findAllDefaults(tables, currDataset.geography).map((dataspec) => ({
      ...dataspec,
    })),
  ].filter(
    (entry, index, self) =>
      self.findIndex((f) => f.table === entry.table) === index
  )

  const currentTimeseriesDataset = currTables.find(
    (t) => t.table === table
  )?.name

  const storedGeojson = useSelector(selectGeojsonData(currentData))
  const currentGeojson = storedGeojson?.properties

  const getName = ['County'].includes(currDataset.geography)
    ? (key) =>
        currentGeojson?.[key]?.NAME + ', ' + currentGeojson?.[key]?.state_abbr
    : (key) => currentGeojson?.[key]?.NAME
  
  const selectionNames = selectionKeys.map(getName)
  const totalPopulation =
    currentGeojson &&
    Object.values(currentGeojson).reduce(
      (acc, curr) => acc + curr.population,
      0
    )

  useEffect(() => {
    if (totalPopulation && currentGeojson) {
      fetchTimeSeries({
        currentGeojson,
        currentTimeseriesDataset,
        selectionKeys,
        totalPopulation,
      }).then((data) => setData(data))
      .catch(e => console.log(e))
    }
  }, [JSON.stringify(selectionKeys), totalPopulation, table])

  const currIndex = dataParams.nType.includes('time')
    ? dataParams.nIndex === null
      ? dataDateRanges[currentTimeseriesDataset].lastIndexOf(1)
      : dataParams.nIndex
    : false

  return {
    ...data,
    isTimeseries: dataParams.nType.includes('time'),
    selectionKeys,
    selectionNames,
    currRange: dataParams.nType.includes('time')
      ? dataParams.nRange || dataParams.nIndex
      : false,
    currIndex,
    currentData,
  }
}

export default useGetLineChartData

/**
 * @typedef {ChartDataEntry[]} ChartDataSchema Series long format timeseries
 *   data, suitable for use in d3/recharts viz.
 */

/**
 * @typedef {Object} ChartDataEntry Single entry of long format timeseries data,
 *   suitable for use in d3/recharts viz.
 * @property {number} date - Date for this row of data
 * @property {number} sum - Total sum of the variable for this date
 * @property {number} sum100k - Total sum of the variable for this date, per
 *   100k people
 * @property {number} daily - Daily sum of the variable for this date
 * @property {number} daily100k - Daily sum of the variable for this date, per
 *   100k people
 * @property {number} weekly - Weekly sum of the variable for this date
 * @property {number} weekly100k - Weekly sum of the variable for this date, per
 *   100k people ...
 */

/**
 * @typedef {Object} MaximumsDataSchema Summary data of maximum values for each
 *   property in ChartdataSchema, in addition to properties including keyDaily,
 *   keyWeekly... for individual GEOIDs and idDaily, idWeekly... for the sum of
 *   GEOIDs
 * @property {number} sum - Highest value
 * @property {number} sum100k - Total sum of the variable for this date, per
 *   100k people
 * @property {number} daily - Daily sum of the variable for this date
 * @property {number} daily100k - Daily sum of the variable for this date, per
 *   100k people
 * @property {number} weekly - Weekly sum of the variable for this date
 * @property {number} weekly100k - Weekly sum of the variable for this date, per
 *   100k people ...
 */

/**
 * @typedef {Object} LineChartData Return shape of useGetLineChartdata
 * @property {Object[]} chartData - See ChartDataSchema
 * @property {Object} maximums - See MaximumsDataSchema
 * @property {string} currentData - Current map data
 * @property {number} currIndex - Current date index
 * @property {number} currRange - Current date range
 * @property {number[]} selectionKeys - List of selected GEOID ids / key
 * @property {string[]} selectionNames - List of selected geography names
 */

/**
 * @typedef {Object} TimeSeriesData Return shape of fetchTimeSeries
 * @property {Object[]} chartData - See ChartDataSchema
 * @property {Object} maximums - See MaximumsDataSchema
 */