sendou.ink/app/components/Chart.tsx
Kalle ef2d3779ec
Some checks failed
E2E Tests / e2e (push) Has been cancelled
Tests and checks on push / run-checks-and-tests (push) Has been cancelled
Updates translation progress / update-translation-progress-issue (push) Has been cancelled
Fix a bunch of TODOs (#2648)
2025-11-23 16:34:18 +02:00

160 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";
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("chart__container", containerClassName)} />;
}
return (
<div className={clsx("chart__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(--theme)",
"var(--theme-secondary)",
"var(--theme-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="chart__tooltip">
<h3 className="text-center text-md">
{header()}
{headerSuffix}
</h3>
{dataPoints.map((dataPoint, index) => {
const color = dataPoint.style?.fill ?? "var(--theme)";
return (
<div key={index} className="stack horizontal items-center sm">
<div
className={clsx("chart__dot", {
chart__dot__focused:
focusedDatum?.seriesId === dataPoint.seriesId,
})}
style={{
"--dot-color": color,
"--dot-color-outline": color.replace(")", "-transparent)"),
}}
/>
<div className="chart__tooltip__label">
{dataPoint.originalSeries.label}
</div>
<div className="chart__tooltip__value">
{dataPoint.secondaryValue}
{valueSuffix}
</div>
</div>
);
})}
</div>
);
}