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 { useTheme } from '../hooks/useTheme';
|
||||||
import { DateUnit } from '@tianji/shared';
|
import { DateUnit } from '@tianji/shared';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { formatDate, formatDateWithUnit } from '../utils/date';
|
import { formatDateWithUnit } from '../utils/date';
|
||||||
import { Column, ColumnConfig } from '@ant-design/charts';
|
import {
|
||||||
import { useIsMobile } from '@/hooks/useIsMobile';
|
Area,
|
||||||
import { useTranslation } from '@i18next-toolkit/react';
|
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<{
|
export const TimeEventChart: React.FC<{
|
||||||
labelMapping?: Record<string, string>;
|
labelMapping?: Record<string, string>;
|
||||||
data: { x: string; y: number; type: string }[];
|
data: { date: string; [key: string]: number | string }[];
|
||||||
unit: DateUnit;
|
unit: DateUnit;
|
||||||
}> = React.memo((props) => {
|
}> = React.memo((props) => {
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const isMobile = useIsMobile();
|
const [calcStrokeDasharray, strokes] = useStrokeDasharray({});
|
||||||
const { t } = useTranslation();
|
const [strokeDasharray, setStrokeDasharray] = React.useState([...strokes]);
|
||||||
|
const handleAnimationEnd = () => setStrokeDasharray([...strokes]);
|
||||||
const labelMapping = props.labelMapping ?? {
|
const getStrokeDasharray = (name: string) => {
|
||||||
pageview: t('pageview'),
|
const lineDasharray = strokeDasharray.find((s) => s.name === name);
|
||||||
session: t('session'),
|
return lineDasharray ? lineDasharray.strokeDasharray : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const config = useMemo(
|
return (
|
||||||
() =>
|
<ChartContainer config={chartConfig}>
|
||||||
({
|
<AreaChart
|
||||||
data: props.data,
|
data={props.data}
|
||||||
isStack: true,
|
margin={{ top: 10, right: 0, left: 0, bottom: 0 }}
|
||||||
xField: 'x',
|
>
|
||||||
yField: 'y',
|
<defs>
|
||||||
seriesField: 'type',
|
<linearGradient id="colorUv" x1="0" y1="0" x2="0" y2="1">
|
||||||
label: {
|
<stop offset="5%" stopColor={colors.chart.pv} stopOpacity={0.8} />
|
||||||
position: 'middle' as const,
|
<stop offset="95%" stopColor={colors.chart.pv} stopOpacity={0} />
|
||||||
style: {
|
</linearGradient>
|
||||||
fill: '#FFFFFF',
|
<linearGradient id="colorPv" x1="0" y1="0" x2="0" y2="1">
|
||||||
opacity: 0.6,
|
<stop offset="5%" stopColor={colors.chart.uv} stopOpacity={0.8} />
|
||||||
},
|
<stop offset="95%" stopColor={colors.chart.uv} stopOpacity={0} />
|
||||||
},
|
</linearGradient>
|
||||||
tooltip: {
|
</defs>
|
||||||
title: (t) => formatDate(t),
|
<Customized component={calcStrokeDasharray} />
|
||||||
},
|
<XAxis
|
||||||
color: [colors.chart.pv, colors.chart.uv],
|
dataKey="date"
|
||||||
legend: isMobile
|
tickFormatter={(text) => formatDateWithUnit(text, props.unit)}
|
||||||
? false
|
/>
|
||||||
: {
|
<YAxis mirror />
|
||||||
itemName: {
|
<ChartLegend content={<ChartLegendContent />} />
|
||||||
formatter: (text) => labelMapping[text] ?? text,
|
<CartesianGrid vertical={false} />
|
||||||
},
|
<ChartTooltip content={<ChartTooltipContent />} />
|
||||||
},
|
<Area
|
||||||
xAxis: {
|
type="monotone"
|
||||||
label: {
|
dataKey="pv"
|
||||||
autoHide: true,
|
stroke={colors.chart.pv}
|
||||||
autoRotate: false,
|
fillOpacity={1}
|
||||||
formatter: (text) => formatDateWithUnit(text, props.unit),
|
fill="url(#colorUv)"
|
||||||
},
|
strokeWidth={2}
|
||||||
},
|
strokeDasharray={getStrokeDasharray('pv')}
|
||||||
}) satisfies ColumnConfig,
|
onAnimationEnd={handleAnimationEnd}
|
||||||
[props.data, props.unit, props.labelMapping]
|
/>
|
||||||
|
<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';
|
TimeEventChart.displayName = 'TimeEventChart';
|
||||||
|
@ -51,10 +51,11 @@ export const TelemetryOverview: React.FC<{
|
|||||||
const pageviewsArr = getDateArray(pageviews, startDate, endDate, unit);
|
const pageviewsArr = getDateArray(pageviews, startDate, endDate, unit);
|
||||||
const sessionsArr = getDateArray(sessions, startDate, endDate, unit);
|
const sessionsArr = getDateArray(sessions, startDate, endDate, unit);
|
||||||
|
|
||||||
return [
|
return pageviewsArr.map((item, i) => ({
|
||||||
...pageviewsArr.map((item) => ({ ...item, type: 'pageview' })),
|
pv: item.y,
|
||||||
...sessionsArr.map((item) => ({ ...item, type: 'session' })),
|
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 pageviewsArr = getDateArray(pageviews, startDate, endDate, unit);
|
||||||
const sessionsArr = getDateArray(sessions, startDate, endDate, unit);
|
const sessionsArr = getDateArray(sessions, startDate, endDate, unit);
|
||||||
|
|
||||||
return [
|
return pageviewsArr.map((item, i) => ({
|
||||||
...pageviewsArr.map((item) => ({ ...item, type: 'pageview' })),
|
pv: item.y,
|
||||||
...sessionsArr.map((item) => ({ ...item, type: 'session' })),
|
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 dayjs from 'dayjs';
|
||||||
import { WebsiteSession } from '@prisma/client';
|
import { WebsiteSession } from '@prisma/client';
|
||||||
|
|
||||||
const websiteId = 'cm1cemqhw0009g2snzh3eqjzr';
|
const websiteId = 'cly5yay7a001v5tp6xdkzmygh';
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
Array.from({ length: 200 }).map(async (_, i) => {
|
Array.from({ length: 200 }).map(async (_, i) => {
|
||||||
|
Loading…
Reference in New Issue
Block a user