2021-05-11 15:41:42 -07:00
|
|
|
import React, { useMemo } from 'react';
|
2023-11-28 00:45:21 +08:00
|
|
|
import styled from 'styled-components';
|
|
|
|
import { AxisScaleOutput } from '@visx/axis';
|
|
|
|
import { Axis, LineSeries, XYChart, Tooltip, GlyphSeries } from '@visx/xychart';
|
|
|
|
import { curveMonotoneX } from '@visx/curve';
|
|
|
|
import { ScaleConfig, scaleOrdinal } from '@visx/scale';
|
2021-07-30 17:41:03 -07:00
|
|
|
import { TimeSeriesChart as TimeSeriesChartType, NumericDataPoint, NamedLine } from '../../../types.generated';
|
2021-05-11 15:41:42 -07:00
|
|
|
import { lineColors } from './lineColors';
|
|
|
|
import Legend from './Legend';
|
2023-02-21 03:47:31 +05:30
|
|
|
import { addInterval } from '../../shared/time/timeUtils';
|
2022-02-15 16:25:01 -08:00
|
|
|
import { formatNumber } from '../../shared/formatNumber';
|
2021-05-11 15:41:42 -07:00
|
|
|
|
2023-07-06 10:19:40 -07:00
|
|
|
type AxisConfig = {
|
|
|
|
formatter: (tick: number) => string;
|
|
|
|
};
|
|
|
|
|
2021-05-11 15:41:42 -07:00
|
|
|
type Props = {
|
|
|
|
chartData: TimeSeriesChartType;
|
|
|
|
width: number;
|
|
|
|
height: number;
|
2021-07-30 17:41:03 -07:00
|
|
|
hideLegend?: boolean;
|
|
|
|
style?: {
|
|
|
|
axisColor?: string;
|
|
|
|
axisWidth?: number;
|
|
|
|
lineColor?: string;
|
|
|
|
lineWidth?: string;
|
|
|
|
crossHairLineColor?: string;
|
|
|
|
};
|
|
|
|
insertBlankPoints?: boolean;
|
2023-11-28 00:45:21 +08:00
|
|
|
yScale?: ScaleConfig<AxisScaleOutput, any, any>;
|
2023-07-06 10:19:40 -07:00
|
|
|
yAxis?: AxisConfig;
|
2021-05-11 15:41:42 -07:00
|
|
|
};
|
|
|
|
|
2023-11-28 00:45:21 +08:00
|
|
|
const StyledTooltip = styled(Tooltip)`
|
|
|
|
font-family: inherit !important;
|
|
|
|
font-weight: 400 !important;
|
|
|
|
`;
|
|
|
|
|
2023-07-06 10:19:40 -07:00
|
|
|
const MARGIN = {
|
|
|
|
TOP: 40,
|
|
|
|
RIGHT: 45,
|
|
|
|
BOTTOM: 40,
|
|
|
|
LEFT: 40,
|
|
|
|
};
|
2021-05-11 15:41:42 -07:00
|
|
|
|
2023-11-28 00:45:21 +08:00
|
|
|
const accessors = {
|
|
|
|
xAccessor: (d) => d.x,
|
|
|
|
yAccessor: (d) => d.y,
|
|
|
|
};
|
|
|
|
|
2021-05-11 15:41:42 -07:00
|
|
|
function insertBlankAt(ts: number, newLine: Array<NumericDataPoint>) {
|
2023-02-25 14:09:07 +05:30
|
|
|
const dateString = new Date(ts).toISOString();
|
2021-05-11 15:41:42 -07:00
|
|
|
for (let i = 0; i < newLine.length; i++) {
|
|
|
|
if (new Date(newLine[i].x).getTime() > ts) {
|
|
|
|
newLine.splice(i, 0, { x: dateString, y: 0 });
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
newLine.push({ x: dateString, y: 0 });
|
|
|
|
}
|
|
|
|
|
2023-02-25 14:09:07 +05:30
|
|
|
export function computeLines(chartData: TimeSeriesChartType, insertBlankPoints: boolean) {
|
2021-07-30 17:41:03 -07:00
|
|
|
if (!insertBlankPoints) {
|
|
|
|
return chartData.lines;
|
|
|
|
}
|
|
|
|
|
2021-05-11 15:41:42 -07:00
|
|
|
const startDate = new Date(Number(chartData.dateRange.start));
|
|
|
|
const endDate = new Date(Number(chartData.dateRange.end));
|
|
|
|
const returnLines: NamedLine[] = [];
|
|
|
|
chartData.lines.forEach((line) => {
|
|
|
|
const newLine = [...line.data];
|
2023-02-21 03:47:31 +05:30
|
|
|
for (let i = startDate; i <= endDate; i = addInterval(1, i, chartData.interval)) {
|
2021-05-11 15:41:42 -07:00
|
|
|
const pointOverlap = line.data.filter((point) => {
|
2023-02-21 03:47:31 +05:30
|
|
|
return Math.abs(new Date(point.x).getTime() - i.getTime()) === 0;
|
2021-05-11 15:41:42 -07:00
|
|
|
});
|
|
|
|
|
2023-02-21 03:47:31 +05:30
|
|
|
if (pointOverlap.length === 0) {
|
|
|
|
insertBlankAt(i.getTime(), newLine);
|
2021-05-11 15:41:42 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
returnLines.push({ name: line.name, data: newLine });
|
|
|
|
});
|
|
|
|
return returnLines;
|
|
|
|
}
|
|
|
|
|
2023-07-06 10:19:40 -07:00
|
|
|
export const TimeSeriesChart = ({
|
|
|
|
chartData,
|
|
|
|
width,
|
|
|
|
height,
|
|
|
|
hideLegend,
|
|
|
|
style,
|
|
|
|
insertBlankPoints,
|
|
|
|
yScale,
|
|
|
|
yAxis,
|
|
|
|
}: Props) => {
|
2021-05-11 15:41:42 -07:00
|
|
|
const ordinalColorScale = scaleOrdinal<string, string>({
|
|
|
|
domain: chartData.lines.map((data) => data.name),
|
|
|
|
range: lineColors.slice(0, chartData.lines.length),
|
|
|
|
});
|
|
|
|
|
2021-07-30 17:41:03 -07:00
|
|
|
const lines = useMemo(() => computeLines(chartData, insertBlankPoints || false), [chartData, insertBlankPoints]);
|
2021-05-11 15:41:42 -07:00
|
|
|
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<XYChart
|
2023-11-28 00:45:21 +08:00
|
|
|
accessibilityLabel={chartData.title}
|
2021-05-11 15:41:42 -07:00
|
|
|
width={width}
|
|
|
|
height={height}
|
2023-07-06 10:19:40 -07:00
|
|
|
margin={{ top: MARGIN.TOP, right: MARGIN.RIGHT, bottom: MARGIN.BOTTOM, left: MARGIN.LEFT }}
|
2021-05-11 15:41:42 -07:00
|
|
|
xScale={{ type: 'time' }}
|
2023-11-28 00:45:21 +08:00
|
|
|
yScale={yScale ?? { type: 'linear' }}
|
2021-05-11 15:41:42 -07:00
|
|
|
>
|
2023-11-28 00:45:21 +08:00
|
|
|
<Axis
|
|
|
|
orientation="bottom"
|
|
|
|
stroke={style?.axisColor}
|
|
|
|
strokeWidth={style?.axisWidth}
|
|
|
|
tickLabelProps={{ fill: 'black', fontFamily: 'inherit', fontSize: 10 }}
|
|
|
|
numTicks={3}
|
|
|
|
/>
|
|
|
|
<Axis
|
|
|
|
orientation="right"
|
|
|
|
stroke={style?.axisColor}
|
|
|
|
strokeWidth={style?.axisWidth}
|
2023-07-06 10:19:40 -07:00
|
|
|
tickFormat={(tick) => (yAxis?.formatter ? yAxis.formatter(tick) : formatNumber(tick))}
|
2023-11-28 00:45:21 +08:00
|
|
|
tickLabelProps={{ fill: 'black', fontFamily: 'inherit', fontSize: 10 }}
|
|
|
|
numTicks={3}
|
2022-02-15 16:25:01 -08:00
|
|
|
/>
|
2021-07-30 17:41:03 -07:00
|
|
|
{lines.map((line, i) => (
|
2023-11-28 00:45:21 +08:00
|
|
|
<>
|
|
|
|
<LineSeries
|
|
|
|
dataKey={line.name}
|
|
|
|
data={line.data.map((point) => ({ x: new Date(point.x), y: point.y }))}
|
|
|
|
stroke={(style && style.lineColor) || lineColors[i]}
|
|
|
|
curve={curveMonotoneX}
|
|
|
|
{...accessors}
|
|
|
|
/>
|
|
|
|
<GlyphSeries
|
|
|
|
dataKey={line.name}
|
|
|
|
data={line.data.map((point) => ({ x: new Date(point.x), y: point.y }))}
|
|
|
|
{...accessors}
|
|
|
|
/>
|
|
|
|
</>
|
2021-05-11 15:41:42 -07:00
|
|
|
))}
|
2023-11-28 00:45:21 +08:00
|
|
|
<StyledTooltip
|
|
|
|
snapTooltipToDatumX
|
|
|
|
showVerticalCrosshair
|
|
|
|
showDatumGlyph
|
|
|
|
verticalCrosshairStyle={{ stroke: '#D8D8D8', strokeDasharray: '5,2', strokeWidth: 1 }}
|
|
|
|
renderTooltip={({ tooltipData }) =>
|
|
|
|
tooltipData?.nearestDatum && (
|
|
|
|
<div>
|
|
|
|
<div>
|
|
|
|
{new Date(
|
|
|
|
Number(accessors.xAccessor(tooltipData.nearestDatum.datum)),
|
|
|
|
).toDateString()}
|
|
|
|
</div>
|
|
|
|
<div>{accessors.yAccessor(tooltipData.nearestDatum.datum)}</div>
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|
2021-07-30 17:41:03 -07:00
|
|
|
/>
|
2021-05-11 15:41:42 -07:00
|
|
|
</XYChart>
|
2021-07-30 17:41:03 -07:00
|
|
|
{!hideLegend && <Legend ordinalScale={ordinalColorScale} />}
|
2021-05-11 15:41:42 -07:00
|
|
|
</>
|
|
|
|
);
|
|
|
|
};
|