refactor: migrate monitor data chart to recharts and remove @ant-design/charts

This commit is contained in:
moonrailgun 2024-10-12 01:23:00 +08:00
parent a218c22397
commit c0e2ef0fe8
4 changed files with 163 additions and 1594 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +1,34 @@
import { AreaConfig, Area } from '@ant-design/charts';
import { Select } from 'antd'; import { Select } from 'antd';
import dayjs, { Dayjs } from 'dayjs'; import dayjs, { Dayjs } from 'dayjs';
import { max, min, uniqBy } from 'lodash-es'; import { get, takeRight, uniqBy } from 'lodash-es';
import React, { useState, useMemo } from 'react'; import React, { useState, useMemo } from 'react';
import { useSocketSubscribeList } from '../../api/socketio'; import { useSocketSubscribeList } from '../../api/socketio';
import { trpc } from '../../api/trpc'; import { trpc } from '../../api/trpc';
import { useCurrentWorkspaceId } from '../../store/user'; import { useCurrentWorkspaceId } from '../../store/user';
import { getMonitorProvider, getProviderDisplay } from './provider'; import { getMonitorProvider, getProviderDisplay } from './provider';
import { useTranslation } from '@i18next-toolkit/react'; import { useTranslation } from '@i18next-toolkit/react';
import {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from '../ui/chart';
import {
Area,
AreaChart,
CartesianGrid,
Customized,
Rectangle,
XAxis,
YAxis,
} from 'recharts';
import { useTheme } from '@/hooks/useTheme';
const chartConfig = {
value: {
label: <span className="text-sm font-bold">Result</span>,
},
} satisfies ChartConfig;
export const MonitorDataChart: React.FC<{ monitorId: string }> = React.memo( export const MonitorDataChart: React.FC<{ monitorId: string }> = React.memo(
(props) => { (props) => {
@ -15,6 +36,7 @@ export const MonitorDataChart: React.FC<{ monitorId: string }> = React.memo(
const workspaceId = useCurrentWorkspaceId(); const workspaceId = useCurrentWorkspaceId();
const { monitorId } = props; const { monitorId } = props;
const [rangeType, setRangeType] = useState('recent'); const [rangeType, setRangeType] = useState('recent');
const { colors } = useTheme();
const subscribedDataList = useSocketSubscribeList( const subscribedDataList = useSocketSubscribeList(
'onMonitorReceiveNewData', 'onMonitorReceiveNewData',
{ {
@ -61,99 +83,25 @@ export const MonitorDataChart: React.FC<{ monitorId: string }> = React.memo(
const providerInfo = getMonitorProvider(monitorInfo?.type ?? ''); const providerInfo = getMonitorProvider(monitorInfo?.type ?? '');
const { data, annotations } = useMemo(() => { const { data } = useMemo(() => {
const annotations: AreaConfig['annotations'] = [];
let start: number | null = null;
let fetchedData = rangeType === 'recent' ? _recentData : _data; let fetchedData = rangeType === 'recent' ? _recentData : _data;
const data = uniqBy( const data = takeRight(
[...fetchedData, ...subscribedDataList], uniqBy([...fetchedData, ...subscribedDataList], 'createdAt'),
'createdAt' fetchedData.length
).map((d, i, arr) => { ).map((d, i, arr) => {
const value = d.value > 0 ? d.value : null; const value = d.value > 0 ? d.value : null;
const time = dayjs(d.createdAt).valueOf(); const time = dayjs(d.createdAt).valueOf();
if (!value && !start && arr[i - 1]) {
start = dayjs(arr[i - 1]['createdAt']).valueOf();
} else if (value && start) {
annotations.push({
type: 'region',
start: [start, 'min'],
end: [time, 'max'],
style: {
fill: 'red',
fillOpacity: 0.25,
},
});
start = null;
}
return { return {
value, value,
time, time,
}; };
}); });
return { data, annotations }; return { data };
}, [_recentData, _data, subscribedDataList]); }, [_recentData, _data, subscribedDataList]);
const config = useMemo<AreaConfig>(() => { const isTrendingMode = monitorInfo?.trendingMode ?? false; // if true, y axis not start from 0
const values = data.map((d) => d.value);
const maxValue = max(values) ?? 0;
const minValue = min(values) ?? 0;
const isTrendingMode = monitorInfo?.trendingMode ?? false; // if true, y axis not start from 0
const yMin = isTrendingMode
? Math.max(minValue - (maxValue - minValue) / 10, 0)
: 0;
return {
data,
height: 200,
xField: 'time',
yField: 'value',
smooth: true,
meta: {
value: {
min: yMin,
},
time: {
formatter(value) {
return dayjs(value).format(
rangeType === '1w' ? 'MM-DD HH:mm' : 'HH:mm'
);
},
},
},
// need explore how to display null data
// xAxis: {
// type: 'time',
// },
color: 'rgb(34 197 94 / 0.8)',
areaStyle: () => {
return {
fill: 'l(270) 0:rgb(34 197 94 / 0.2) 0.5:rgb(34 197 94 / 0.5) 1:rgb(34 197 94 / 0.8)',
};
},
annotations,
tooltip: {
title: (title, datum) => {
return dayjs(datum.time).format('YYYY-MM-DD HH:mm');
},
formatter(datum) {
const { name, text } = getProviderDisplay(
datum.value,
providerInfo
);
return {
name,
value: datum.value ? text : 'null',
};
},
},
};
}, [data, rangeType]);
return ( return (
<div> <div>
@ -172,9 +120,122 @@ export const MonitorDataChart: React.FC<{ monitorId: string }> = React.memo(
</Select> </Select>
</div> </div>
<Area {...config} /> <ChartContainer className="h-[200px] w-full" config={chartConfig}>
<AreaChart
data={data}
margin={{ top: 10, right: 0, left: 0, bottom: 0 }}
>
<defs>
<linearGradient id="color" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor={colors.chart.monitor}
stopOpacity={0.3}
/>
<stop
offset="95%"
stopColor={colors.chart.monitor}
stopOpacity={0}
/>
</linearGradient>
</defs>
<XAxis
dataKey="time"
type="number"
domain={['dataMin', 'dataMax']}
tickFormatter={(date) =>
dayjs(date).format(rangeType === '1w' ? 'MM-DD HH:mm' : 'HH:mm')
}
/>
<YAxis
mirror
domain={[isTrendingMode ? 'dataMin' : 0, 'dataMax']}
/>
<CartesianGrid vertical={false} />
<ChartTooltip
labelFormatter={(label, payload) =>
dayjs(get(payload, [0, 'payload', 'time'])).format(
'YYYY-MM-DD HH:mm:ss'
)
}
formatter={(value, defaultText, item, index, payload) => {
if (typeof value !== 'number') {
return defaultText;
}
const { name, text } = getProviderDisplay(
Number(value),
providerInfo
);
return (
<div>
<span className="mr-2">{name}:</span>
<span>{text}</span>
</div>
);
}}
content={<ChartTooltipContent />}
/>
<Customized component={CustomizedErrorArea} />
<Area
type="monotone"
dataKey="value"
stroke={colors.chart.monitor}
fillOpacity={1}
fill="url(#color)"
strokeWidth={2}
isAnimationActive={false}
/>
</AreaChart>
</ChartContainer>
</div> </div>
); );
} }
); );
MonitorDataChart.displayName = 'MonitorDataChart'; MonitorDataChart.displayName = 'MonitorDataChart';
const CustomizedErrorArea: React.FC = (props) => {
const { colors } = useTheme();
const y = get(props, 'offset.top', 10);
const height = get(props, 'offset.height', 160);
const points = get(props, 'formattedGraphicalItems.0.props.points', []) as {
x: number;
y: number | null;
}[];
const errorArea = useMemo(() => {
const _errorArea: { x: number; width: number }[] = [];
let prevX: number | null = null;
points.forEach((item, i, arr) => {
if (i === 0 && !item.y) {
prevX = 0;
} else if (!item.y && prevX === null && arr[i - 1].y) {
prevX = arr[i - 1].x;
} else if (item.y && prevX !== null) {
_errorArea.push({
x: prevX,
width: item.x - prevX,
});
prevX = null;
}
});
return _errorArea;
}, [points]);
return errorArea.map((area, i) => {
return (
<Rectangle
key={i}
width={area.width}
height={height}
x={area.x}
y={y}
fill={colors.chart.error}
/>
);
});
};
CustomizedErrorArea.displayName = 'CustomizedErrorArea';

View File

@ -2,6 +2,7 @@ import { useColorSchema } from '@/store/settings';
import { theme, ThemeConfig } from 'antd'; import { theme, ThemeConfig } from 'antd';
import { useEffect, useMemo } from 'react'; import { useEffect, useMemo } from 'react';
import { colord } from 'colord'; import { colord } from 'colord';
import twColors from 'tailwindcss/colors';
const THEME_CONFIG = 'tianji.theme'; const THEME_CONFIG = 'tianji.theme';
@ -19,6 +20,9 @@ const THEME_COLORS = {
gray700: '#6e6e6e', gray700: '#6e6e6e',
gray800: '#4b4b4b', gray800: '#4b4b4b',
gray900: '#2c2c2c', gray900: '#2c2c2c',
green400: twColors.green['400'],
green500: twColors.green['500'],
green600: twColors.green['600'],
}, },
dark: { dark: {
primary: '#2680eb', primary: '#2680eb',
@ -33,6 +37,9 @@ const THEME_COLORS = {
gray700: '#b9b9b9', gray700: '#b9b9b9',
gray800: '#e3e3e3', gray800: '#e3e3e3',
gray900: '#ffffff', gray900: '#ffffff',
green400: twColors.green['600'],
green500: twColors.green['500'],
green600: twColors.green['400'],
}, },
}; };
@ -55,7 +62,14 @@ export function useTheme() {
const customTheme = window.localStorage.getItem(THEME_CONFIG); const customTheme = window.localStorage.getItem(THEME_CONFIG);
const theme = isValidTheme(customTheme) ? customTheme : defaultTheme; const theme = isValidTheme(customTheme) ? customTheme : defaultTheme;
const primaryColor = useMemo(() => colord(THEME_COLORS[theme].primary), []); const primaryColor = useMemo(
() => colord(THEME_COLORS[theme].primary),
[theme]
);
const healthColor = useMemo(
() => colord(THEME_COLORS[theme].green400),
[theme]
);
const colors = useMemo( const colors = useMemo(
() => ({ () => ({
@ -63,10 +77,12 @@ export function useTheme() {
...THEME_COLORS[theme], ...THEME_COLORS[theme],
}, },
chart: { chart: {
error: twColors.red[500],
text: THEME_COLORS[theme].gray700, text: THEME_COLORS[theme].gray700,
line: THEME_COLORS[theme].gray200, line: THEME_COLORS[theme].gray200,
pv: primaryColor.alpha(0.4).toRgbString(), pv: primaryColor.alpha(0.4).toRgbString(),
uv: primaryColor.alpha(0.6).toRgbString(), uv: primaryColor.alpha(0.6).toRgbString(),
monitor: healthColor.alpha(0.8).toRgbString(),
}, },
map: { map: {
baseColor: THEME_COLORS[theme].primary, baseColor: THEME_COLORS[theme].primary,

View File

@ -17,7 +17,6 @@
"keywords": [], "keywords": [],
"author": "moonrailgun <moonrailgun@gmail.com>", "author": "moonrailgun <moonrailgun@gmail.com>",
"dependencies": { "dependencies": {
"@ant-design/charts": "^1.4.2",
"@ant-design/icons": "^5.3.6", "@ant-design/icons": "^5.3.6",
"@antv/l7": "^2.20.14", "@antv/l7": "^2.20.14",
"@antv/larkmap": "^1.4.13", "@antv/larkmap": "^1.4.13",