Move history from query data to ref and avoid rerenders
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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'],
|
||||
}}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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'],
|
||||
}}
|
||||
|
||||
@@ -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,
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user