-
Notifications
You must be signed in to change notification settings - Fork 80
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: refactor time event chart to recharts
- Loading branch information
1 parent
055f57e
commit 1337eaa
Showing
5 changed files
with
280 additions
and
58 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters