-
Notifications
You must be signed in to change notification settings - Fork 0
Timeline Widget
David Jin edited this page Jun 30, 2023
·
2 revisions
Visualization type | Description |
---|---|
timeline | This is a timeline visualization of the publication dates for volumes in a workset. |
/src/components/widgets/PublicationTimeLineChart/index.tsx
/src/components/CustomSlider.tsx
The project is currently using dummy metadata under /src/data/publicationDateTimeLineMetaData.ts
to apply Data Filters and show the number of htid
on Publication Timeline Chart.
You will need to change the data there to update the metadata or change the API endpoint of getTimeLineData
in /src/services/index.ts
to use the API data.
The Timeline chart consists of two g
components - one for axes and one for bars.
<svg width={width} height={height}>
<g width={boundsWidth} height={boundsHeight} transform={`translate(${[MARGIN.left, MARGIN.top].join(',')})`}>
{allRects}
</g>
<g width={boundsWidth} height={boundsHeight} ref={axesRef} transform={`translate(${[MARGIN.left, MARGIN.top].join(',')})`} />
</svg>
The chart data needs to be updated when date changes on Slider, so I used useEffect
and React Hook to get updated data.
const [dateRange, setDateRange] = useState<number[]>([]);
const chartDataHistogram = useMemo(() => {
return Object.keys(modifiedDataHistogram)
.filter((item) => Number(item) >= dateRange[0] && Number(item) <= dateRange[1])
.map((item) => ({ date: item, value: modifiedDataHistogram[item] }));
}, [modifiedDataHistogram, dateRange]);
And update the axes and bars on the Chart.
Axes:
//x-axis scale for Histogram
const xScaleHistogram = d3.scaleLinear().domain([dateRange[0], dateRange[1]]).range([0, boundsWidth]);
//y-axis scale for Histogram
const yScaleHistogram = d3
.scaleLinear()
.domain([0, Math.max(...Object.values(modifiedDataHistogram).map((item: any) => Number(item)))])
.range([boundsHeight, 0]);
useEffect(() => {
const svgElement = d3.select(axesRef.current);
svgElement.selectAll('*').remove();
const xAxisGenerator = d3.axisBottom(xScaleHistogram);
svgElement
.append('g')
.attr('transform', 'translate(0,' + boundsHeight + ')')
.call(xAxisGenerator);
const yAxisGenerator = d3.axisLeft(yScaleHistogram);
svgElement.append('g').call(yAxisGenerator);
}, [xScaleHistogram, yScaleHistogram, boundsHeight]);
Bars:
const allRects = chartDataHistogram.map((bucket, i) => {
return (
<rect
key={i}
fill="#6689c6"
x={xScaleHistogram(Number(bucket.date)) + BUCKET_PADDING / 2}
width={dateRange[1] - dateRange[0] == 0 ? 10 : boundsWidth / (dateRange[1] - dateRange[0]) - BUCKET_PADDING}
y={yScaleHistogram(bucket.value)}
height={boundsHeight - yScaleHistogram(bucket.value)}
/>
);
});
// src/components/widgets/PublicationTimeLineChart/index.tsx
import * as d3 from 'd3';
import { useEffect, useState, useMemo, useRef } from 'react';
import { Box, Typography, useTheme } from '@mui/material';
import { ITimelineChart } from 'types/chart';
import CustomSlider from 'components/CustomSlider';
import useResizeObserver from 'hooks/useResizeObserver';
import { useSelector } from 'store';
import MainCard from '../../MainCard';
const MARGIN = { top: 20, right: 20, bottom: 20, left: 25 };
const BUCKET_PADDING = 1;
export const PublicationTimeLineChart = () => {
const theme = useTheme();
const axesRef = useRef(null);
const { timelineData } = useSelector((state) => state.dashboard);
const chartWrapper = useRef();
const dimensions = useResizeObserver(chartWrapper);
//group by pubDate timelineData
const modifiedDataHistogram = useMemo(() => {
return timelineData.reduce((prev: any, curr: ITimelineChart) => {
const pubDate = curr.metadata.pubDate;
if (prev[pubDate]) return { ...prev, [pubDate]: prev[pubDate] + 1 };
else return { ...prev, [pubDate]: 1 };
}, {});
}, [timelineData]);
const [dateRange, setDateRange] = useState<number[]>([]);
const chartDataHistogram = useMemo(() => {
return Object.keys(modifiedDataHistogram)
.filter((item) => Number(item) >= dateRange[0] && Number(item) <= dateRange[1])
.map((item) => ({ date: item, value: modifiedDataHistogram[item] }));
}, [modifiedDataHistogram, dateRange]);
const xAxisLabels = useMemo(() => {
return Object.keys(modifiedDataHistogram)
.filter((item) => Number(item) >= dateRange[0] && Number(item) <= dateRange[1])
.map((item) => Number(item));
}, [modifiedDataHistogram, dateRange]);
const minDate = useMemo(() => {
return Object.keys(modifiedDataHistogram).length ? Math.min(...Object.keys(modifiedDataHistogram).map((item: any) => Number(item))) : 0;
}, [modifiedDataHistogram]);
const maxDate = useMemo(() => {
return Object.keys(modifiedDataHistogram).length ? Math.max(...Object.keys(modifiedDataHistogram).map((item: any) => Number(item))) : 0;
}, [modifiedDataHistogram]);
useEffect(() => {
setDateRange([minDate, maxDate]);
}, [minDate, maxDate]);
//Histogram properties
let width = dimensions?.width || 500;
let height = width / 2;
const boundsWidth = width - MARGIN.right - MARGIN.left;
const boundsHeight = height - MARGIN.top - MARGIN.bottom;
//x-axis scale for Histogram
const xScaleHistogram = d3.scaleLinear().domain([dateRange[0], dateRange[1]]).range([0, boundsWidth]);
//y-axis scale for Histogram
const yScaleHistogram = d3
.scaleLinear()
.domain([0, Math.max(...Object.values(modifiedDataHistogram).map((item: any) => Number(item)))])
.range([boundsHeight, 0]);
useEffect(() => {
const svgElement = d3.select(axesRef.current);
svgElement.selectAll('*').remove();
const xAxisGenerator = d3.axisBottom(xScaleHistogram);
svgElement
.append('g')
.attr('transform', 'translate(0,' + boundsHeight + ')')
.call(xAxisGenerator);
const yAxisGenerator = d3.axisLeft(yScaleHistogram);
svgElement.append('g').call(yAxisGenerator);
}, [xScaleHistogram, yScaleHistogram, boundsHeight]);
const allRects = chartDataHistogram.map((bucket, i) => {
return (
<rect
key={i}
fill="#6689c6"
x={xScaleHistogram(Number(bucket.date)) + BUCKET_PADDING / 2}
width={dateRange[1] - dateRange[0] == 0 ? 10 : boundsWidth / (dateRange[1] - dateRange[0]) - BUCKET_PADDING}
y={yScaleHistogram(bucket.value)}
height={boundsHeight - yScaleHistogram(bucket.value)}
/>
);
});
const handleSliderChange = (event: any) => {
setDateRange(event.target.value);
};
return (
<MainCard
content={false}
sx={{
mt: 1.5,
padding: theme.spacing(4),
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
position: 'relative'
}}
>
<Typography variant="h3" sx={{ color: '#1e98d7' }}>
Publication Date Timeline
</Typography>
<Box sx={{ width: '100%' }} ref={chartWrapper}>
<svg width={width} height={height}>
<g width={boundsWidth} height={boundsHeight} transform={`translate(${[MARGIN.left, MARGIN.top].join(',')})`}>
{allRects}
</g>
<g width={boundsWidth} height={boundsHeight} ref={axesRef} transform={`translate(${[MARGIN.left, MARGIN.top].join(',')})`} />
</svg>
<CustomSlider value={dateRange} minValue={minDate} maxValue={maxDate} step={10} handleSliderChange={handleSliderChange} />
</Box>
</MainCard>
);
};