sendou.ink/app/components/Chart.tsx
Kalle fef1ffc955
Design refresh + a bunch of stuff (#2864)
Co-authored-by: hfcRed <hfcred@gmx.net>
2026-03-19 17:51:42 +02:00

159 lines
3.7 KiB
TypeScript

import clsx from "clsx";
import * as React from "react";
import { type AxisOptions, Chart as ReactChart } from "react-charts";
import type { TooltipRendererProps } from "react-charts/types/components/TooltipRenderer";
import { useTranslation } from "react-i18next";
import { Theme, useTheme } from "~/features/theme/core/provider";
import { useIsMounted } from "~/hooks/useIsMounted";
import { useTimeFormat } from "~/hooks/useTimeFormat";
import styles from "./Chart.module.css";
export default function Chart({
options,
containerClassName,
headerSuffix,
valueSuffix,
xAxis,
}: {
options: [
{ label: string; data: Array<{ primary: Date; secondary: number }> },
];
containerClassName?: string;
headerSuffix?: string;
valueSuffix?: string;
xAxis: "linear" | "localTime";
}) {
const { i18n } = useTranslation();
const theme = useTheme();
const isMounted = useIsMounted();
const primaryAxis = React.useMemo<
AxisOptions<(typeof options)[number]["data"][number]>
>(
// @ts-expect-error - some weirdness here but maybe not worth fixing as the whole library needs to be replaced (it is unmaintained/deprecated)
() => ({
getValue: (datum) => datum.primary,
scaleType: xAxis,
shouldNice: false,
formatters: {
scale: (val: any) => {
if (val instanceof Date) {
return val.toLocaleDateString(i18n.language, {
day: "numeric",
month: "numeric",
});
}
return val;
},
},
}),
[i18n.language, xAxis],
);
const secondaryAxes = React.useMemo<
AxisOptions<(typeof options)[number]["data"][number]>[]
>(
() => [
{
getValue: (datum) => datum.secondary,
},
],
[],
);
if (!isMounted) {
return <div className={clsx(styles.container, containerClassName)} />;
}
return (
<div className={clsx(styles.container, containerClassName)}>
<ReactChart
options={{
data: options,
tooltip: {
render: (props) => (
<ChartTooltip
{...props}
headerSuffix={headerSuffix}
valueSuffix={valueSuffix}
/>
),
},
primaryCursor: false,
secondaryCursor: false,
primaryAxis,
secondaryAxes,
dark: theme.htmlThemeClass === Theme.DARK,
defaultColors: [
"var(--color-text-accent)",
"var(--color-accent)",
"var(--color-info)",
],
}}
/>
</div>
);
}
interface ChartTooltipProps extends TooltipRendererProps<any> {
headerSuffix?: string;
valueSuffix?: string;
}
function ChartTooltip({
focusedDatum,
headerSuffix = "",
valueSuffix = "",
}: ChartTooltipProps) {
const { formatDate } = useTimeFormat();
const dataPoints = focusedDatum?.interactiveGroup ?? [];
const header = () => {
const primaryValue = dataPoints[0]?.primaryValue;
if (!primaryValue) return null;
if (primaryValue instanceof Date) {
return formatDate(primaryValue, {
weekday: "short",
day: "numeric",
month: "long",
});
}
return primaryValue;
};
return (
<div className={styles.tooltip}>
<h3 className="text-center text-md">
{header()}
{headerSuffix}
</h3>
{dataPoints.map((dataPoint, index) => {
const color = dataPoint.style?.fill ?? "var(--color-accent)";
return (
<div key={index} className="stack horizontal items-center sm">
<div
className={clsx(styles.dot, {
[styles.dotFocused]:
focusedDatum?.seriesId === dataPoint.seriesId,
})}
style={{
"--dot-color": color,
"--dot-color-outline": color.replace(")", "-transparent)"),
}}
/>
<div>{dataPoint.originalSeries.label}</div>
<div className={styles.tooltipValue}>
{dataPoint.secondaryValue}
{valueSuffix}
</div>
</div>
);
})}
</div>
);
}