Files
triggerdotdev--trigger.dev/apps/webapp/app/components/primitives/charts/ChartLine.tsx
T
2026-07-13 13:32:57 +08:00

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>
);
}