240 lines
6.8 KiB
TypeScript
240 lines
6.8 KiB
TypeScript
import {
|
|
Area,
|
|
AreaChart,
|
|
CartesianGrid,
|
|
Line,
|
|
LineChart,
|
|
XAxis,
|
|
YAxis,
|
|
type XAxisProps,
|
|
type YAxisProps,
|
|
} from "recharts";
|
|
import { ChartTooltip, ChartTooltipContent } from "~/components/primitives/charts/Chart";
|
|
import { CHART_MARGIN } from "./ChartBar";
|
|
import { useChartContext } from "./ChartContext";
|
|
import { ChartLineInvalid, ChartLineLoading, ChartLineNoData } from "./ChartLoading";
|
|
import { useHasNoData } from "./ChartRoot";
|
|
import { defaultYAxisTickFormatter, useYAxisWidth } from "./useYAxisWidth";
|
|
// Legend is now rendered by ChartRoot outside the chart container
|
|
|
|
type CurveType =
|
|
| "basis"
|
|
| "basisClosed"
|
|
| "basisOpen"
|
|
| "linear"
|
|
| "linearClosed"
|
|
| "natural"
|
|
| "monotoneX"
|
|
| "monotoneY"
|
|
| "monotone"
|
|
| "step"
|
|
| "stepBefore"
|
|
| "stepAfter";
|
|
|
|
// ============================================================================
|
|
// COMPOUND COMPONENT API
|
|
// ============================================================================
|
|
|
|
export type ChartLineRendererProps = {
|
|
/** Line curve type */
|
|
lineType?: CurveType;
|
|
/** Custom X-axis props to merge with defaults */
|
|
xAxisProps?: Partial<XAxisProps>;
|
|
/** Custom Y-axis props to merge with defaults */
|
|
yAxisProps?: Partial<YAxisProps>;
|
|
/** Render as stacked area chart instead of line chart */
|
|
stacked?: boolean;
|
|
/** Custom tooltip label formatter */
|
|
tooltipLabelFormatter?: (label: string, payload: any[]) => string;
|
|
/** Optional formatter for numeric tooltip values (e.g. bytes, duration) */
|
|
tooltipValueFormatter?: (value: number) => string;
|
|
/** Width injected by ResponsiveContainer */
|
|
width?: number;
|
|
/** Height injected by ResponsiveContainer */
|
|
height?: number;
|
|
};
|
|
|
|
/**
|
|
* Line chart renderer for the compound component system.
|
|
* Must be used within a Chart.Root.
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* <Chart.Root config={config} data={data} dataKey="day">
|
|
* <Chart.Line type="step" />
|
|
* <Chart.Legend simple />
|
|
* </Chart.Root>
|
|
* ```
|
|
*/
|
|
export function ChartLineRenderer({
|
|
lineType = "step",
|
|
xAxisProps: xAxisPropsProp,
|
|
yAxisProps: yAxisPropsProp,
|
|
stacked = false,
|
|
tooltipLabelFormatter,
|
|
tooltipValueFormatter,
|
|
width,
|
|
height,
|
|
}: ChartLineRendererProps) {
|
|
const {
|
|
config,
|
|
data,
|
|
dataKey,
|
|
dataKeys: _dataKeys,
|
|
visibleSeries,
|
|
state,
|
|
highlight,
|
|
setActivePayload,
|
|
showLegend,
|
|
} = useChartContext();
|
|
const hasNoData = useHasNoData();
|
|
const yAxisTickFormatter = yAxisPropsProp?.tickFormatter ?? defaultYAxisTickFormatter;
|
|
const computedYAxisWidth = useYAxisWidth(data, visibleSeries, yAxisTickFormatter);
|
|
|
|
// Render loading/error states
|
|
if (state === "loading") {
|
|
return <ChartLineLoading />;
|
|
} else if (state === "noData" || hasNoData) {
|
|
return <ChartLineNoData />;
|
|
} else if (state === "invalid") {
|
|
return <ChartLineInvalid />;
|
|
}
|
|
|
|
// Get the x-axis ticks based on tooltip state
|
|
const xAxisTicks =
|
|
highlight.tooltipActive && data.length > 2
|
|
? [data[0]?.[dataKey], data[data.length - 1]?.[dataKey]]
|
|
: undefined;
|
|
|
|
const xAxisConfig = {
|
|
dataKey,
|
|
tickLine: false,
|
|
axisLine: false,
|
|
tickMargin: 10,
|
|
ticks: xAxisTicks,
|
|
interval: "preserveStartEnd" as const,
|
|
tick: {
|
|
fill: "var(--color-text-dimmed)",
|
|
fontSize: 11,
|
|
style: { fontVariantNumeric: "tabular-nums" },
|
|
},
|
|
...xAxisPropsProp,
|
|
};
|
|
|
|
const yAxisConfig = {
|
|
axisLine: false,
|
|
tickLine: false,
|
|
tickMargin: 8,
|
|
width: computedYAxisWidth,
|
|
tick: {
|
|
fill: "var(--color-text-dimmed)",
|
|
fontSize: 11,
|
|
style: { fontVariantNumeric: "tabular-nums" },
|
|
},
|
|
tickFormatter: yAxisTickFormatter,
|
|
...yAxisPropsProp,
|
|
};
|
|
|
|
// Handle mouse leave to also reset highlight
|
|
const handleMouseLeave = () => {
|
|
highlight.setTooltipActive(false);
|
|
highlight.reset();
|
|
};
|
|
|
|
// Render stacked area chart if stacked prop is true
|
|
if (stacked && visibleSeries.length > 1) {
|
|
return (
|
|
<AreaChart
|
|
data={data}
|
|
width={width}
|
|
height={height}
|
|
stackOffset="none"
|
|
margin={CHART_MARGIN}
|
|
onMouseMove={(e: any) => {
|
|
if (e?.activePayload?.length) {
|
|
setActivePayload(e.activePayload, e.activeTooltipIndex);
|
|
highlight.setTooltipActive(true);
|
|
} else {
|
|
highlight.setTooltipActive(false);
|
|
}
|
|
}}
|
|
onMouseLeave={handleMouseLeave}
|
|
>
|
|
<CartesianGrid vertical={false} stroke="var(--color-grid-bright)" strokeDasharray="3 3" />
|
|
<XAxis {...xAxisConfig} />
|
|
<YAxis {...yAxisConfig} />
|
|
{/* When legend is shown below, render tooltip with cursor only (no content popup) */}
|
|
<ChartTooltip
|
|
cursor={{ stroke: "rgba(255, 255, 255, 0.1)", strokeWidth: 1 }}
|
|
content={
|
|
showLegend ? (
|
|
() => null
|
|
) : (
|
|
<ChartTooltipContent indicator="line" valueFormatter={tooltipValueFormatter} />
|
|
)
|
|
}
|
|
labelFormatter={tooltipLabelFormatter}
|
|
/>
|
|
{/* Note: Legend is now rendered by ChartRoot outside the chart container */}
|
|
{visibleSeries.map((key) => (
|
|
<Area
|
|
key={key}
|
|
type={lineType}
|
|
dataKey={key}
|
|
stroke={config[key]?.color}
|
|
fill={config[key]?.color}
|
|
fillOpacity={0.6}
|
|
strokeWidth={1}
|
|
stackId="stack"
|
|
isAnimationActive={false}
|
|
/>
|
|
))}
|
|
</AreaChart>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<LineChart
|
|
accessibilityLayer
|
|
data={data}
|
|
width={width}
|
|
height={height}
|
|
margin={CHART_MARGIN}
|
|
onMouseMove={(e: any) => {
|
|
if (e?.activePayload?.length) {
|
|
setActivePayload(e.activePayload, e.activeTooltipIndex);
|
|
highlight.setTooltipActive(true);
|
|
} else {
|
|
highlight.setTooltipActive(false);
|
|
}
|
|
}}
|
|
onMouseLeave={handleMouseLeave}
|
|
>
|
|
<CartesianGrid vertical={false} stroke="var(--color-grid-bright)" strokeDasharray="3 3" />
|
|
<XAxis {...xAxisConfig} />
|
|
<YAxis {...yAxisConfig} />
|
|
{/* When legend is shown below, render tooltip with cursor only (no content popup) */}
|
|
<ChartTooltip
|
|
cursor={{ stroke: "rgba(255, 255, 255, 0.1)", strokeWidth: 1 }}
|
|
content={
|
|
showLegend ? () => null : <ChartTooltipContent valueFormatter={tooltipValueFormatter} />
|
|
}
|
|
labelFormatter={tooltipLabelFormatter}
|
|
/>
|
|
{/* Note: Legend is now rendered by ChartRoot outside the chart container */}
|
|
{visibleSeries.map((key) => (
|
|
<Line
|
|
key={key}
|
|
dataKey={key}
|
|
type={lineType}
|
|
stroke={config[key]?.color}
|
|
strokeWidth={1}
|
|
dot={{ r: 1.5, fill: config[key]?.color, strokeWidth: 0 }}
|
|
activeDot={{ r: 4 }}
|
|
isAnimationActive={false}
|
|
/>
|
|
))}
|
|
</LineChart>
|
|
);
|
|
}
|