Source

src/components/HeroMap.jsx

// general imports, state
import React, { useState, useEffect } from 'react';
import styled from 'styled-components';

// deck GL and helper function import
import DeckGL from '@deck.gl/react';
import { PolygonLayer } from '@deck.gl/layers';
import { fitBounds } from '@math.gl/web-mercator';

import { getCSV } from '../utils';

const MapContainer = styled.div`
  height: 400px;
  position: relative;
  pointer-events: none;
  cursor: default;
  p.caption {
    position: absolute;
    bottom: 0;
    right: 0;
    color: white;
    text-align: right;
    font-size: 0.8rem;
    span {
      font-weight: bold;
    }
  }
  @media (max-width: 960px) {
    display: none;
  }
`;
// US bounds
const bounds = fitBounds({
  width: window.innerWidth > 1140 ? 650 : (window.innerWidth / 12) * 7,
  height: 400,
  bounds: [
    [-130, 54],
    [-80, 23],
  ],
});

const colorscale = [
  [255, 255, 204, 200],
  [255, 237, 160, 200],
  [254, 217, 118, 200],
  [254, 178, 76, 200],
  [253, 141, 60, 200],
  [252, 78, 42, 200],
  [227, 26, 28, 200],
  [177, 0, 38, 200],
];

const getColor = (val, bins, colors) => {
  if (isNaN(val)) return [0, 0, 0, 0];
  for (let i = 0; i < bins.length; i++) {
    if (val < bins[i]) return colors[i];
  }
  return colors[7];
};

/**
 * Animated map for the main splash map
 * @component
 * @category Components/Layout
 * 
 * @returns {JSX.Element}
 */
function HeroMap() {
  const [geoData, setGeoData] = useState([]);
  const [dateList, setDateList] = useState([]);
  const [dataBins, setDataBins] = useState([]);
  const [currDate, setCurrDate] = useState({
    current: '2020-01-30',
    previous: '2020-01-23',
  });
  const [initialDates, setInitialDates] = useState({
    current: '2020-01-30',
    previous: '2020-01-23',
  });
  const [intervalFn, setIntervalFn] = useState(null);

  // map view location
  const fixedViewstate = {
    latitude: bounds.latitude,
    longitude: bounds.longitude,
    zoom: bounds.zoom,
    bearing: 45,
    pitch: 45,
  };

  const getGeoData = async () => {
    const data = await fetch(
      `${process.env.PUBLIC_URL}/geojson/county_usfacts.geojson`,
    )
      .then((r) => r.json())
      .then((r) => {
        let returnArray = [];

        for (let i = 0; i < r.features.length; i++) {
          for (let n = 0; n < r.features[i].geometry.coordinates.length; n++) {
            returnArray.push({
              geom: r.features[i].geometry.coordinates[n],
              GEOID: r.features[i].properties.GEOID,
              population: r.features[i].properties.population,
            });
          }
        }

        return returnArray;
      });
    return data;
  };

  const formatData = (data) => {
    let returnObj = {};

    for (let i = 0; i < data.length; i++) {
      returnObj[data[i].countyFIPS] = data[i];
    }

    return returnObj;
  };

  const getDates = (data) => {
    const keys = Object.keys(data);

    for (let i = 0; i < keys.length; i++) {
      if (!Number.isNaN(Date.parse(keys[i]))) {
        return keys.slice(i);
      }
    }
  };

  const getBins = (data, dates, geoData) => {
    const finalDate = dates.slice(-1)[0];
    const weekBefore = dates.slice(-7)[0];
    let populations = {};
    const values = Object.values(data);

    for (let i = 0; i < geoData.length; i++) {
      populations[geoData[i].GEOID] = geoData[i].population;
    }

    const valArray = values.map(
      (d) =>
        ((d[finalDate] - d[weekBefore]) / 7 / populations[d.countyFIPS]) *
        100000,
    );

    valArray.sort(function (a, b) {
      return a - b;
    });

    let quantileArray = [];

    for (let i = 0; i < 8; i++) {
      quantileArray.push(
        valArray[Math.round((valArray.length / 100) * (12.5 * i))],
      );
    }

    return quantileArray;
  };

  const joinData = (geoData, data) => {
    for (let i = 0; i < geoData.length; i++) {
      geoData[i]['data'] = data[geoData[i].GEOID];
    }
    return geoData;
  };

  const handleInitialDataLoad = async () => {
    const [data, geoData] = await Promise.all([
      getCSV(
        `${process.env.PUBLIC_URL}/csv/covid_confirmed_usafacts_extract.csv`,
      ),
      getGeoData(),
    ]);
    const dates = getDates(data[0]);
    const formattedData = formatData(data);
    const bins = getBins(formattedData, dates, geoData);
    const joinedData = joinData(geoData, formattedData);
    setDataBins(bins);
    setGeoData(joinedData);
    setDateList(dates);
    setInitialDates({
      current: dates[7],
      previous: dates[0],
    });
  };

  useEffect(() => {
    handleInitialDataLoad();
  }, []);

  useEffect(() => {
    clearInterval(intervalFn);
    setIntervalFn(
      setInterval(
        () =>
          setCurrDate((prev) => {
            if (dateList.indexOf(prev.current) + 1 < dateList.length) {
              return {
                current: dateList[dateList.indexOf(prev.current) + 1],
                previous:
                  dateList[
                    (dateList.indexOf(prev.current) - 6) % dateList.length
                  ],
              };
            } else {
              return {
                ...initialDates,
              };
            }
          }),
        250,
      ),
    );
  }, [dateList]);

  const layer = new PolygonLayer({
    id: 'home choropleth',
    data: geoData,
    getFillColor: (d) =>
      getColor(
        ((d.data[currDate.current] - d.data[currDate.previous]) /
          7 /
          d.population) *
          100000,
        dataBins,
        colorscale,
      ),
    getPolygon: (d) => d.geom,
    getElevation: (d) =>
      ((((d.data[currDate.current] - d.data[currDate.previous]) /
        7 /
        d.population) *
        100000) /
        dataBins[7]) *
      25000,
    pickable: false,
    stroked: false,
    filled: true,
    wireframe: true,
    extruded: true,
    opacity: 0.8,
    material: false,
    updateTriggers: {
      getFillColor: currDate,
      getElevation: currDate,
    },
  });

  return (
    <MapContainer>
      <DeckGL
        layers={layer}
        initialViewState={fixedViewstate}
        controller={false}
      />
      <p className="caption">
        <span>{currDate.current}</span>
        <br />
        7-Day Average of New Cases per Capita by County (Source: USA Facts)
      </p>
    </MapContainer>
  );
}

export default HeroMap;