From f6e933f784db7126bd2e0a810dd27c9019a1f4ab Mon Sep 17 00:00:00 2001 From: luisdralves Date: Mon, 6 May 2024 15:02:35 +0100 Subject: [PATCH] Replace recharts with custom canvas --- bun.lockb | Bin 96634 -> 82366 bytes package.json | 5 +- .../components/chart-cards/common/card.tsx | 29 ++++ .../components/chart-cards/common/chart.css | 40 +++++ .../components/chart-cards/common/chart.tsx | 143 ++++++++++++++++++ .../{ => chart-cards/common}/legend/index.css | 0 .../{ => chart-cards/common}/legend/index.tsx | 0 src/client/components/chart-cards/cpu.tsx | 17 +-- src/client/components/chart-cards/disks.tsx | 18 +-- src/client/components/chart-cards/index.tsx | 71 --------- src/client/components/chart-cards/memory.tsx | 18 +-- src/client/components/chart-cards/network.tsx | 18 +-- src/client/components/chart-cards/temps.tsx | 20 +-- 13 files changed, 226 insertions(+), 153 deletions(-) create mode 100644 src/client/components/chart-cards/common/card.tsx create mode 100644 src/client/components/chart-cards/common/chart.css create mode 100644 src/client/components/chart-cards/common/chart.tsx rename src/client/components/{ => chart-cards/common}/legend/index.css (100%) rename src/client/components/{ => chart-cards/common}/legend/index.tsx (100%) delete mode 100644 src/client/components/chart-cards/index.tsx diff --git a/bun.lockb b/bun.lockb index 3f166ebcbcf0c87b07f85815080d7d5d8fb1223a..60009f15469fd16770abb0102d7a89335f760c0b 100755 GIT binary patch delta 13991 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�w5_o^~L8m&A(> z@g8L%eV_Ir{eYC0me6t^5BY0}_PjKXv)<;7kli?-o00QaJ1(V|Ltd72e6+OOFOHMo zq7LQ-jBNa5Xo0_H@qun$##oQCW>9@w>_2`aQ&^S;;WHH6p*5w#*SswL9nlO$)3u`g zpB*g}a+gRNcWm`N{!#k>k9LgO|KqWcCYHm=dg}*eZ7zBBUX~Ua_%Blu$KTzbdHQeR z_oTnC9WK)H_ldsmGv>!T`oo3q0{r!p!vv4XdAc6~T=$+K`K#=6fFJIW>s~Xu_P1S|%Y0{p|~FTiKO=fGcqFM$t%3&1I$9(V)bzn;7T903jibpU_;Zv-|0 z)xZjXfLp*fz(wFR@G7ssPGo9A|A zi~}YCJk}KcjW`*Zsel`p2IK*=fl^=wkPnmq#Xu3jlPLrWfSE>~K@%$Hqk`AIP)_4}otCwXmu1m+wgG;-(_bF z>h(R$mg$JXT?|4fD{iizR>g@bDHs3(qxb^z%?(H)35*W z#+t1jYKxX=dlJ2eVQ*M5&zd!N);Hme8s-h#QCDBBUHBjK0vl_*Eb3UawVIc5gC80$ z{`|=M6^$A;i@F>da<)Z%wk3RhqM3S?M>B8HM!vtL=-l+@$I4<{Y;+uZ%Djzw_3EoN zJL8U|c!-#2tX~`sx8`bhAB<++Uma3=BE4CYbw1bz=)%pY<4{sRWs9|3BjqnZcDVBUx+gS6x zw>p>Gniqs8`rT4mwr}%oqefT@eP*AmF8Qjq&$ovgYZ6Uwai zdB68+!ich*z?w8HE?&Us1f0Tt>N`A|c_aA7;9)t>Ht+Mehlq{lR|Q{ne!7D$ep;z^ zTCx3XV@;-?8qykVbD`9B<6|KQfL#+qtBH4kmA=GEn{ z4QI|i-g@ZbMvZzubrbh)UUq(A-K!s7{&-7uqsDEF+Z^Mb8MFHA`S%_h--fM9;E;vg zU!bk*8>HUX1u`y3eIyKWNRXP=1~NZLE$krT)XFv@&}v@(EPi?P&OLI=8*om%9{HaK zsjomi(Y#mcn%gkDDC^obXvD#N@%Uh|>c2<)ELp+Bf*-G!MRIh4w!eg0)#hzspm}Gt zSFC+h;JiQV(%vgLA7{2zO^1f;wyN1}MW$R~RkuT0%`3diYM&o{`_Z>YYx9X=vo=~) zA3wC%X;qh7MO(FxpBP~^Zx9EZs#2~j*t-+zJu$hS*qg2DEzV0AB~)x2??e>3H~q0>eNX`?6d+nyS%{@4zq_YYPpI>R#a z@^|R0A$yD340#QjG0`bJa89tA=?@L_+W6>;*CKt}+aJ<2w9y;T2Q=jAA!@ZhMlTIf z-(o&DM7_?uGDNiopa=8*`2PCx6@91d>Wm(ESAzGTc^@4(BhCzS`n>Q4S|&zoEAT^@ zIw}xrWL^XBvCa0{#|tJts#zAxYc#Km`f?ynpvbPOEeNX$uRR%rF%r9~uLX(8R`UY* zjBX(lpItd|xGZ|2YZxSn2(_;jqnlT}*Q`uG{@%d{{LyzpbWdIZecdC%nn&vQsm)8@ z&euDf7`P_j8)(Ku6SF{HSFNzfkg89x7$z%G>V#lS>2j31Cm7B(?{g39@%rX~)5qFq z%{3!7MXP@b##GFk-Vns9r5ba_HZxUyuQ7tmQxbcub?zYwa1pUqH>=%sFlrkv1A zz1$ID^p8H%RtBEbx0SX$2fX-mI-$|kUg|EEVX5jLJHcf0mU-{5U)y)uCt)fK!=A-X zG4GvwJP@Q>Y+{>G;uf7Re z&8z2C2aX+ccZ{u+MN)JUcP+)+)E`;X*p`irD$dwx8h!R=XA%F)+cj1UKLKp26}!Ea zFTLYbuh=l_- zx_;&;1!*Tg9Z`K)7IPD#;}UTejLcA9#==^0<6XP(elBr;-&f!IY8;X*<+ zH1EhC*)^z-)seihQNz4N|L2=Mr~iAWCioaK+n&r&ZQ;Vdk9qI@P_@f{;L2&o8a2$D z`Jg#N`&WueI=sa<39mMmdFeM delta 22501 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 & Partial>; + formatOptions?: FormatOptions; + domain?: [number, number]; + hueOffset?: number; + data: number[]; + total: number; +}; + +export const ChartCard = ({ data, domain, legend, hueOffset = 0, title, subtitle, formatOptions, total }: Props) => { + return ( +
+

{title}

+ + {subtitle} + + {legend && } + + +
+ ); +}; diff --git a/src/client/components/chart-cards/common/chart.css b/src/client/components/chart-cards/common/chart.css new file mode 100644 index 0000000..0d8fa30 --- /dev/null +++ b/src/client/components/chart-cards/common/chart.css @@ -0,0 +1,40 @@ +.chart { + color: var(--color-neutral2); + display: grid; + font-size: 16px; + gap: 4px; + grid-template-columns: max-content 1fr; + height: 100%; + line-height: 1; + overflow: hidden; +} + +.chart > .y-axis { + display: flex; + flex-direction: column; + height: 100%; + justify-content: space-between; + + > div { + text-align: right; + } +} + +.chart .cartesian-grid { + height: calc(100% - 16px); + position: absolute; + top: 8px; + width: 100%; +} + +.chart .canvas-wrapper { + position: relative; + + > canvas { + height: calc(100% - 16px); + left: 8px; + position: absolute; + top: 8px; + width: calc(100% - 8px); + } +} \ No newline at end of file diff --git a/src/client/components/chart-cards/common/chart.tsx b/src/client/components/chart-cards/common/chart.tsx new file mode 100644 index 0000000..633d680 --- /dev/null +++ b/src/client/components/chart-cards/common/chart.tsx @@ -0,0 +1,143 @@ +import { useAnimationFrame } from '@/hooks/use-animation-frame'; +import { getFillColor, getStrokeColor } from '@/utils/colors'; +import { type FormatOptions, formatValue } from '@/utils/format'; +import { useEffect, useMemo, useRef, useState } from 'react'; +import './chart.css'; + +const stepWindow = Number(import.meta.env.CLIENT_GRAPH_STEPS); +const stepPeriod = Number(import.meta.env.CLIENT_REFETCH_INTERVAL); +const width = 640; +const height = 480; +const xMargin = 4; +const fps = 30; +const framePeriod = 1000 / fps; + +type Props = { + total: number; + data: number[]; + domain?: [number, number]; + formatOptions?: FormatOptions; + hueOffset?: number; +}; + +const xFromTimestamp = (timestamp: number) => + ((timestamp - Date.now()) / stepPeriod) * (width / stepWindow) + width + (width / stepWindow) * 2; + +const YAxis = ({ max, formatOptions }: Pick & { max: number }) => ( +
+ {Array.from({ length: 5 }).map((_, index) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: supress react console error +
{formatValue((max * (4 - index)) / 4, formatOptions)}
+ ))} +
+); + +const CartesianGrid = () => ( + + {'Cartesian grid'} + + {Array.from({ length: 5 }).map((_, index) => ( + + ))} + +); + +export const CanvasChart = ({ total, hueOffset = 0, domain, data, formatOptions }: Props) => { + const canvasRef = useRef(null!); + const ignored = useRef(0); + const now = useMemo(() => Date.now(), []); + const [history, setHistory] = useState<[number, number[]][]>([[now, []]]); + const max = useMemo(() => { + if (domain) { + return domain[1]; + } + + return 1.25 * history.reduce((max, [_, values]) => Math.max(max, ...values), 0); + }, [history, domain]); + + useEffect(() => { + if (data) { + setHistory(history => { + const firstValidIndex = history.findIndex(([timestamp]) => xFromTimestamp(timestamp) >= -xMargin); + const newHistory = history.slice(firstValidIndex); + newHistory.push([Date.now(), data]); + + return newHistory; + }); + } + }, [data]); + + useEffect(() => { + const ctx = canvasRef.current.getContext('2d'); + if (!ctx) { + return; + } + + ctx.lineWidth = 2; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + }, []); + + useAnimationFrame(dt => { + ignored.current += dt; + if (ignored.current < framePeriod) { + return; + } + ignored.current = 0; + const ctx = canvasRef.current.getContext('2d'); + if (!ctx) { + return; + } + + ctx.clearRect(0, 0, width, height); + + for (let i = 0; i < total; i++) { + ctx.fillStyle = getFillColor(hueOffset + (360 * Number(i)) / total); + ctx.strokeStyle = getStrokeColor(hueOffset + (360 * Number(i)) / total); + ctx.beginPath(); + ctx.moveTo(-xMargin, height); + ctx.lineTo(-xMargin, height - (height * (history[0][1][i] ?? 0)) / max); + for (const [timestamp, values] of history) { + const x = xFromTimestamp(timestamp); + const y = height - (height * (values[i] ?? 0)) / max; + + ctx.lineTo(x, y); + } + ctx.lineTo(width + xMargin, height - (height * (history[0][1][i] ?? 0)) / max); + ctx.lineTo(width + xMargin, height); + ctx.stroke(); + ctx.fill(); + ctx.closePath(); + } + }); + + return ( +
+ + +
+ + + +
+
+ ); +}; diff --git a/src/client/components/legend/index.css b/src/client/components/chart-cards/common/legend/index.css similarity index 100% rename from src/client/components/legend/index.css rename to src/client/components/chart-cards/common/legend/index.css diff --git a/src/client/components/legend/index.tsx b/src/client/components/chart-cards/common/legend/index.tsx similarity index 100% rename from src/client/components/legend/index.tsx rename to src/client/components/chart-cards/common/legend/index.tsx diff --git a/src/client/components/chart-cards/cpu.tsx b/src/client/components/chart-cards/cpu.tsx index 1bd44ec..5566338 100644 --- a/src/client/components/chart-cards/cpu.tsx +++ b/src/client/components/chart-cards/cpu.tsx @@ -1,22 +1,9 @@ import { useQuery } from '@tanstack/react-query'; -import { useEffect, useState } from 'react'; -import { ChartCard } from './index'; +import { ChartCard } from './common/card'; export const Cpu = () => { const { data: staticData } = useQuery({ queryKey: ['static'] }); const { data: dynamicData } = useQuery({ queryKey: ['dynamic'] }); - const [history, setHistory] = useState(new Array(Number(import.meta.env.CLIENT_GRAPH_STEPS)).fill([])); - - useEffect(() => { - if (dynamicData) { - setHistory(history => { - const newHistory = history.slice(1); - newHistory.push(dynamicData.cpu_usage); - - return newHistory; - }); - } - }, [dynamicData]); if (!staticData || !dynamicData) { return
; @@ -36,7 +23,7 @@ export const Cpu = () => { } domain={[0, 100]} formatOptions={{ prefix: false, units: '%' }} - data={history} + data={dynamicData.cpu_usage} total={total_cpus} /> ) diff --git a/src/client/components/chart-cards/disks.tsx b/src/client/components/chart-cards/disks.tsx index 035b373..1469ed0 100644 --- a/src/client/components/chart-cards/disks.tsx +++ b/src/client/components/chart-cards/disks.tsx @@ -1,21 +1,8 @@ import { useQuery } from '@tanstack/react-query'; -import { useEffect, useState } from 'react'; -import { ChartCard } from './index'; +import { ChartCard } from './common/card'; export const Disks = () => { const { data: dynamicData } = useQuery({ queryKey: ['dynamic'] }); - const [history, setHistory] = useState(new Array(Number(import.meta.env.CLIENT_GRAPH_STEPS)).fill([])); - - useEffect(() => { - if (dynamicData) { - setHistory(history => { - const newHistory = history.slice(1); - newHistory.push([dynamicData.disks.read, dynamicData.disks.write]); - - return newHistory; - }); - } - }, [dynamicData]); if (!dynamicData) { return
; @@ -25,12 +12,11 @@ export const Disks = () => { ); diff --git a/src/client/components/chart-cards/index.tsx b/src/client/components/chart-cards/index.tsx deleted file mode 100644 index 7d8b92d..0000000 --- a/src/client/components/chart-cards/index.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { Legend, type LegendProps } from '@/components/legend'; -import { getFillColor, getStrokeColor } from '@/utils/colors'; -import { type FormatOptions, formatValue } from '@/utils/format'; -import { type ReactNode, useMemo } from 'react'; -import { Area, AreaChart, CartesianGrid, ResponsiveContainer, XAxis, YAxis } from 'recharts'; -import type { AxisDomain } from 'recharts/types/util/types'; - -type Props = { - title: ReactNode; - subtitle?: ReactNode; - legend?: LegendProps; - formatOptions?: FormatOptions; - domain?: AxisDomain; - hueOffset?: number; - data: number[][]; - total: number; -}; - -export const ChartCard = ({ data, domain, legend, hueOffset = 0, title, subtitle, formatOptions, total }: Props) => { - const estimateTickWidth = useMemo( - () => ((formatOptions?.units?.length ?? 0) + (formatOptions?.prefix === false ? 0 : 2)) * 8, - [formatOptions], - ); - - return ( -
-

{title}

- - {subtitle} - - {legend && } - - - - - - formatValue(value, formatOptions)} - /> - - {Array.from({ length: total }).map((_, index) => ( - // biome-ignore lint/correctness/useJsxKeyInIterable: order irrelevant - - ))} - - - - -
- ); -}; diff --git a/src/client/components/chart-cards/memory.tsx b/src/client/components/chart-cards/memory.tsx index 297c5fc..78aea40 100644 --- a/src/client/components/chart-cards/memory.tsx +++ b/src/client/components/chart-cards/memory.tsx @@ -1,25 +1,13 @@ import { formatValue } from '@/utils/format'; import { useQuery } from '@tanstack/react-query'; -import { useEffect, useMemo, useState } from 'react'; -import { ChartCard } from './index'; +import { useMemo } from 'react'; +import { ChartCard } from './common/card'; const formatOptions = { units: 'B' }; export const Memory = () => { const { data: staticData } = useQuery({ queryKey: ['static'] }); const { data: dynamicData } = useQuery({ queryKey: ['dynamic'] }); - const [history, setHistory] = useState(new Array(Number(import.meta.env.CLIENT_GRAPH_STEPS)).fill([])); - - useEffect(() => { - if (dynamicData) { - setHistory(history => { - const newHistory = history.slice(1); - newHistory.push([dynamicData.mem_usage, dynamicData.swap_usage]); - - return newHistory; - }); - } - }, [dynamicData]); const formatedTotals = useMemo(() => { if (!staticData) { @@ -44,7 +32,7 @@ export const Memory = () => { }} domain={[0, Math.max(staticData.total_memory, staticData.total_swap)]} formatOptions={formatOptions} - data={history} + data={[dynamicData.mem_usage, dynamicData.swap_usage]} total={2} /> ); diff --git a/src/client/components/chart-cards/network.tsx b/src/client/components/chart-cards/network.tsx index 2c18246..349802a 100644 --- a/src/client/components/chart-cards/network.tsx +++ b/src/client/components/chart-cards/network.tsx @@ -1,21 +1,8 @@ import { useQuery } from '@tanstack/react-query'; -import { useEffect, useState } from 'react'; -import { ChartCard } from './index'; +import { ChartCard } from './common/card'; export const Network = () => { const { data: dynamicData } = useQuery({ queryKey: ['dynamic'] }); - const [history, setHistory] = useState(new Array(Number(import.meta.env.CLIENT_GRAPH_STEPS)).fill([])); - - useEffect(() => { - if (dynamicData) { - setHistory(history => { - const newHistory = history.slice(1); - newHistory.push([dynamicData.network.down, dynamicData.network.up]); - - return newHistory; - }); - } - }, [dynamicData]); if (!dynamicData) { return
; @@ -25,12 +12,11 @@ export const Network = () => { ); diff --git a/src/client/components/chart-cards/temps.tsx b/src/client/components/chart-cards/temps.tsx index 4285267..b963398 100644 --- a/src/client/components/chart-cards/temps.tsx +++ b/src/client/components/chart-cards/temps.tsx @@ -1,22 +1,9 @@ import { useQuery } from '@tanstack/react-query'; -import { useEffect, useState } from 'react'; -import { ChartCard } from './index'; +import { ChartCard } from './common/card'; export const Temps = () => { const { data: staticData } = useQuery({ queryKey: ['static'] }); const { data: dynamicData } = useQuery({ queryKey: ['dynamic'] }); - const [history, setHistory] = useState(new Array(Number(import.meta.env.CLIENT_GRAPH_STEPS)).fill([])); - - useEffect(() => { - if (dynamicData) { - setHistory(history => { - const newHistory = history.slice(1); - newHistory.push(dynamicData.temps); - - return newHistory; - }); - } - }, [dynamicData]); if (!staticData || !dynamicData) { return
; @@ -26,12 +13,11 @@ export const Temps = () => { Math.max(100, Math.ceil(1.25*max))]} + domain={[0, 100]} //(max: number) => Math.max(100, Math.ceil(1.25 * max))]} formatOptions={{ si: true, prefix: false, units: 'ÂșC' }} - data={history} + data={dynamicData.temps} total={staticData.components.length} /> );