react-use#useMeasure JavaScript Examples

The following examples show how to use react-use#useMeasure. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: Table.js    From covid19india-react with MIT License 6 votes vote down vote up
TableLoader = () => {
  let [ref, {width}] = useMeasure();
  const windowWidth = window.innerWidth;

  width = width || (windowWidth >= 769 ? 528 : windowWidth);
  if (windowWidth < 769) {
    width -= 30;
  }

  const height = 45;
  const rows = 20;

  return (
    <div ref={ref} className="TableLoader">
      <ContentLoader
        viewBox={`0 0 ${width} ${height * rows}`}
        height={height * rows}
        width={width}
        speed={2}
        animate={false}
      >
        {[...Array(rows).keys()].map((i) => (
          <rect
            key={i}
            x="0"
            y={height * i}
            rx="3"
            ry="3"
            width={width}
            height={height - 5}
          />
        ))}
      </ContentLoader>
    </div>
  );
}
Example #2
Source File: MapSwitcher.js    From covid19india-react with MIT License 5 votes vote down vote up
MapSwitcher = ({mapStatistic, setMapStatistic}) => {
  const [mapSwitcher, {width}] = useMeasure();
  const [clicked, setClicked] = useState(false);
  const [count, setCount] = useState(0);

  const isPresent = LEVEL_STATISTICS.indexOf(mapStatistic) >= 0;

  const [spring, springApi] = useSpring(() => ({
    opacity: 0,
    background: `${STATISTIC_CONFIGS[mapStatistic].color}20`,
    transform: isPresent
      ? `translate3d(${
          (width * LEVEL_STATISTICS.indexOf(mapStatistic)) /
          LEVEL_STATISTICS.length
        }px, 0, 0)`
      : null,
    width: `calc(${100 / LEVEL_STATISTICS.length}%)`,
    config: config.gentle,
  }));

  useEffect(() => {
    if (width > 0) {
      const isPresent = LEVEL_STATISTICS.indexOf(mapStatistic) >= 0;
      ReactDOM.unstable_batchedUpdates(() => {
        springApi.start({
          transform: isPresent
            ? `translate3d(${
                (width * LEVEL_STATISTICS.indexOf(mapStatistic)) /
                LEVEL_STATISTICS.length
              }px, 0, 0)`
            : null,
          opacity: isPresent ? 1 : 0,
          background: isPresent
            ? `${STATISTIC_CONFIGS[mapStatistic]?.color}20`
            : null,
          delay: count === 0 ? 1500 : 0,
          onStart: setClicked.bind(this, true),
          onRest: setClicked.bind(this, false),
        });
      });
    }
  }, [count, mapStatistic, springApi, width]);

  const handleClick = useCallback(
    (statistic) => {
      setCount((prevCount) => prevCount + 1);
      setMapStatistic(statistic);
    },
    [setMapStatistic]
  );

  return (
    <div className="MapSwitcher" ref={mapSwitcher}>
      <animated.div className="highlight" style={spring}></animated.div>

      {LEVEL_STATISTICS.map((statistic, index) => (
        <div
          key={index}
          className={classnames('clickable', {[`is-${statistic}`]: !clicked})}
          onClick={handleClick.bind(this, statistic)}
          style={{width: `calc(${100 / LEVEL_STATISTICS.length}%)`}}
        ></div>
      ))}
    </div>
  );
}
Example #3
Source File: state.js    From covid19Nepal-react with MIT License 4 votes vote down vote up
function State(props) {
  const stateCode = useParams().stateCode.toUpperCase();
  const stateName = STATE_CODES[stateCode];

  const [allStateData, setAllStateData] = useState({});
  const [fetched, setFetched] = useState(false);
  const [districtZones, setDistrictZones] = useState(null);
  const [timeseries, setTimeseries] = useState({});
  const [stateData, setStateData] = useState(null);
  const [testData, setTestData] = useState({});
  const [sources, setSources] = useState({});
  const [districtData, setDistrictData] = useState({});
  const [mapOption, setMapOption] = useState('confirmed');
  const [mapSwitcher, {width}] = useMeasure();
  const [showAllDistricts, setShowAllDistricts] = useState(false);
  const [regionHighlighted, setRegionHighlighted] = useState({
    state: stateName,
  });

  useEffectOnce(() => {
    getState(stateCode);
  });

  const getState = async (code) => {
    try {
      const [
        {data: dataResponse},
        {data: stateDistrictWiseResponse},
        {data: statesDailyResponse},
        {data: stateTestResponse},
        {data: sourcesResponse},
        {data: zonesResponse},
      ] = await Promise.all([
        axios.get('https://api.nepalcovid19.org/latest_data.json'),
        axios.get('https://api.nepalcovid19.org/state-district-wise.json'),
        axios.get('https://api.nepalcovid19.org/states_daily.json'),
        axios.get('https://api.nepalcovid19.org/state_test_data.json'),
        axios.get('https://api.nepalcovid19.org/sources_list.json'),
        axios.get(`${DATA_DIR}/zones.json`),
      ]);
      const name = STATE_CODES[code];

      const states = dataResponse.statewise;
      setAllStateData(states.filter((state) => state.statecode !== code));
      setStateData([states.find((s) => s.statecode === code)]);
      // Timeseries
      const ts = parseStateTimeseries(statesDailyResponse)[code];
      const testTs = parseStateTestTimeseries(
        stateTestResponse.states_tested_data
      )[code];
      // Merge
      const tsMerged = mergeTimeseries({[code]: ts}, {[code]: testTs});
      setTimeseries(tsMerged[code]);
      // District data
      setDistrictData({
        [name]: stateDistrictWiseResponse[name],
      });
      const sourceList = sourcesResponse.sources_list;
      setSources(sourceList.filter((state) => state.statecode === code));

      const statesTests = stateTestResponse.states_tested_data;
      setTestData(
        statesTests.filter(
          (obj) => obj.state === name && obj.totaltested !== ''
        )
      );

      setDistrictZones(parseDistrictZones(zonesResponse.zones, stateName));

      setFetched(true);
      anime({
        targets: '.highlight',
        duration: 200,
        delay: 3000,
        translateX:
          mapOption === 'confirmed'
            ? `${width * 0}px`
            : mapOption === 'active'
            ? `${width * 0.25}px`
            : mapOption === 'recovered'
            ? `${width * 0.5}px`
            : mapOption === 'deceased'
            ? `${width * 0.75}px`
            : '0px',
        easing: 'spring(1, 80, 90, 10)',
        opacity: 1,
      });
    } catch (err) {
      console.log(err);
    }
  };

  const testObjLast = testData[testData.length - 1];
  const population = STATE_POPULATIONS[stateName];

  function toggleShowAllDistricts() {
    setShowAllDistricts(!showAllDistricts);
  }

  const getGridRowCount = () => {
    const gridColumnCount = window.innerWidth >= 540 ? 3 : 2;
    const districtCount =
      (districtData[stateName] &&
        Object.keys(districtData[stateName].districtData).length) ||
      0;
    const gridRowCount = Math.ceil(districtCount / gridColumnCount);
    return gridRowCount;
  };
  const gridRowCount = getGridRowCount();

  if (!stateName) {
    return <Redirect to="/" />;
  } else {
    return (
      <React.Fragment>
        <Helmet>
          <title>
            Coronavirus Outbreak in {STATE_CODES[stateCode]} - nepalcovid19.org
          </title>
          <meta
            name="title"
            content={`Coronavirus Outbreak in ${STATE_CODES[stateCode]}: Latest Map and Case Count`}
          />
        </Helmet>

        <div className="State">
          <div className="state-left">
            <Breadcrumbs
              stateName={stateName}
              stateCode={stateCode}
              fetched={fetched}
              allStateData={allStateData}
            />

            <div className="header">
              <div
                className="header-left fadeInUp"
                style={{animationDelay: '0.3s'}}
              >
                <h1>{stateName}</h1>
                <h5>
                  Last Updated on{' '}
                  {stateData && Object.keys(stateData[0]).length
                    ? formatDateAbsolute(stateData[0].lastupdatedtime)
                    : ''}
                </h5>
              </div>

              <div
                className="header-right fadeInUp"
                style={{animationDelay: '0.5s'}}
              >
                <h5>Tested</h5>
                <h2>{formatNumber(testObjLast?.totaltested)}</h2>
                <h5 className="timestamp">
                  {!isNaN(
                    parse(testObjLast?.updatedon, 'dd/MM/yyyy', new Date())
                  )
                    ? `As of ${format(
                        parse(testObjLast?.updatedon, 'dd/MM/yyyy', new Date()),
                        'dd MMM'
                      )}`
                    : ''}
                </h5>
                <h5>
                  {'per '}
                  {testObjLast?.totaltested && (
                    <a href={testObjLast.source} target="_noblank">
                      source
                    </a>
                  )}
                </h5>
              </div>
            </div>

            {fetched && (
              <div className="map-switcher" ref={mapSwitcher}>
                <div
                  className={`highlight ${mapOption}`}
                  style={{
                    transform: `translateX(${width * 0}px)`,
                    opacity: 0,
                  }}
                ></div>
                <div
                  className="clickable"
                  onClick={() => {
                    setMapOption('confirmed');
                    anime({
                      targets: '.highlight',
                      translateX: `${width * 0}px`,
                      easing: 'spring(1, 80, 90, 10)',
                    });
                  }}
                ></div>
                <div
                  className="clickable"
                  onClick={() => {
                    setMapOption('active');
                    anime({
                      targets: '.highlight',
                      translateX: `${width * 0.25}px`,
                      easing: 'spring(1, 80, 90, 10)',
                    });
                  }}
                ></div>
                <div
                  className="clickable"
                  onClick={() => {
                    setMapOption('recovered');
                    anime({
                      targets: '.highlight',
                      translateX: `${width * 0.5}px`,
                      easing: 'spring(1, 80, 90, 10)',
                    });
                  }}
                ></div>
                <div
                  className="clickable"
                  onClick={() => {
                    setMapOption('deceased');
                    anime({
                      targets: '.highlight',
                      translateX: `${width * 0.75}px`,
                      easing: 'spring(1, 80, 90, 10)',
                    });
                  }}
                ></div>
              </div>
            )}

            {fetched && <Level data={stateData[0]} />}
            {fetched && <Minigraph timeseries={timeseries} />}
            {fetched && (
              <MapExplorer
                mapName={stateName}
                states={stateData}
                districts={districtData}
                zones={districtZones}
                stateTestData={testData}
                regionHighlighted={regionHighlighted}
                setRegionHighlighted={setRegionHighlighted}
                mapOption={mapOption}
                isCountryLoaded={false}
              />
            )}

            {fetched && (
              <div className="meta-secondary">
                <div className="alert">
                  <Icon.AlertCircle />
                  <div className="alert-right">
                    Awaiting district details for{' '}
                    {districtData[stateName]?.districtData['Unknown']
                      ?.confirmed || '0'}{' '}
                    cases
                  </div>
                </div>
                <div className="alert">
                  <Icon.Compass />
                  <div className="alert-right">
                    Data collected from sources{' '}
                    {sources.length > 0
                      ? Object.keys(sources[0]).map((key, index) => {
                          if (key.match('source') && sources[0][key] !== '') {
                            const num = key.match(/\d+/);
                            return (
                              <React.Fragment key={index}>
                                {num > 1 ? ',' : ''}
                                <a href={sources[0][key]}>{num}</a>
                              </React.Fragment>
                            );
                          }
                          return null;
                        })
                      : ''}
                  </div>
                </div>
              </div>
            )}

            {fetched && (
              <StateMeta
                stateData={stateData[0]}
                lastTestObject={testObjLast}
                population={population}
                lastSevenDaysData={timeseries.slice(-7)}
                totalData={allStateData.filter(
                  (state) => state.statecode === 'TT'
                )}
              />
            )}
          </div>

          <div className="state-right">
            {fetched && (
              <React.Fragment>
                <div
                  className="district-bar"
                  style={!showAllDistricts ? {display: 'flex'} : {}}
                >
                  <div
                    className="district-bar-left fadeInUp"
                    style={{animationDelay: '0.6s'}}
                  >
                    <h2 className={mapOption}>Top districts</h2>
                    <div
                      className={`districts ${
                        showAllDistricts ? 'is-grid' : ''
                      }`}
                      style={
                        showAllDistricts
                          ? {gridTemplateRows: `repeat(${gridRowCount}, 2rem)`}
                          : {}
                      }
                    >
                      {districtData[stateName]
                        ? Object.keys(districtData[stateName].districtData)
                            .filter((d) => d !== 'Unknown')
                            .sort((a, b) => {
                              const districtB =
                                districtData[stateName].districtData[b];
                              const districtA =
                                districtData[stateName].districtData[a];
                              return (
                                districtB[mapOption] - districtA[mapOption]
                              );
                            })
                            .slice(0, showAllDistricts ? undefined : 5)
                            .map((district, index) => {
                              const cases =
                                districtData[stateName].districtData[district];
                              return (
                                <div key={index} className="district">
                                  <h2>{cases[mapOption]}</h2>
                                  <h5>{district}</h5>
                                  {mapOption !== 'active' && (
                                    <div className="delta">
                                      <Icon.ArrowUp className={mapOption} />
                                      <h6 className={mapOption}>
                                        {cases.delta[mapOption]}
                                      </h6>
                                    </div>
                                  )}
                                </div>
                              );
                            })
                        : ''}
                    </div>
                    {districtData[stateName] &&
                      Object.keys(districtData[stateName].districtData).length >
                        5 && (
                        <button
                          className="button"
                          onClick={toggleShowAllDistricts}
                        >
                          {showAllDistricts ? `View less` : `View all`}
                        </button>
                      )}
                  </div>
                  <div className="district-bar-right">
                    {(mapOption === 'confirmed' ||
                      mapOption === 'deceased') && (
                      <div
                        className="happy-sign fadeInUp"
                        style={{animationDelay: '0.6s'}}
                      >
                        {timeseries
                          .slice(-5)
                          .every((day) => day[`daily${mapOption}`] === 0) && (
                          <div
                            className={`alert ${
                              mapOption === 'confirmed' ? 'is-green' : ''
                            }`}
                          >
                            <Icon.Smile />
                            <div className="alert-right">
                              No new {mapOption} cases in the past five days
                            </div>
                          </div>
                        )}
                      </div>
                    )}
                    {
                      <DeltaBarGraph
                        timeseries={timeseries.slice(-5)}
                        arrayKey={`daily${mapOption}`}
                      />
                    }
                  </div>
                </div>

                {false && (
                  <Link to="/essentials">
                    <div
                      className="to-essentials fadeInUp"
                      style={{animationDelay: '0.9s'}}
                    >
                      <h2>Go to essentials</h2>
                      <Icon.ArrowRightCircle />
                    </div>
                  </Link>
                )}

                <TimeSeriesExplorer timeseries={timeseries} />
              </React.Fragment>
            )}
          </div>

          <div className="state-left">
            <div className="Clusters fadeInUp" style={{animationDelay: '0.8s'}}>
              <h1>Network of Transmission</h1>
              <Clusters stateCode={stateCode} />
            </div>
          </div>

          <div className="state-right"></div>
        </div>
        <Footer />
      </React.Fragment>
    );
  }
}
Example #4
Source File: DeltaBarGraph.js    From covid19india-react with MIT License 4 votes vote down vote up
function DeltaBarGraph({timeseries, statistic, lookback}) {
  const svgRef = useRef();
  const [wrapperRef, {width, height}] = useMeasure();

  const pastDates = Object.keys(timeseries || {}).filter(
    (date) => date <= getIndiaDateYesterdayISO()
  );
  const dates = pastDates.slice(-lookback);

  const getDeltaStatistic = useCallback(
    (date, statistic) => {
      return getStatistic(timeseries?.[date], 'delta', statistic);
    },
    [timeseries]
  );

  useEffect(() => {
    if (!width) return;
    const svg = select(svgRef.current);

    const chartRight = width - margin.right;
    const chartBottom = height - margin.bottom;
    const r = 5;

    // const formatTime = timeFormat('%e %b');
    const xScale = scaleBand()
      .domain(dates)
      .range([margin.left, chartRight])
      .paddingInner(width / 1000);

    const [statisticMin, statisticMax] = extent(dates, (date) =>
      getDeltaStatistic(date, statistic)
    );

    const yScale = scaleLinear()
      .domain([Math.min(0, statisticMin || 0), Math.max(1, statisticMax || 0)])
      .range([chartBottom, margin.top]);

    const xAxis = axisBottom(xScale)
      .tickSize(0)
      .tickFormat((date) => formatDate(date, 'dd MMM'));

    const t = svg.transition().duration(D3_TRANSITION_DURATION);

    const statisticConfig = STATISTIC_CONFIGS[statistic];

    svg
      .select('.x-axis')
      .transition(t)
      .style('transform', `translate3d(0, ${yScale(0)}px, 0)`)
      .call(xAxis)
      .on('start', () => svg.select('.domain').remove())
      .selectAll('text')
      .attr('y', 0)
      .attr('dy', (date, i) =>
        getDeltaStatistic(date, statistic) < 0 ? '-1em' : '1.5em'
      )
      .style('text-anchor', 'middle')
      .attr('fill', statisticConfig.color);

    svg
      .selectAll('.bar')
      .data(dates)
      .join((enter) =>
        enter
          .append('path')
          .attr('class', 'bar')
          .attr('d', (date) =>
            roundedBar(xScale(date), yScale(0), xScale.bandwidth(), 0, r)
          )
      )
      .transition(t)
      .attr('d', (date) =>
        roundedBar(
          xScale(date),
          yScale(0),
          xScale.bandwidth(),
          yScale(0) - yScale(getDeltaStatistic(date, statistic)),
          r
        )
      )
      .attr('fill', (date, i) => {
        return i < dates.length - 1
          ? statisticConfig.color + '90'
          : statisticConfig.color;
      });

    const textSelection = svg
      .selectAll('.label')
      .data(dates)
      .join('text')
      .attr('class', 'label')
      .attr('x', (date) => xScale(date) + xScale.bandwidth() / 2)
      .text((date) =>
        formatNumber(
          getDeltaStatistic(date, statistic),
          statisticConfig?.showDelta || statisticConfig?.nonLinear
            ? statisticConfig.format
            : 'short'
        )
      );

    textSelection
      .transition(t)
      .attr('fill', statisticConfig.color)
      .attr('y', (date) => {
        const val = getDeltaStatistic(date, statistic);
        return yScale(val) + (val < 0 ? 15 : -6);
      });

    textSelection
      .append('tspan')
      .attr(
        'dy',
        (date) => `${getDeltaStatistic(date, statistic) < 0 ? 1.2 : -1.2}em`
      )
      .attr('x', (date) => xScale(date) + xScale.bandwidth() / 2)
      .text((date, i) => {
        if (i === 0) return '';
        const prevVal = getDeltaStatistic(dates[i - 1], statistic);
        if (!prevVal) return '';
        const delta = getDeltaStatistic(date, statistic) - prevVal;
        return `${delta > 0 ? '+' : ''}${formatNumber(
          (100 * delta) / Math.abs(prevVal),
          '%'
        )}`;
      })
      .transition(t)
      .attr('fill', statisticConfig.color + '90');
  }, [dates, height, statistic, width, getDeltaStatistic]);

  return (
    <div className="DeltaBarGraph" ref={wrapperRef}>
      <svg
        ref={svgRef}
        width={width}
        height={250}
        viewBox={`0 0 ${width} ${height}`}
        preserveAspectRatio="xMidYMid meet"
      >
        <g className="x-axis" />
        <g className="y-axis" />
      </svg>
    </div>
  );
}
Example #5
Source File: MapLegend.js    From covid19india-react with MIT License 4 votes vote down vote up
function MapLegend({data, statistic, mapViz, mapScale}) {
  const {t} = useTranslation();
  const svgLegendRef = useRef(null);
  const svgLegendChoroRef = useRef(null);
  const [wrapperRef, {width}] = useMeasure();

  useEffect(() => {
    const t = transition().duration(D3_TRANSITION_DURATION);

    if (mapViz !== MAP_VIZS.CHOROPLETH) {
      const svg = select(svgLegendChoroRef.current);
      svg
        .select('.ramp')
        .transition(t)
        .attr('opacity', 0)
        .attr('display', 'none')
        .attr('xlink:href', null);

      svg
        .select('.bars')
        .selectAll('rect')
        .transition(t)
        .attr('opacity', 0)
        .remove();
      svg.selectAll('.axis > *:not(.axistext)').remove();
      svg.select('.axistext').text('');
    }

    if (mapViz !== MAP_VIZS.BUBBLE) {
      const svg = select(svgLegendRef.current);
      svg
        .select('.circles')
        .selectAll('circle')
        .transition(t)
        .attr('r', 0)
        .attr('cy', 0)
        .remove();
      svg.selectAll('.circle-axis > *').remove();
    }

    if (mapViz !== MAP_VIZS.SPIKES) {
      const svg = select(svgLegendRef.current);
      svg
        .select('.spikes')
        .call((g) =>
          g.selectAll('path').transition(t).attr('d', spike(0)).remove()
        )
        .call((g) => g.selectAll('text').remove())
        .transition(t)
        .selectAll('g')
        .remove();
      svg.selectAll('.spike-axis > *').remove();
    }
  }, [mapViz]);

  useEffect(() => {
    if (!width) return;

    const statisticConfig = STATISTIC_CONFIGS[statistic];
    const zoom = width / MAP_DIMENSIONS[0];

    if (mapViz === MAP_VIZS.BUBBLE) {
      const svg = select(svgLegendRef.current);

      const [, domainMax] = mapScale.domain();

      const legend = svg
        .select('.circles')
        .attr('transform', `translate(48,40)`)
        .attr('text-anchor', 'middle');

      const legendRadius = [0.1, 0.4, 1].map((d) => d * domainMax);

      legend
        .selectAll('circle')
        .data(legendRadius)
        .join('circle')
        .attr('fill', 'none')
        .attr('stroke', statisticConfig.color + '70')
        .transition(t)
        .attr('cy', (d) => -mapScale(d))
        .attr('r', (d) => mapScale(d));

      const yScale = mapScale.copy().range([0, -2 * mapScale(domainMax)]);

      svg
        .select('.circle-axis')
        .attr('transform', `translate(48,50)`)
        .transition(t)
        .call(
          axisRight(yScale)
            .tickSize(0)
            .tickPadding(0)
            .tickValues(legendRadius)
            .tickFormat((num) =>
              formatNumber(
                num,
                statisticConfig.format === 'long'
                  ? 'short'
                  : statisticConfig.format
              )
            )
        )
        .selectAll('.tick text')
        .style('text-anchor', 'middle')
        .attr('font-size', 10 / zoom);

      svg.select('.circle-axis').call((g) => g.select('.domain').remove());
    } else if (mapViz === MAP_VIZS.SPIKE) {
      const svg = select(svgLegendRef.current);
      const ticks = mapScale.ticks(3).slice(1).reverse();

      const gap = 28 / zoom;

      svg
        .select('.spikes')
        .attr('transform', `translate(32,24)`)
        .selectAll('g')
        .data(ticks)
        .join((enter) =>
          enter.append('g').call((g) =>
            g
              .append('path')
              .attr('fill-opacity', 0.3)
              .attr('d', (d) => spike(0))
          )
        )
        .attr('transform', (d, i) => `translate(${i * gap},0)`)
        .call((g) =>
          g
            .select('path')
            .transition(t)
            .attr('d', (d) => spike(mapScale(d)))
            .attr('fill', statisticConfig.color + '70')
            .attr('stroke', statisticConfig.color + '70')
        );

      const xScale = mapScale.copy().range([gap * ticks.length, 0]);
      svg
        .select('.spike-axis')
        .attr('transform', `translate(32,32)`)
        .transition(t)
        .call(
          axisBottom(xScale)
            .tickSize(0)
            .tickPadding(0)
            .tickValues(ticks)
            .tickFormat((num) =>
              formatNumber(
                num,
                statisticConfig.format === 'long'
                  ? 'short'
                  : statisticConfig.format
              )
            )
        )
        .selectAll('.tick text')
        .style('text-anchor', 'middle')
        .attr('font-size', 10 / zoom);

      svg.select('.spike-axis').call((g) => g.select('.domain').remove());
    } else {
      const svg = select(svgLegendChoroRef.current);
      svg.call(() =>
        legend({
          svg: svg,
          color: mapScale,
          width: width,
          height: MAP_LEGEND_HEIGHT,
          ticks: 5,
          tickFormat: function (d, i, n) {
            if (statisticConfig?.mapConfig?.colorScale) {
              return d;
            } else if (mapViz === MAP_VIZS.CHOROPLETH && !Number.isInteger(d)) {
              return '';
            } else if (i === n.length - 1) {
              return formatNumber(d, statisticConfig.format) + '+';
            } else {
              return formatNumber(d, statisticConfig.format);
            }
          },
          marginLeft: 2,
          marginRight: 0,
        })
      );
      svg.attr('class', statisticConfig?.mapConfig?.colorScale ? 'zone' : '');
    }
  }, [t, width, statistic, mapScale, mapViz]);

  return (
    <div
      className="svg-parent maplegend"
      ref={wrapperRef}
      style={{height: 2 * MAP_LEGEND_HEIGHT}}
    >
      <svg
        id="legend"
        preserveAspectRatio="xMinYMid meet"
        ref={svgLegendRef}
        viewBox={`0 0 ${MAP_DIMENSIONS[0]} ${MAP_LEGEND_HEIGHT}`}
      >
        <g className="circles"></g>
        <g className="spikes"></g>
        <g className="circle-axis"></g>
        <g className="spike-axis"></g>
        <g className="axis">
          <text className="axistext" />
        </g>
      </svg>
      <svg
        id="legend-choro"
        preserveAspectRatio="xMinYMid meet"
        ref={svgLegendChoroRef}
      >
        <image className="ramp" preserveAspectRatio="none" />
        <g className="bars"></g>
        <g className="axis">
          <text className="axistext" />
        </g>
      </svg>
      <canvas
        className="color-scale"
        style={{position: 'absolute', height: 0}}
      />
    </div>
  );
}
Example #6
Source File: Minigraphs.js    From covid19india-react with MIT License 4 votes vote down vote up
function Minigraphs({timeseries, date: timelineDate}) {
  const refs = useRef([]);
  const endDate = timelineDate || getIndiaDateYesterdayISO();

  let [wrapperRef, {width}] = useMeasure();
  width = Math.min(width, maxWidth);

  const dates = useMemo(() => {
    const pastDates = Object.keys(timeseries || {}).filter(
      (date) => date <= endDate
    );
    const lastDate = pastDates[pastDates.length - 1];

    const cutOffDateLower = formatISO(
      subDays(parseIndiaDate(lastDate), MINIGRAPH_LOOKBACK_DAYS),
      {representation: 'date'}
    );
    return pastDates.filter((date) => date >= cutOffDateLower);
  }, [endDate, timeseries]);

  const getMinigraphStatistic = useCallback(
    (date, statistic) => {
      return getStatistic(timeseries?.[date], 'delta', statistic);
    },
    [timeseries]
  );

  useEffect(() => {
    if (!width) return;

    const T = dates.length;

    const chartRight = width - margin.right;
    const chartBottom = height - margin.bottom;

    const xScale = scaleTime()
      .clamp(true)
      .domain([
        parseIndiaDate(dates[0] || endDate),
        parseIndiaDate(dates[T - 1]) || endDate,
      ])
      .range([margin.left, chartRight]);

    refs.current.forEach((ref, index) => {
      const svg = select(ref);
      const statistic = LEVEL_STATISTICS[index];
      const color = STATISTIC_CONFIGS[statistic].color;

      const dailyMaxAbs = max(dates, (date) =>
        Math.abs(getMinigraphStatistic(date, statistic))
      );

      const yScale = scaleLinear()
        .clamp(true)
        .domain([-dailyMaxAbs, dailyMaxAbs])
        .range([chartBottom, margin.top]);

      const linePath = line()
        .curve(curveMonotoneX)
        .x((date) => xScale(parseIndiaDate(date)))
        .y((date) => yScale(getMinigraphStatistic(date, statistic)));

      let pathLength;
      svg
        .selectAll('path')
        .data(T ? [dates] : [])
        .join(
          (enter) =>
            enter
              .append('path')
              .attr('fill', 'none')
              .attr('stroke', color + '99')
              .attr('stroke-width', 2.5)
              .attr('d', linePath)
              .attr('stroke-dasharray', function () {
                return (pathLength = this.getTotalLength());
              })
              .call((enter) =>
                enter
                  .attr('stroke-dashoffset', pathLength)
                  .transition()
                  .delay(100)
                  .duration(2500)
                  .attr('stroke-dashoffset', 0)
              ),
          (update) =>
            update
              .attr('stroke-dasharray', null)
              .transition()
              .duration(500)
              .attrTween('d', function (date) {
                const previous = select(this).attr('d');
                const current = linePath(date);
                return interpolatePath(previous, current);
              })
              .selection()
        );

      svg
        .selectAll('circle')
        .data(T ? [dates[T - 1]] : [])
        .join(
          (enter) =>
            enter
              .append('circle')
              .attr('fill', color)
              .attr('r', 2.5)
              .attr('cx', (date) => xScale(parseIndiaDate(date)))
              .attr('cy', (date) =>
                yScale(getMinigraphStatistic(date, statistic))
              )
              .style('opacity', 0)
              .call((enter) =>
                enter
                  .transition()
                  .delay(2100)
                  .duration(500)
                  .style('opacity', 1)
                  .attr('cx', (date) => xScale(parseIndiaDate(date)))
                  .attr('cy', (date) =>
                    yScale(getMinigraphStatistic(date, statistic))
                  )
              ),
          (update) =>
            update
              .transition()
              .duration(500)
              .attr('cx', (date) => xScale(parseIndiaDate(date)))
              .attr('cy', (date) =>
                yScale(getMinigraphStatistic(date, statistic))
              )
              .style('opacity', 1)
              .selection()
        );
    });
  }, [endDate, dates, width, getMinigraphStatistic]);

  return (
    <div className="Minigraph">
      {LEVEL_STATISTICS.map((statistic, index) => (
        <div
          key={statistic}
          className={classnames('svg-parent')}
          ref={index === 0 ? wrapperRef : null}
          style={{width: `calc(${100 / LEVEL_STATISTICS.length}%)`}}
        >
          <svg
            ref={(el) => {
              refs.current[index] = el;
            }}
            preserveAspectRatio="xMidYMid meet"
            width={width}
            height={height}
          />
        </div>
      ))}
    </div>
  );
}
Example #7
Source File: StatisticDropdown.js    From covid19india-react with MIT License 4 votes vote down vote up
StatisticDropdown = ({
  currentStatistic,
  isPerLakh,
  delta7Mode,
  mapStatistic,
  setMapStatistic,
  mapType,
  hideDistrictTestData,
  hideVaccinated,
  zoneColor,
}) => {
  const {t} = useTranslation();
  const [wrapperRef, {width}] = useMeasure();
  const selectRef = useRef();

  const currentStatisticConfig = STATISTIC_CONFIGS[currentStatistic];

  const statistics = useMemo(() => {
    const filteredStatistics = TABLE_STATISTICS_EXPANDED.filter(
      (statistic) =>
        (mapType === MAP_TYPES.COUNTRY ||
          STATISTIC_CONFIGS[statistic]?.category !== 'tested' ||
          !hideDistrictTestData) &&
        (STATISTIC_CONFIGS[statistic]?.category !== 'vaccinated' ||
          !hideVaccinated)
    );
    return filteredStatistics.includes(currentStatistic)
      ? filteredStatistics
      : [currentStatistic, ...filteredStatistics];
  }, [currentStatistic, mapType, hideDistrictTestData, hideVaccinated]);

  const handleChange = useCallback(
    (event) => {
      setMapStatistic(event.target.value);
    },
    [setMapStatistic]
  );

  useEffect(() => {
    const tempSelect = document.createElement('select');
    const tempOption = document.createElement('option');

    tempOption.textContent = STATISTIC_CONFIGS[mapStatistic]?.displayName;
    tempSelect.style.cssText += `
      visibility: hidden;
      position: fixed;
      `;

    tempSelect.appendChild(tempOption);
    selectRef.current.after(tempSelect);

    const tempSelectWidth = tempSelect.getBoundingClientRect().width;
    if (tempSelectWidth > 0) {
      selectRef.current.style.width = `${tempSelectWidth + 2}px`;
    }
    tempSelect.remove();
  }, [width, mapStatistic]);

  return (
    <div className="StatisticDropdown" ref={wrapperRef}>
      <div className={classnames('triangle-icon')}>
        <TriangleRightIcon size={20} />
      </div>
      <select
        ref={selectRef}
        value={currentStatistic}
        className={classnames(currentStatistic, zoneColor)}
        style={
          (zoneColor && {
            color: zoneColor,
            backgroundColor: zoneColor + '20',
            outlineColor: zoneColor + '40',
          }) ||
          {}
        }
        onChange={handleChange}
      >
        {statistics.map((statistic) => {
          const statisticConfig = STATISTIC_CONFIGS[statistic];
          return (
            <option key={statistic} value={statistic}>
              {t(capitalize(statisticConfig?.displayName))}
            </option>
          );
        })}
      </select>
      <span>{`${
        isPerLakh &&
        !currentStatisticConfig?.nonLinear &&
        mapStatistic !== 'population'
          ? ` ${t('per lakh')}`
          : ''
      }${
        (delta7Mode && currentStatisticConfig?.showDelta) ||
        currentStatisticConfig?.onlyDelta7
          ? ` ${t('in last 7 days')}`
          : ''
      }`}</span>
    </div>
  );
}
Example #8
Source File: Table.js    From covid19india-react with MIT License 4 votes vote down vote up
function Table({
  data: states,
  date: timelineDate,
  regionHighlighted,
  setRegionHighlighted,
  expandTable,
  setExpandTable,
  hideDistrictData,
  hideDistrictTestData,
  hideVaccinated,
  lastDataDate,
  noDistrictDataStates,
}) {
  const {t} = useTranslation();
  const [sortData, setSortData] = useSessionStorage('sortData', {
    sortColumn: 'confirmed',
    isAscending: false,
    delta: false,
  });
  const [page, setPage] = useState(0);
  const [delta7Mode, setDelta7Mode] = useState(false);

  const [tableContainerRef, {width: tableWidth}] = useMeasure();

  const handleSortClick = useCallback(
    (statistic) => {
      if (sortData.sortColumn !== statistic) {
        setSortData(
          produce(sortData, (draftSortData) => {
            if (
              sortData.sortColumn === 'regionName' ||
              statistic === 'regionName'
            ) {
              draftSortData.isAscending = !sortData.isAscending;
            }
            draftSortData.sortColumn = statistic;
          })
        );
      } else {
        setSortData(
          produce(sortData, (draftSortData) => {
            draftSortData.isAscending = !sortData.isAscending;
          })
        );
      }
    },
    [sortData, setSortData]
  );

  const trail = useTrail(5, {
    from: {transform: 'translate3d(0, 10px, 0)', opacity: 0},
    to: {transform: 'translate3d(0, 0px, 0)', opacity: 1},
    config: config.wobbly,
  });

  const [allDistricts, setAllDistricts] = useState();

  const [tableOption, setTableOption] = useState('States');
  const [isPerLakh, setIsPerLakh] = useState(false);
  const [isInfoVisible, setIsInfoVisible] = useState(false);

  const getTableStatistic = useCallback(
    (data, statistic, type) => {
      const statisticConfig = STATISTIC_CONFIGS[statistic];
      if (type == 'total' && statisticConfig?.onlyDelta7) {
        type = 'delta7';
      }

      if (statisticConfig?.showDelta && type === 'total' && delta7Mode) {
        type = 'delta7';
      }

      return getStatistic(data, type, statistic, {
        expiredDate: lastDataDate,
        normalizedByPopulationPer: isPerLakh ? 'lakh' : null,
      });
    },
    [isPerLakh, lastDataDate, delta7Mode]
  );

  const districts = useMemo(() => {
    if (!isPerLakh) {
      return allDistricts;
    } else {
      return Object.keys(allDistricts || {})
        .filter(
          (districtKey) =>
            getStatistic(allDistricts[districtKey], 'total', 'population') > 0
        )
        .reduce((res, districtKey) => {
          res[districtKey] = allDistricts[districtKey];
          return res;
        }, {});
    }
  }, [isPerLakh, allDistricts]);

  const numPages = Math.ceil(
    Object.keys(districts || {}).length / DISTRICT_TABLE_COUNT
  );

  const sortingFunction = useCallback(
    (regionKeyA, regionKeyB) => {
      if (sortData.sortColumn !== 'regionName') {
        const statisticConfig = STATISTIC_CONFIGS[sortData.sortColumn];
        const dataType =
          sortData.delta && statisticConfig?.showDelta ? 'delta' : 'total';

        const statisticA = getTableStatistic(
          districts?.[regionKeyA] || states[regionKeyA],
          sortData.sortColumn,
          dataType
        );
        const statisticB = getTableStatistic(
          districts?.[regionKeyB] || states[regionKeyB],
          sortData.sortColumn,
          dataType
        );
        return sortData.isAscending
          ? statisticA - statisticB
          : statisticB - statisticA;
      } else {
        const regionNameA =
          districts?.[regionKeyA]?.districtName || STATE_NAMES[regionKeyA];
        const regionNameB =
          districts?.[regionKeyB]?.districtName || STATE_NAMES[regionKeyB];
        return sortData.isAscending
          ? regionNameA.localeCompare(regionNameB)
          : regionNameB.localeCompare(regionNameA);
      }
    },
    [
      districts,
      getTableStatistic,
      sortData.delta,
      sortData.isAscending,
      sortData.sortColumn,
      states,
    ]
  );

  const _setTableOption = useCallback(() => {
    setTableOption((prevTableOption) =>
      prevTableOption === 'States' ? 'Districts' : 'States'
    );
  }, []);

  useEffect(() => {
    const workerInstance = worker();
    workerInstance.getDistricts(states);
    workerInstance.addEventListener('message', (message) => {
      if (message.data.type !== 'RPC') {
        setAllDistricts(message.data);
        workerInstance.terminate();
      }
    });
  }, [tableOption, states]);

  useEffect(() => {
    setPage((p) => Math.max(0, Math.min(p, numPages - 1)));
  }, [numPages]);

  const handlePageClick = (direction) => {
    if (Math.abs(direction) === 1) {
      setPage(Math.min(Math.max(0, page + direction), numPages - 1));
    } else if (direction < 0) {
      setPage(0);
    } else if (direction > 0) {
      setPage(numPages - 1);
    }
  };

  const transition = useTransition(isInfoVisible, {
    from: TABLE_FADE_OUT,
    enter: TABLE_FADE_IN,
    leave: TABLE_FADE_OUT,
  });

  const tableStatistics = (
    expandTable ? TABLE_STATISTICS_EXPANDED : TABLE_STATISTICS
  ).filter(
    (statistic) =>
      (tableOption === 'States' ||
        STATISTIC_CONFIGS[statistic]?.category !== 'tested' ||
        !hideDistrictTestData) &&
      (STATISTIC_CONFIGS[statistic]?.category !== 'vaccinated' ||
        !hideVaccinated)
  );

  const showDistricts = tableOption === 'Districts' && !hideDistrictData;

  useEffect(() => {
    if (!showDistricts) {
      setPage(0);
    }
  }, [showDistricts]);

  useKeyPressEvent('?', () => {
    setIsInfoVisible(!isInfoVisible);
  });

  return (
    <div className="Table">
      <div className="table-top">
        <div className="table-top-left">
          <Tooltip message={'Toggle between states/districts'} hold>
            <animated.div
              className={classnames('toggle', 'option-toggle', {
                'is-highlighted': showDistricts,
                disabled: hideDistrictData,
              })}
              onClick={_setTableOption}
              style={trail[0]}
            >
              <DistrictIcon />
            </animated.div>
          </Tooltip>

          <Tooltip message={'Per lakh people'} hold>
            <animated.div
              className={classnames('toggle', 'lakh-toggle', {
                'is-highlighted': isPerLakh,
              })}
              onClick={setIsPerLakh.bind(this, !isPerLakh)}
              style={trail[1]}
            >
              <PerLakhIcon />
            </animated.div>
          </Tooltip>

          <Tooltip message={'Last 7 day values'} hold>
            <animated.div
              className={classnames('toggle', 'delta-toggle', {
                'is-highlighted': delta7Mode,
              })}
              style={trail[2]}
              onClick={setDelta7Mode.bind(this, !delta7Mode)}
            >
              <Delta7Icon />
            </animated.div>
          </Tooltip>

          <animated.div
            className={classnames('toggle', 'info-toggle', {
              'is-highlighted': isInfoVisible,
            })}
            onClick={setIsInfoVisible.bind(this, !isInfoVisible)}
            style={trail[3]}
          >
            <QuestionIcon size={14} />
          </animated.div>
        </div>

        <Tooltip message={`${expandTable ? 'Collapse' : 'Expand'} table`} hold>
          <animated.div
            className={classnames('toggle', 'expand-table-toggle', {
              'is-highlighted': expandTable,
            })}
            style={trail[4]}
            onClick={setExpandTable.bind(this, !expandTable)}
          >
            <FoldDownIcon size={16} />
          </animated.div>
        </Tooltip>
      </div>

      {transition(
        (style, item) =>
          item && (
            <animated.div className="table-helper" {...{style}}>
              <div className="helper-top">
                <div className="helper-left">
                  <div className="info-item">
                    <div>
                      <OrganizationIcon size={14} />
                    </div>
                    <p>{t('Toggle between States/Districts')}</p>
                  </div>

                  <div className="info-item">
                    <div>
                      <PeopleIcon size={16} />
                    </div>
                    <p>{t('Per Lakh People')}</p>
                  </div>

                  <div className="info-item">
                    <div>
                      <PulseIcon size={16} />
                    </div>
                    <p>{t('Last 7 day values')}</p>
                  </div>

                  <div className="info-item sort">
                    <div>
                      <SortDescIcon size={14} />
                    </div>
                    <p>{t('Sorted by Descending')}</p>
                  </div>

                  <div className="info-item sort">
                    <div>
                      <SortAscIcon size={14} />
                    </div>
                    <p>{t('Sorted by Ascending')}</p>
                  </div>

                  <div className="info-item sort">
                    <TableDeltaHelper />
                  </div>

                  <div className="info-item notes">
                    <div>
                      <InfoIcon size={15} />
                    </div>
                    <p>{t('Notes')}</p>
                  </div>
                </div>

                <div className="helper-right">
                  <div className="info-item">
                    <p>{t('Units')}</p>
                  </div>
                  {Object.entries({'1K': 3, '1L': 5, '1Cr': 7}).map(
                    ([abbr, exp]) => (
                      <div className="info-item abbr" key={abbr}>
                        <h5>{abbr}</h5>
                        <p>
                          10
                          <sup>{exp}</sup>
                        </p>
                      </div>
                    )
                  )}
                </div>
              </div>

              <h5 className="text">
                {t('Compiled from State Govt. numbers')},{' '}
                <Link to="/about">{t('know more')}!</Link>
              </h5>
            </animated.div>
          )
      )}

      <div className="table-container" ref={tableContainerRef}>
        <div
          className="table fadeInUp"
          style={{
            gridTemplateColumns: `repeat(${tableStatistics.length + 1}, auto)`,
          }}
        >
          <div className="row heading">
            <div
              className="cell heading"
              onClick={handleSortClick.bind(this, 'regionName')}
            >
              <div>{t(!showDistricts ? 'State/UT' : 'District')}</div>
              {sortData.sortColumn === 'regionName' && (
                <div className={'sort-icon'}>
                  {sortData.isAscending ? (
                    <SortAscIcon size={12} />
                  ) : (
                    <SortDescIcon size={12} />
                  )}
                </div>
              )}
            </div>

            {tableStatistics.map((statistic) => (
              <HeaderCell
                key={statistic}
                {...{
                  statistic,
                  sortData,
                  setSortData,
                }}
                handleSort={handleSortClick.bind(this, statistic)}
              />
            ))}
          </div>

          {!showDistricts &&
            Object.keys(states)
              .filter(
                (stateCode) =>
                  stateCode !== 'TT' &&
                  !(stateCode === UNASSIGNED_STATE_CODE && isPerLakh)
              )
              .sort((a, b) => sortingFunction(a, b))
              .map((stateCode) => {
                return (
                  <Row
                    key={stateCode}
                    data={states[stateCode]}
                    noDistrictData={noDistrictDataStates[stateCode]}
                    {...{
                      stateCode,
                      regionHighlighted,
                      setRegionHighlighted,
                      expandTable,
                      tableStatistics,
                      getTableStatistic,
                      tableWidth,
                    }}
                  />
                );
              })}

          {showDistricts && !districts && <TableLoader />}

          {showDistricts &&
            districts &&
            Object.keys(districts)
              .sort((a, b) => sortingFunction(a, b))
              .slice(
                page * DISTRICT_TABLE_COUNT,
                (page + 1) * DISTRICT_TABLE_COUNT
              )
              .map((districtKey) => {
                const noDistrictData =
                  noDistrictDataStates[districts[districtKey].stateCode];
                return (
                  <Row
                    key={districtKey}
                    data={districts[districtKey]}
                    districtName={districts[districtKey].districtName}
                    {...{
                      regionHighlighted,
                      setRegionHighlighted,
                      expandTable,
                      tableStatistics,
                      getTableStatistic,
                      noDistrictData,
                    }}
                  />
                );
              })}

          <Row
            key={'TT'}
            data={states['TT']}
            stateCode={'TT'}
            {...{
              regionHighlighted,
              setRegionHighlighted,
              expandTable,
              tableStatistics,
              getTableStatistic,
            }}
          />
        </div>
      </div>
      {showDistricts && (
        <div className="paginate">
          <div
            className={classnames('left', {disabled: page === 0})}
            onClick={handlePageClick.bind(this, -2)}
          >
            <ChevronsLeft size={16} />
          </div>
          <div
            className={classnames('left', {disabled: page === 0})}
            onClick={handlePageClick.bind(this, -1)}
          >
            <ChevronLeft size={16} />
          </div>
          <h5>{`${page + 1} / ${numPages}`}</h5>
          <div
            className={classnames('right', {disabled: page === numPages - 1})}
            onClick={handlePageClick.bind(this, 1)}
          >
            <ChevronRight size={16} />
          </div>
          <div
            className={classnames('right', {disabled: page === numPages - 1})}
            onClick={handlePageClick.bind(this, 2)}
          >
            <ChevronsRight size={16} />
          </div>
        </div>
      )}
    </div>
  );
}
Example #9
Source File: Timeseries.js    From covid19india-react with MIT License 4 votes vote down vote up
function Timeseries({
  statistics,
  timeseries,
  dates,
  endDate,
  chartType,
  isUniform,
  isLog,
  isMovingAverage,
  noRegionHighlightedDistrictData,
}) {
  const {t} = useTranslation();
  const refs = useRef([]);
  const [wrapperRef, {width, height}] = useMeasure();

  const [highlightedDate, setHighlightedDate] = useState(
    dates[dates.length - 1]
  );
  useEffect(() => {
    setHighlightedDate(dates[dates.length - 1]);
  }, [dates]);

  const getTimeseriesStatistic = useCallback(
    (date, statistic, movingAverage = isMovingAverage) => {
      return getStatistic(timeseries?.[date], chartType, statistic, {
        movingAverage,
      });
    },
    [chartType, isMovingAverage, timeseries]
  );

  const condenseChart = useMemo(() => {
    const T = dates.length;
    const days = differenceInDays(
      parseIndiaDate(dates[T - 1]),
      parseIndiaDate(dates[0])
    );
    // Chart extremes
    const chartRight = width - margin.right;
    // Bar widths
    const axisWidth = Math.max(0, chartRight - margin.left) / (1.25 * days);
    return axisWidth < 4;
  }, [width, dates]);

  const xScale = useMemo(() => {
    const T = dates.length;
    const chartRight = width - margin.right;

    return scaleTime()
      .clamp(true)
      .domain([
        parseIndiaDate(dates[0] || endDate),
        parseIndiaDate(dates[T - 1] || endDate),
      ])
      .range([margin.left, chartRight]);
  }, [width, endDate, dates]);

  const yScales = useMemo(() => {
    const chartBottom = height - margin.bottom;

    const addScaleBuffer = (scale, log = false) => {
      const domain = scale.domain();
      if (log) {
        scale.domain([
          domain[0],
          domain[0] * Math.pow(domain[1] / domain[0], 1 / yScaleShrinkFactor),
        ]);
      } else {
        scale.domain([
          domain[0],
          domain[0] + (domain[1] - domain[0]) / yScaleShrinkFactor,
        ]);
      }
      return scale;
    };

    const [uniformScaleMin, uniformScaleMax] = extent(
      dates.reduce((res, date) => {
        res.push(
          ...PRIMARY_STATISTICS.map((statistic) =>
            getTimeseriesStatistic(date, statistic)
          )
        );
        return res;
      }, [])
    );

    const yScaleUniformLinear = addScaleBuffer(
      scaleLinear()
        .clamp(true)
        .domain([Math.min(0, uniformScaleMin), Math.max(1, uniformScaleMax)])
        .range([chartBottom, margin.top])
        .nice(4)
    );

    const yScaleUniformLog = addScaleBuffer(
      scaleLog()
        .clamp(true)
        .domain([1, Math.max(10, uniformScaleMax)])
        .range([chartBottom, margin.top])
        .nice(4),
      true
    );

    return statistics.map((statistic) => {
      if (isUniform) {
        if (
          chartType === 'total' &&
          isLog &&
          PRIMARY_STATISTICS.includes(statistic)
        ) {
          return yScaleUniformLog;
        } else if (PRIMARY_STATISTICS.includes(statistic)) {
          return yScaleUniformLinear;
        }
      }

      const [scaleMin, scaleMax] = extent(dates, (date) =>
        getTimeseriesStatistic(date, statistic)
      );

      if (chartType === 'total' && isLog) {
        return addScaleBuffer(
          scaleLog()
            .clamp(true)
            .domain([1, Math.max(10, scaleMax)])
            .range([chartBottom, margin.top])
            .nice(4),
          true
        );
      } else {
        return addScaleBuffer(
          scaleLinear()
            .clamp(true)
            .domain([
              Math.min(0, scaleMin),
              STATISTIC_CONFIGS[statistic].format === '%'
                ? Math.min(100, Math.max(1, scaleMax))
                : Math.max(1, scaleMax),
            ])
            .range([chartBottom, margin.top])
            .nice(4)
        );
      }
    });
  }, [
    height,
    chartType,
    isUniform,
    isLog,
    statistics,
    dates,
    getTimeseriesStatistic,
  ]);

  useEffect(() => {
    if (!width || !height) return;

    const T = dates.length;
    // Chart extremes
    const chartRight = width - margin.right;
    const chartBottom = height - margin.bottom;

    const isDiscrete = chartType === 'delta' && !isMovingAverage;

    const xAxis = (g) =>
      g
        .attr('class', 'x-axis')
        .call(axisBottom(xScale).ticks(numTicksX(width)));

    const xAxis2 = (g, yScale) => {
      g.attr('class', 'x-axis2')
        .call(axisBottom(xScale).tickValues([]).tickSize(0))
        .select('.domain')
        .style('transform', `translate3d(0, ${yScale(0)}px, 0)`);

      if (yScale(0) !== chartBottom) g.select('.domain').attr('opacity', 0.4);
      else g.select('.domain').attr('opacity', 0);
    };

    const yAxis = (g, yScale, format) =>
      g.attr('class', 'y-axis').call(
        axisRight(yScale)
          .ticks(4)
          .tickFormat((num) => formatNumber(num, format))
          .tickPadding(4)
      );

    function mousemove(event) {
      const xm = pointer(event)[0];
      const date = xScale.invert(xm);
      if (!isNaN(date)) {
        const bisectDate = bisector((date) => parseIndiaDate(date)).left;
        const index = bisectDate(dates, date, 1);
        const dateLeft = dates[index - 1];
        const dateRight = dates[index];
        setHighlightedDate(
          date - parseIndiaDate(dateLeft) < parseIndiaDate(dateRight) - date
            ? dateLeft
            : dateRight
        );
      }
    }

    function mouseout(event) {
      setHighlightedDate(dates[T - 1]);
    }

    /* Begin drawing charts */
    statistics.forEach((statistic, i) => {
      const ref = refs.current[i];
      const svg = select(ref);
      const t = svg.transition().duration(D3_TRANSITION_DURATION);

      const yScale = yScales[i];
      const statisticConfig = STATISTIC_CONFIGS[statistic];
      const format =
        STATISTIC_CONFIGS[statistic].format === '%' ? '%' : 'short';
      const isNonLinear = !!STATISTIC_CONFIGS[statistic]?.nonLinear;

      /* X axis */
      svg
        .select('.x-axis')
        .style('transform', `translate3d(0, ${chartBottom}px, 0)`)
        .transition(t)
        .call(xAxis);

      svg.select('.x-axis2').transition(t).call(xAxis2, yScale);

      /* Y axis */
      svg
        .select('.y-axis')
        .style('transform', `translate3d(${chartRight}px, 0, 0)`)
        .transition(t)
        .call(yAxis, yScale, format);

      /* Path dots */
      svg
        .selectAll('circle.normal')
        .data(condenseChart ? [] : dates, (date) => date)
        .join((enter) =>
          enter
            .append('circle')
            .attr('class', 'normal')
            .attr('fill', statisticConfig?.color)
            .attr('stroke', statisticConfig?.color)
            .attr('cx', (date) => xScale(parseIndiaDate(date)))
            .attr('cy', (date) =>
              yScale(isDiscrete ? 0 : getTimeseriesStatistic(date, statistic))
            )
            .attr('r', 2)
        )
        .transition(t)
        .attr('cx', (date) => xScale(parseIndiaDate(date)))
        .attr('cy', (date) => yScale(getTimeseriesStatistic(date, statistic)));

      const areaPath = (dates, allZero = false) =>
        area()
          .curve(curveStep)
          .x((date) => xScale(parseIndiaDate(date)))
          .y0(yScale(0))
          .y1(
            allZero
              ? yScale(0)
              : (date) => yScale(getTimeseriesStatistic(date, statistic, false))
          )(dates);

      svg
        .selectAll('.trend-area')
        .data(
          T && chartType === 'delta' && !isNonLinear && condenseChart
            ? [dates]
            : []
        )
        .join(
          (enter) =>
            enter
              .append('path')
              .attr('class', 'trend-area')
              .call((enter) =>
                enter
                  .attr('d', (dates) => areaPath(dates, true))
                  .transition(t)
                  .attr('d', areaPath)
              ),
          (update) =>
            update.call((update) =>
              update.transition(t).attrTween('d', function (dates) {
                const previous = select(this).attr('d');
                const current = areaPath(dates);
                return interpolatePath(previous, current);
              })
            ),
          (exit) =>
            exit.call((exit) =>
              exit
                .transition(t)
                .attr('d', (dates) => areaPath(dates, true))
                .remove()
            )
        )
        .transition(t)
        .attr('opacity', isMovingAverage ? 0.3 : 1);

      svg
        .selectAll('.stem')
        .data(
          T && chartType === 'delta' && !isNonLinear && !condenseChart
            ? dates
            : [],
          (date) => date
        )
        .join(
          (enter) =>
            enter
              .append('line')
              .attr('class', 'stem')
              .attr('stroke-width', 4)
              .attr('x1', (date) => xScale(parseIndiaDate(date)))
              .attr('y1', yScale(0))
              .attr('x2', (date) => xScale(parseIndiaDate(date)))
              .attr('y2', yScale(0)),
          (update) => update,
          (exit) =>
            exit.call((exit) =>
              exit
                .transition(t)
                .attr('x1', (date) => xScale(parseIndiaDate(date)))
                .attr('x2', (date) => xScale(parseIndiaDate(date)))
                .attr('y1', yScale(0))
                .attr('y2', yScale(0))
                .remove()
            )
        )
        .transition(t)
        .attr('x1', (date) => xScale(parseIndiaDate(date)))
        .attr('y1', yScale(0))
        .attr('x2', (date) => xScale(parseIndiaDate(date)))
        .attr('y2', (date) =>
          yScale(getTimeseriesStatistic(date, statistic, false))
        )
        .attr('opacity', isMovingAverage ? 0.2 : 1);

      const linePath = (
        movingAverage = isMovingAverage,
        curve = curveMonotoneX
      ) =>
        line()
          .curve(curve)
          .x((date) => xScale(parseIndiaDate(date)))
          .y((date) =>
            yScale(getTimeseriesStatistic(date, statistic, movingAverage))
          );

      svg
        .select('.trend')
        .selectAll('path')
        .data(
          T && (chartType === 'total' || isNonLinear || isMovingAverage)
            ? [dates]
            : []
        )
        .join(
          (enter) =>
            enter
              .append('path')
              .attr('class', 'trend')
              .attr('fill', 'none')
              .attr('stroke-width', 4)
              .attr('d', linePath())
              .call((enter) => enter.transition(t).attr('opacity', 1)),
          (update) =>
            update.call((update) =>
              update
                .transition(t)
                .attrTween('d', function (date) {
                  const previous = select(this).attr('d');
                  const current = linePath()(date);
                  return interpolatePath(previous, current);
                })
                .attr('opacity', 1)
            ),
          (exit) =>
            exit.call((exit) => exit.transition(t).attr('opacity', 0).remove())
        )
        .attr('stroke', statisticConfig?.color + (condenseChart ? '99' : '50'));

      svg
        .select('.trend-background')
        .selectAll('path')
        .data(
          T && chartType === 'delta' && isNonLinear && isMovingAverage
            ? [dates]
            : []
        )
        .join(
          (enter) =>
            enter
              .append('path')
              .attr('class', 'trend-background')
              .attr('fill', 'none')
              .attr('stroke-width', 4)
              .attr('d', linePath(false, curveLinear))
              .call((enter) => enter.transition(t).attr('opacity', 0.2)),
          (update) =>
            update.call((update) =>
              update
                .transition(t)
                .attrTween('d', function (date) {
                  const previous = select(this).attr('d');
                  const current = linePath(false, curveLinear)(date);
                  return interpolatePath(previous, current);
                })
                .attr('opacity', 0.2)
            ),
          (exit) =>
            exit.call((exit) => exit.transition(t).attr('opacity', 0).remove())
        )
        .attr('stroke', statisticConfig?.color + (condenseChart ? '99' : '50'));

      svg.selectAll('*').attr('pointer-events', 'none');
      svg
        .on('mousemove', mousemove)
        .on('touchmove', (event) => mousemove(event.touches[0]))
        .on('mouseout touchend', mouseout);
    });
  }, [
    width,
    height,
    chartType,
    isMovingAverage,
    condenseChart,
    xScale,
    yScales,
    statistics,
    getTimeseriesStatistic,
    dates,
  ]);

  useEffect(() => {
    statistics.forEach((statistic, i) => {
      const ref = refs.current[i];
      const svg = select(ref);
      const statisticConfig = STATISTIC_CONFIGS[statistic];
      const yScale = yScales[i];
      const t = svg.transition().duration(D3_TRANSITION_DURATION);

      svg
        .selectAll('circle.condensed')
        .data(
          condenseChart && highlightedDate ? [highlightedDate] : [],
          (date) => date
        )
        .join((enter) =>
          enter
            .append('circle')
            .attr('class', 'condensed')
            .attr('fill', statisticConfig?.color)
            .attr('stroke', statisticConfig?.color)
            .attr('pointer-events', 'none')
            .attr('cx', (date) => xScale(parseIndiaDate(date)))
            .attr('cy', (date) =>
              yScale(getTimeseriesStatistic(date, statistic))
            )
            .attr('r', 4)
        )
        .transition(t)
        .attr('cx', (date) => xScale(parseIndiaDate(date)))
        .attr('cy', (date) => yScale(getTimeseriesStatistic(date, statistic)));

      if (!condenseChart) {
        svg
          .selectAll('circle')
          .attr('r', (date) => (date === highlightedDate ? 4 : 2));
      }
    });
  }, [
    condenseChart,
    highlightedDate,
    xScale,
    yScales,
    statistics,
    getTimeseriesStatistic,
  ]);

  const getTimeseriesStatisticDelta = useCallback(
    (statistic) => {
      if (!highlightedDate) return;

      const currCount = getTimeseriesStatistic(highlightedDate, statistic);
      if (STATISTIC_CONFIGS[statistic]?.hideZero && currCount === 0) return;

      const prevDate =
        dates[dates.findIndex((date) => date === highlightedDate) - 1];

      const prevCount = getTimeseriesStatistic(prevDate, statistic);
      return currCount - prevCount;
    },
    [dates, highlightedDate, getTimeseriesStatistic]
  );

  const trail = useMemo(
    () =>
      statistics.map((statistic, index) => ({
        animationDelay: `${index * 250}ms`,
      })),
    [statistics]
  );

  return (
    <div className="Timeseries">
      {statistics.map((statistic, index) => {
        const total = getTimeseriesStatistic(highlightedDate, statistic);
        const delta = getTimeseriesStatisticDelta(statistic, index);
        const statisticConfig = STATISTIC_CONFIGS[statistic];
        return (
          <div
            key={statistic}
            className={classnames('svg-parent fadeInUp', `is-${statistic}`)}
            style={trail[index]}
            ref={index === 0 ? wrapperRef : null}
          >
            {highlightedDate && (
              <div className={classnames('stats', `is-${statistic}`)}>
                <h5 className="title">
                  {t(capitalize(statisticConfig.displayName))}
                </h5>
                <h5>{formatDate(highlightedDate, 'dd MMMM')}</h5>
                <div className="stats-bottom">
                  <h2>
                    {!noRegionHighlightedDistrictData ||
                    !statisticConfig?.hasPrimary
                      ? formatNumber(
                          total,
                          statisticConfig.format !== 'short'
                            ? statisticConfig.format
                            : 'long',
                          statistic
                        )
                      : '-'}
                  </h2>
                  <h6>
                    {!noRegionHighlightedDistrictData ||
                    !statisticConfig?.hasPrimary
                      ? `${delta > 0 ? '+' : ''}${formatNumber(
                          delta,
                          statisticConfig.format !== 'short'
                            ? statisticConfig.format
                            : 'long',
                          statistic
                        )}`
                      : ''}
                  </h6>
                </div>
              </div>
            )}
            <svg
              ref={(element) => {
                refs.current[index] = element;
              }}
              preserveAspectRatio="xMidYMid meet"
            >
              <g className="x-axis" />
              <g className="x-axis2" />
              <g className="y-axis" />
              <g className="trend-background" />
              <g className="trend" />
            </svg>
          </div>
        );
      })}
    </div>
  );
}
Example #10
Source File: TimeseriesBrush.js    From covid19india-react with MIT License 4 votes vote down vote up
function TimeseriesBrush({
  timeseries,
  dates,
  currentBrushSelection,
  endDate,
  lookback,
  setBrushSelectionEnd,
  setLookback,
  animationIndex,
}) {
  const chartRef = useRef();
  const [wrapperRef, {width, height}] = useMeasure();

  const endDateMin =
    lookback !== null
      ? min([
          formatISO(addDays(parseIndiaDate(dates[0]), lookback), {
            representation: 'date',
          }),
          endDate,
        ])
      : endDate;

  const xScale = useMemo(() => {
    const T = dates.length;

    // Chart extremes
    const chartRight = width - margin.right;

    return scaleTime()
      .clamp(true)
      .domain([
        parseIndiaDate(dates[0] || endDate),
        parseIndiaDate(dates[T - 1] || endDate),
      ])
      .range([margin.left, chartRight]);
  }, [width, endDate, dates]);

  useEffect(() => {
    if (!width || !height) return;

    // Chart extremes
    const chartBottom = height - margin.bottom;

    const xAxis = (g) =>
      g
        .attr('class', 'x-axis')
        .call(axisBottom(xScale).ticks(numTicksX(width)));

    // Switched to daily confirmed instead of cumulative ARD
    const timeseriesStacked = stack()
      .keys(BRUSH_STATISTICS)
      .value((date, statistic) =>
        Math.max(0, getStatistic(timeseries[date], 'delta', statistic))
      )(dates);

    const yScale = scaleLinear()
      .clamp(true)
      .domain([
        0,
        max(
          timeseriesStacked[timeseriesStacked.length - 1],
          ([, y1]) => yBufferTop * y1
        ),
      ])
      .range([chartBottom, margin.top]);

    const svg = select(chartRef.current);

    const t = svg.transition().duration(D3_TRANSITION_DURATION);

    svg
      .select('.x-axis')
      .attr('pointer-events', 'none')
      .style('transform', `translate3d(0, ${chartBottom}px, 0)`)
      .transition(t)
      .call(xAxis);

    const areaPath = area()
      .curve(curveMonotoneX)
      .x((d) => xScale(parseIndiaDate(d.data)))
      .y0((d) => yScale(d[0]))
      .y1((d) => yScale(d[1]));

    svg
      .select('.trend-areas')
      .selectAll('.trend-area')
      .data(timeseriesStacked)
      .join(
        (enter) =>
          enter
            .append('path')
            .attr('class', 'trend-area')
            .attr('fill', ({key}) => STATISTIC_CONFIGS[key].color)
            .attr('fill-opacity', 0.4)
            .attr('stroke', ({key}) => STATISTIC_CONFIGS[key].color)
            .attr('d', areaPath)
            .attr('pointer-events', 'none'),
        (update) =>
          update
            .transition(t)
            .attrTween('d', function (date) {
              const previous = select(this).attr('d');
              const current = areaPath(date);
              return interpolatePath(previous, current);
            })
            .selection()
      );
  }, [dates, width, height, xScale, timeseries]);

  const defaultSelection = currentBrushSelection.map((date) =>
    xScale(parseIndiaDate(date))
  );

  const brush = useMemo(() => {
    if (!width || !height) return;
    // Chart extremes
    const chartRight = width - margin.right;
    const chartBottom = height - margin.bottom;

    const brush = brushX()
      .extent([
        [margin.left, margin.top],
        [chartRight, chartBottom],
      ])
      .handleSize(20);
    return brush;
  }, [width, height]);

  const brushed = useCallback(
    ({sourceEvent, selection}) => {
      if (!sourceEvent) return;
      const [brushStartDate, brushEndDate] = selection.map(xScale.invert);

      ReactDOM.unstable_batchedUpdates(() => {
        setBrushSelectionEnd(formatISO(brushEndDate, {representation: 'date'}));
        setLookback(differenceInDays(brushEndDate, brushStartDate));
      });
    },
    [xScale, setBrushSelectionEnd, setLookback]
  );

  const beforebrushstarted = useCallback(
    (event) => {
      const svg = select(chartRef.current);
      const selection = brushSelection(svg.select('.brush').node());

      if (!selection) return;

      const dx = selection[1] - selection[0];
      const [[cx]] = pointers(event);
      const [x0, x1] = [cx - dx / 2, cx + dx / 2];
      const [X0, X1] = xScale.range();
      svg
        .select('.brush')
        .call(
          brush.move,
          x1 > X1 ? [X1 - dx, X1] : x0 < X0 ? [X0, X0 + dx] : [x0, x1]
        );
    },
    [brush, xScale]
  );

  const brushended = useCallback(
    ({sourceEvent, selection}) => {
      if (!sourceEvent || !selection) return;
      const domain = selection
        .map(xScale.invert)
        .map((date) => formatISO(date, {representation: 'date'}));

      const svg = select(chartRef.current);
      svg
        .select('.brush')
        .call(
          brush.move,
          domain.map((date) => xScale(parseIndiaDate(date)))
        )
        .call((g) => g.select('.overlay').attr('cursor', 'pointer'));
    },
    [brush, xScale]
  );

  useEffect(() => {
    if (!brush) return;
    brush.on('start brush', brushed).on('end', brushended);
    const svg = select(chartRef.current);
    svg
      .select('.brush')
      .call(brush)
      .call((g) =>
        g
          .select('.overlay')
          .attr('cursor', 'pointer')
          .datum({type: 'selection'})
          .on('mousedown touchstart', beforebrushstarted)
      );
  }, [brush, brushed, brushended, beforebrushstarted]);

  useEffect(() => {
    if (!brush) return;
    const svg = select(chartRef.current);
    svg.select('.brush').call(brush.move, defaultSelection);
  }, [brush, defaultSelection]);

  const handleWheel = (event) => {
    if (event.deltaX) {
      setBrushSelectionEnd(
        max([
          endDateMin,
          dates[
            Math.max(
              0,
              Math.min(
                dates.length - 1,
                dates.indexOf(currentBrushSelection[1]) +
                  Math.sign(event.deltaX) * brushWheelDelta
              )
            )
          ],
        ])
      );
    }
  };

  return (
    <div className="Timeseries">
      <div
        className={classnames('svg-parent is-brush fadeInUp')}
        ref={wrapperRef}
        onWheel={handleWheel}
        style={{animationDelay: `${animationIndex * 250}ms`}}
      >
        <svg ref={chartRef} preserveAspectRatio="xMidYMid meet">
          <defs>
            <clipPath id="clipPath">
              <rect
                x={0}
                y={`${margin.top}`}
                width={width}
                height={`${Math.max(0, height - margin.bottom)}`}
              />
            </clipPath>
            <mask id="mask">
              <rect
                x={0}
                y={`${margin.top}`}
                width={width}
                height={`${Math.max(0, height - margin.bottom)}`}
                fill="hsl(0, 0%, 40%)"
              />
              <use href="#selection" fill="white" />
            </mask>
          </defs>

          <g className="brush" clipPath="url(#clipPath)">
            <g mask="url(#mask)">
              <rect className="overlay" />
              <g className="trend-areas" />
              <rect className="selection" id="selection" />
            </g>
          </g>
          <g className="x-axis" />
        </svg>
      </div>
    </div>
  );
}