Move history from query data to ref and avoid rerenders

This commit is contained in:
2024-05-08 19:45:36 +01:00
parent 9f14c53762
commit 66bef70cb8
12 changed files with 122 additions and 105 deletions

View File

@@ -1,4 +1,4 @@
import { Legend, type LegendProps } from '@/components/chart-cards/common/legend';
import { Legend, type LegendByDataProps, type LegendByKeyProps } from '@/components/chart-cards/common/legend';
import type { FormatOptions } from '@/utils/format';
import type { ReactNode } from 'react';
import { CanvasChart } from './chart';
@@ -6,7 +6,7 @@ import { CanvasChart } from './chart';
type Props = {
title: ReactNode;
subtitle?: ReactNode;
legend?: LegendProps;
legend?: Omit<LegendByKeyProps, 'dataKey'> | LegendByDataProps;
formatOptions?: FormatOptions;
domain?: [number, number];
hardDomain?: boolean;
@@ -22,7 +22,8 @@ export const ChartCard = ({ dataKey, legend, hueOffset = 0, title, subtitle, for
{subtitle}
{legend && (
{legend && (dataKey || 'values' in legend) && (
// @ts-expect-error: not inferable, but safe
<Legend
hueOffset={hueOffset}
formatOptions={formatOptions}

View File

@@ -1,8 +1,9 @@
import { historyAtom } from '@/atoms';
import { useAnimationFrame } from '@/hooks/use-animation-frame';
import { getFillColor, getStrokeColor } from '@/utils/colors';
import type { FormatOptions } from '@/utils/format';
import { useQuery } from '@tanstack/react-query';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useAtomValue } from 'jotai';
import { useEffect, useRef, useState } from 'react';
import './index.css';
import { YAxis } from './y-axis';
@@ -24,21 +25,9 @@ const xFromTimestamp = (timestamp: number, width: number) =>
export const CanvasChart = ({ total, hueOffset = 0, domain, hardDomain, dataKey, formatOptions }: Props) => {
const canvasRef = useRef<HTMLCanvasElement>(null!);
const { data: historyData } = useQuery<HistorySlice[]>({ queryKey: ['history'] });
const targetMax = useMemo(() => {
if (domain && hardDomain) {
return domain[1];
}
const historyMax = (historyData ?? []).reduce((max, slice) => Math.max(max, ...slice[dataKey]), 0);
if (!domain || historyMax > domain[1]) {
return historyMax;
}
return domain[1];
}, [domain, hardDomain, historyData, dataKey]);
const max = useRef(targetMax);
const history = useAtomValue(historyAtom);
const [lockedTargetMax, setLockedTargetMax] = useState(domain?.[1] ?? 0);
const max = useRef(lockedTargetMax);
const [width, setWidth] = useState(640);
const [height, setHeight] = useState(480);
@@ -61,10 +50,28 @@ export const CanvasChart = ({ total, hueOffset = 0, domain, hardDomain, dataKey,
// Redraw chart
useAnimationFrame(() => {
const ctx = canvasRef.current.getContext('2d');
if (!ctx || !historyData) {
if (!ctx || !history.current) {
return;
}
const targetMax = (() => {
if (domain && hardDomain) {
return domain[1];
}
const historyMax = history.current.maxes[dataKey as keyof typeof history.current.maxes] ?? 0;
if (!domain || historyMax > domain[1]) {
return historyMax;
}
return domain[1];
})();
if (lockedTargetMax !== targetMax) {
setLockedTargetMax(targetMax);
}
if (!hardDomain) {
max.current = (4 * max.current + targetMax) / 5;
}
@@ -96,16 +103,16 @@ export const CanvasChart = ({ total, hueOffset = 0, domain, hardDomain, dataKey,
ctx.beginPath();
ctx.moveTo(-xMargin, height);
ctx.lineTo(-xMargin, height - (height * (historyData[0][dataKey][i] ?? 0)) / max.current);
ctx.lineTo(-xMargin, height - (height * (history.current.data[0][dataKey][i] ?? 0)) / max.current);
for (const { timestamp, [dataKey]: values } of historyData) {
for (const { timestamp, [dataKey]: values } of history.current.data) {
const x = xFromTimestamp(timestamp, width);
const y = height - (height * (values[i] ?? 0)) / max.current;
ctx.lineTo(x, y);
}
ctx.lineTo(width + xMargin, height - (height * (historyData[0][dataKey][i] ?? 0)) / max.current);
ctx.lineTo(width + xMargin, height - (height * (history.current.data[0][dataKey][i] ?? 0)) / max.current);
ctx.lineTo(width + xMargin, height);
if (type === 'fill') ctx.fill();
@@ -121,7 +128,7 @@ export const CanvasChart = ({ total, hueOffset = 0, domain, hardDomain, dataKey,
return (
<div className='chart'>
<YAxis max={targetMax} formatOptions={formatOptions} />
<YAxis max={lockedTargetMax} formatOptions={formatOptions} />
<div className='canvas-wrapper'>
<canvas ref={canvasRef} width={width} height={height} />

View File

@@ -1,25 +1,24 @@
import { getTextColor } from '@/utils/colors';
import { type FormatOptions, formatValue } from '@/utils/format';
import { useQuery } from '@tanstack/react-query';
import { useMemo } from 'react';
import { memo, useMemo } from 'react';
import './index.css';
type LegendCommonProps = {
export type LegendCommonProps = {
labels: string[];
formatOptions?: FormatOptions;
hueOffset?: number;
};
type LegendByKeyProps = LegendCommonProps & {
export type LegendByKeyProps = LegendCommonProps & {
dataKey: Exclude<keyof HistorySlice, 'timestamp'>;
totals?: number[];
};
type LegendByDataProps = LegendCommonProps & {
export type LegendByDataProps = LegendCommonProps & {
values: string[] | number[];
};
export type LegendProps = LegendByDataProps | LegendByKeyProps;
export const LegendByData = ({ labels, formatOptions, hueOffset = 0, values }: LegendByDataProps) => (
<div className='legend-wrapper'>
{labels.map((label, index) => (
@@ -36,15 +35,26 @@ export const LegendByData = ({ labels, formatOptions, hueOffset = 0, values }: L
</div>
);
export const LegendByKey = ({ dataKey, ...rest }: LegendByKeyProps) => {
const { data: historyData } = useQuery<HistorySlice[]>({ queryKey: ['history'] });
const values = useMemo(() => historyData?.at(-1)?.[dataKey], [historyData, dataKey]);
export const LegendByKey = memo(({ dataKey, totals, formatOptions, ...rest }: LegendByKeyProps) => {
const { data: dynamicData } = useQuery<HistorySlice>({ queryKey: ['dynamic'] });
const values = useMemo(
() =>
dynamicData?.[dataKey]?.map((value, index) => {
if (totals) {
return `${formatValue(value, formatOptions)} / ${formatValue(totals[index], formatOptions)}`;
}
return formatValue(value, formatOptions);
}),
[dynamicData, dataKey, formatOptions, totals],
);
if (values) {
return <LegendByData {...rest} values={values} />;
}
};
});
export const Legend = (props: LegendProps) => {
export const Legend = (props: LegendByDataProps | LegendByKeyProps) => {
if ('values' in props) {
return <LegendByData {...props} />;
}

View File

@@ -3,30 +3,25 @@ import { ChartCard } from './common/card';
export const Cpu = () => {
const { data: staticData } = useQuery<StaticData>({ queryKey: ['static'] });
const { data: historyData } = useQuery<HistorySlice[]>({ queryKey: ['history'] });
if (!staticData || !historyData) {
if (!staticData) {
return <div />;
}
const total_cpus = historyData[0].cpu.length;
return (
!!total_cpus && (
<ChartCard
title='CPU'
subtitle={
<h3>
{staticData.cpu.brand}
<small>{` (${total_cpus} threads)`}</small>
</h3>
}
domain={[0, 100]}
hardDomain
formatOptions={{ prefix: false, units: '%' }}
dataKey={'cpu'}
total={total_cpus}
/>
)
<ChartCard
title='CPU'
subtitle={
<h3>
{staticData.cpu.brand}
<small>{` (${staticData.cpu.threads} threads)`}</small>
</h3>
}
domain={[0, 100]}
hardDomain
formatOptions={{ prefix: false, units: '%' }}
dataKey={'cpu'}
total={staticData.cpu.threads}
/>
);
};

View File

@@ -1,20 +1,13 @@
import { siAtom } from '@/atoms';
import { useQuery } from '@tanstack/react-query';
import { useAtomValue } from 'jotai';
import { ChartCard } from './common/card';
export const Disks = () => {
const { data: historyData } = useQuery<HistorySlice[]>({ queryKey: ['history'] });
const isSi = useAtomValue(siAtom);
if (!historyData) {
return <div />;
}
return (
<ChartCard
title='Disk activity'
// @ts-expect-error: write a better union later
legend={{
labels: ['Read', 'Write'],
}}

View File

@@ -1,26 +1,13 @@
import { siAtom } from '@/atoms';
import { formatValue } from '@/utils/format';
import { useQuery } from '@tanstack/react-query';
import { useAtomValue } from 'jotai';
import { useMemo } from 'react';
import { ChartCard } from './common/card';
export const Memory = () => {
const { data: staticData } = useQuery<StaticData>({ queryKey: ['static'] });
const { data: historyData } = useQuery<HistorySlice[]>({ queryKey: ['history'] });
const isSi = useAtomValue(siAtom);
const formatOptions = { units: 'B', ...(isSi && { si: true }) };
const formatedTotals = useMemo(() => {
if (!staticData) {
return [];
}
return [formatValue(staticData.total_memory, formatOptions), formatValue(staticData.total_swap, formatOptions)];
}, [staticData, formatOptions]);
const last = useMemo(() => historyData?.at(-1), [historyData]);
if (!staticData || !historyData || !last) {
if (!staticData) {
return <div />;
}
@@ -28,15 +15,12 @@ export const Memory = () => {
<ChartCard
title='Memory'
legend={{
values: [
`${formatValue(last.mem[0], formatOptions)} / ${formatedTotals[0]}`,
`${formatValue(last.mem[1], formatOptions)} / ${formatedTotals[1]}`,
],
totals: [staticData.total_memory, staticData.total_swap],
labels: ['Memory', 'Swap'],
}}
domain={[0, Math.max(staticData.total_memory, staticData.total_swap)]}
hardDomain
formatOptions={formatOptions}
formatOptions={{ units: 'B', ...(isSi && { si: true }) }}
dataKey={'mem'}
total={2}
/>

View File

@@ -1,20 +1,13 @@
import { siAtom } from '@/atoms';
import { useQuery } from '@tanstack/react-query';
import { useAtomValue } from 'jotai';
import { ChartCard } from './common/card';
export const Network = () => {
const { data: historyData } = useQuery<HistorySlice[]>({ queryKey: ['history'] });
const isSi = useAtomValue(siAtom);
if (!historyData) {
return <div />;
}
return (
<ChartCard
title='Network'
// @ts-expect-error: write a better union later
legend={{
labels: ['Down', 'Up'],
}}

View File

@@ -3,16 +3,14 @@ import { ChartCard } from './common/card';
export const Temps = () => {
const { data: staticData } = useQuery<StaticData>({ queryKey: ['static'] });
const { data: historyData } = useQuery<HistorySlice[]>({ queryKey: ['history'] });
if (!staticData || !historyData) {
if (!staticData) {
return <div />;
}
return (
<ChartCard
title='Temperatures'
// @ts-expect-error: write a better union later
legend={{
labels: staticData.components,
}}