refactor: refactor time event chart to recharts
This commit is contained in:
parent
055f57e087
commit
1337eaa2c0
@ -1,63 +1,95 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useTheme } from '../hooks/useTheme';
|
||||
import { DateUnit } from '@tianji/shared';
|
||||
import React from 'react';
|
||||
import { formatDate, formatDateWithUnit } from '../utils/date';
|
||||
import { Column, ColumnConfig } from '@ant-design/charts';
|
||||
import { useIsMobile } from '@/hooks/useIsMobile';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
import { formatDateWithUnit } from '../utils/date';
|
||||
import {
|
||||
Area,
|
||||
AreaChart,
|
||||
CartesianGrid,
|
||||
Customized,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from 'recharts';
|
||||
import {
|
||||
ChartConfig,
|
||||
ChartContainer,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from './ui/chart';
|
||||
import { useStrokeDasharray } from '@/hooks/useStrokeDasharray';
|
||||
|
||||
const chartConfig = {
|
||||
pv: {
|
||||
label: 'PV',
|
||||
},
|
||||
uv: {
|
||||
label: 'UV',
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export const TimeEventChart: React.FC<{
|
||||
labelMapping?: Record<string, string>;
|
||||
data: { x: string; y: number; type: string }[];
|
||||
data: { date: string; [key: string]: number | string }[];
|
||||
unit: DateUnit;
|
||||
}> = React.memo((props) => {
|
||||
const { colors } = useTheme();
|
||||
const isMobile = useIsMobile();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const labelMapping = props.labelMapping ?? {
|
||||
pageview: t('pageview'),
|
||||
session: t('session'),
|
||||
const [calcStrokeDasharray, strokes] = useStrokeDasharray({});
|
||||
const [strokeDasharray, setStrokeDasharray] = React.useState([...strokes]);
|
||||
const handleAnimationEnd = () => setStrokeDasharray([...strokes]);
|
||||
const getStrokeDasharray = (name: string) => {
|
||||
const lineDasharray = strokeDasharray.find((s) => s.name === name);
|
||||
return lineDasharray ? lineDasharray.strokeDasharray : undefined;
|
||||
};
|
||||
|
||||
const config = useMemo(
|
||||
() =>
|
||||
({
|
||||
data: props.data,
|
||||
isStack: true,
|
||||
xField: 'x',
|
||||
yField: 'y',
|
||||
seriesField: 'type',
|
||||
label: {
|
||||
position: 'middle' as const,
|
||||
style: {
|
||||
fill: '#FFFFFF',
|
||||
opacity: 0.6,
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
title: (t) => formatDate(t),
|
||||
},
|
||||
color: [colors.chart.pv, colors.chart.uv],
|
||||
legend: isMobile
|
||||
? false
|
||||
: {
|
||||
itemName: {
|
||||
formatter: (text) => labelMapping[text] ?? text,
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
label: {
|
||||
autoHide: true,
|
||||
autoRotate: false,
|
||||
formatter: (text) => formatDateWithUnit(text, props.unit),
|
||||
},
|
||||
},
|
||||
}) satisfies ColumnConfig,
|
||||
[props.data, props.unit, props.labelMapping]
|
||||
return (
|
||||
<ChartContainer config={chartConfig}>
|
||||
<AreaChart
|
||||
data={props.data}
|
||||
margin={{ top: 10, right: 0, left: 0, bottom: 0 }}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="colorUv" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={colors.chart.pv} stopOpacity={0.8} />
|
||||
<stop offset="95%" stopColor={colors.chart.pv} stopOpacity={0} />
|
||||
</linearGradient>
|
||||
<linearGradient id="colorPv" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={colors.chart.uv} stopOpacity={0.8} />
|
||||
<stop offset="95%" stopColor={colors.chart.uv} stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Customized component={calcStrokeDasharray} />
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tickFormatter={(text) => formatDateWithUnit(text, props.unit)}
|
||||
/>
|
||||
<YAxis mirror />
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
<CartesianGrid vertical={false} />
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="pv"
|
||||
stroke={colors.chart.pv}
|
||||
fillOpacity={1}
|
||||
fill="url(#colorUv)"
|
||||
strokeWidth={2}
|
||||
strokeDasharray={getStrokeDasharray('pv')}
|
||||
onAnimationEnd={handleAnimationEnd}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="uv"
|
||||
stroke={colors.chart.uv}
|
||||
fillOpacity={1}
|
||||
fill="url(#colorPv)"
|
||||
strokeWidth={2}
|
||||
strokeDasharray={getStrokeDasharray('uv')}
|
||||
onAnimationEnd={handleAnimationEnd}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
);
|
||||
|
||||
return <Column {...config} />;
|
||||
});
|
||||
TimeEventChart.displayName = 'TimeEventChart';
|
||||
|
@ -51,10 +51,11 @@ export const TelemetryOverview: React.FC<{
|
||||
const pageviewsArr = getDateArray(pageviews, startDate, endDate, unit);
|
||||
const sessionsArr = getDateArray(sessions, startDate, endDate, unit);
|
||||
|
||||
return [
|
||||
...pageviewsArr.map((item) => ({ ...item, type: 'pageview' })),
|
||||
...sessionsArr.map((item) => ({ ...item, type: 'session' })),
|
||||
];
|
||||
return pageviewsArr.map((item, i) => ({
|
||||
pv: item.y,
|
||||
uv: sessionsArr[i]?.y ?? 0,
|
||||
date: item.x,
|
||||
}));
|
||||
},
|
||||
}
|
||||
);
|
||||
|
@ -50,10 +50,11 @@ export const WebsiteOverview: React.FC<{
|
||||
const pageviewsArr = getDateArray(pageviews, startDate, endDate, unit);
|
||||
const sessionsArr = getDateArray(sessions, startDate, endDate, unit);
|
||||
|
||||
return [
|
||||
...pageviewsArr.map((item) => ({ ...item, type: 'pageview' })),
|
||||
...sessionsArr.map((item) => ({ ...item, type: 'session' })),
|
||||
];
|
||||
return pageviewsArr.map((item, i) => ({
|
||||
pv: item.y,
|
||||
uv: sessionsArr[i]?.y ?? 0,
|
||||
date: item.x,
|
||||
}));
|
||||
},
|
||||
}
|
||||
);
|
||||
|
188
src/client/hooks/useStrokeDasharray.ts
Normal file
188
src/client/hooks/useStrokeDasharray.ts
Normal file
@ -0,0 +1,188 @@
|
||||
/**
|
||||
* Reference: https://bit.cloud/teambit/analytics/hooks/use-recharts-line-stroke-dasharray
|
||||
*/
|
||||
|
||||
import { useCallback, useRef } from 'react';
|
||||
|
||||
type GraphicalItemPoint = {
|
||||
/**
|
||||
* x point coordinate.
|
||||
*/
|
||||
x?: number;
|
||||
/**
|
||||
* y point coordinate.
|
||||
*/
|
||||
y?: number;
|
||||
};
|
||||
|
||||
type GraphicalItemProps = {
|
||||
/**
|
||||
* graphical item points.
|
||||
*/
|
||||
points?: GraphicalItemPoint[];
|
||||
};
|
||||
|
||||
type ItemProps = {
|
||||
/**
|
||||
* item data key.
|
||||
*/
|
||||
dataKey?: string;
|
||||
};
|
||||
|
||||
type ItemType = {
|
||||
/**
|
||||
* recharts item display name.
|
||||
*/
|
||||
displayName?: string;
|
||||
};
|
||||
|
||||
type Item = {
|
||||
/**
|
||||
* item props.
|
||||
*/
|
||||
props?: ItemProps;
|
||||
/**
|
||||
* recharts item class.
|
||||
*/
|
||||
type?: ItemType;
|
||||
};
|
||||
|
||||
type GraphicalItem = {
|
||||
/**
|
||||
* from recharts internal state and props of chart.
|
||||
*/
|
||||
props?: GraphicalItemProps;
|
||||
/**
|
||||
* from recharts internal state and props of chart.
|
||||
*/
|
||||
item?: Item;
|
||||
};
|
||||
|
||||
type RechartsChartProps = {
|
||||
/**
|
||||
* from recharts internal state and props of chart.
|
||||
*/
|
||||
formattedGraphicalItems?: GraphicalItem[];
|
||||
};
|
||||
|
||||
type CalculateStrokeDasharray = (props?: any) => any;
|
||||
|
||||
type LineStrokeDasharray = {
|
||||
/**
|
||||
* line name.
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* line strokeDasharray.
|
||||
*/
|
||||
strokeDasharray?: string;
|
||||
};
|
||||
|
||||
type LinesStrokeDasharray = LineStrokeDasharray[];
|
||||
|
||||
type LineProps = {
|
||||
/**
|
||||
* line name.
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* specifies the starting index of the first dot in the dash pattern.
|
||||
*/
|
||||
dotIndex?: number;
|
||||
/**
|
||||
* defines the pattern of dashes and gaps. an array of [gap length, dash length].
|
||||
*/
|
||||
strokeDasharray?: [number, number];
|
||||
/**
|
||||
* adjusts the percentage correction of the first line segment for better alignment in curved lines.
|
||||
*/
|
||||
curveCorrection?: number;
|
||||
};
|
||||
|
||||
export type UseStrokeDasharrayProps = {
|
||||
/**
|
||||
* an array of properties to target specific line(s) and override default settings.
|
||||
*/
|
||||
linesProps?: LineProps[];
|
||||
} & LineProps;
|
||||
|
||||
export function useStrokeDasharray({
|
||||
linesProps = [],
|
||||
dotIndex = -2,
|
||||
strokeDasharray: restStroke = [5, 3],
|
||||
curveCorrection = 1,
|
||||
}: UseStrokeDasharrayProps): [CalculateStrokeDasharray, LinesStrokeDasharray] {
|
||||
const linesStrokeDasharray = useRef<LinesStrokeDasharray>([]);
|
||||
|
||||
const calculateStrokeDasharray = useCallback(
|
||||
(props: RechartsChartProps): null => {
|
||||
const items = props?.formattedGraphicalItems;
|
||||
|
||||
const getLineWidth = (points: GraphicalItemPoint[]) => {
|
||||
const width = points?.reduce((acc, point, index) => {
|
||||
if (!index) return acc;
|
||||
|
||||
const prevPoint = points?.[index - 1];
|
||||
|
||||
const xAxis = point?.x || 0;
|
||||
const prevXAxis = prevPoint?.x || 0;
|
||||
const xWidth = xAxis - prevXAxis;
|
||||
|
||||
const yAxis = point?.y || 0;
|
||||
const prevYAxis = prevPoint?.y || 0;
|
||||
const yWidth = Math.abs(yAxis - prevYAxis);
|
||||
|
||||
const hypotenuse = Math.sqrt(xWidth * xWidth + yWidth * yWidth);
|
||||
acc += hypotenuse;
|
||||
return acc;
|
||||
}, 0);
|
||||
|
||||
return width || 0;
|
||||
};
|
||||
|
||||
items?.forEach((line) => {
|
||||
const linePoints = line?.props?.points ?? [];
|
||||
const lineWidth = getLineWidth(linePoints);
|
||||
|
||||
const name = line?.item?.props?.dataKey;
|
||||
const targetLine = linesProps?.find((target) => target?.name === name);
|
||||
const targetIndex = targetLine?.dotIndex ?? dotIndex;
|
||||
const dashedPoints = linePoints?.slice(targetIndex);
|
||||
const dashedWidth = getLineWidth(dashedPoints);
|
||||
|
||||
if (!lineWidth || !dashedWidth) return;
|
||||
|
||||
const firstWidth = lineWidth - dashedWidth;
|
||||
const targetCurve = targetLine?.curveCorrection ?? curveCorrection;
|
||||
const correctionWidth = (firstWidth * targetCurve) / 100;
|
||||
const firstDasharray = firstWidth + correctionWidth;
|
||||
|
||||
const targetRestStroke = targetLine?.strokeDasharray || restStroke;
|
||||
const gapDashWidth = targetRestStroke?.[0] + targetRestStroke?.[1] || 1;
|
||||
const restDasharrayLength = dashedWidth / gapDashWidth;
|
||||
const restDasharray = new Array(Math.ceil(restDasharrayLength)).fill(
|
||||
targetRestStroke.join(' ')
|
||||
);
|
||||
|
||||
const strokeDasharray = `${firstDasharray} ${restDasharray.join(' ')}`;
|
||||
const lineStrokeDasharray = { name, strokeDasharray };
|
||||
|
||||
const dasharrayIndex = linesStrokeDasharray.current.findIndex((d) => {
|
||||
return d.name === line?.item?.props?.dataKey;
|
||||
});
|
||||
|
||||
if (dasharrayIndex === -1) {
|
||||
linesStrokeDasharray.current.push(lineStrokeDasharray);
|
||||
return;
|
||||
}
|
||||
|
||||
linesStrokeDasharray.current[dasharrayIndex] = lineStrokeDasharray;
|
||||
});
|
||||
|
||||
return null;
|
||||
},
|
||||
[dotIndex]
|
||||
);
|
||||
|
||||
return [calculateStrokeDasharray, linesStrokeDasharray.current];
|
||||
}
|
@ -5,7 +5,7 @@ import { getLocation } from '../../utils/detect.js';
|
||||
import dayjs from 'dayjs';
|
||||
import { WebsiteSession } from '@prisma/client';
|
||||
|
||||
const websiteId = 'cm1cemqhw0009g2snzh3eqjzr';
|
||||
const websiteId = 'cly5yay7a001v5tp6xdkzmygh';
|
||||
|
||||
async function main() {
|
||||
Array.from({ length: 200 }).map(async (_, i) => {
|
||||
|
Loading…
Reference in New Issue
Block a user