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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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>
);
}