mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-03-21 18:04:39 -05:00
159 lines
3.7 KiB
TypeScript
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>
|
|
);
|
|
}
|