diff --git a/component/src/charts/base-chart.tsx b/component/src/charts/base-chart.tsx
index 17589360..eb91db0b 100644
--- a/component/src/charts/base-chart.tsx
+++ b/component/src/charts/base-chart.tsx
@@ -9,6 +9,8 @@ import {
DataZoomComponent,
AriaComponent,
RadarComponent,
+ MarkLineComponent,
+ GraphicComponent,
} from "echarts/components";
import { CanvasRenderer } from "echarts/renderers";
import type { EChartsOption } from "echarts";
@@ -35,6 +37,8 @@ echarts.use([
DataZoomComponent,
AriaComponent,
RadarComponent,
+ MarkLineComponent,
+ GraphicComponent,
CanvasRenderer,
]);
diff --git a/component/src/charts/chart-utils.ts b/component/src/charts/chart-utils.ts
index 7fc9ba74..49ae5724 100644
--- a/component/src/charts/chart-utils.ts
+++ b/component/src/charts/chart-utils.ts
@@ -4,6 +4,59 @@ import { resolveThresholdColor } from "./color-threshold";
import type { StylingRule } from "./styling-rule";
import { resolveStylingRuleColor } from "./styling-rule";
+// ---------------------------------------------------------------------------
+// Reference lines (markLine)
+// ---------------------------------------------------------------------------
+
+export interface ReferenceLine {
+ value: number;
+ label?: string;
+ color?: string;
+}
+
+/**
+ * Parse a JSON string of reference lines. Returns empty array on
+ * invalid input or missing values.
+ */
+export function parseReferenceLines(input: string | undefined): ReferenceLine[] {
+ if (!input) return [];
+ try {
+ const parsed = JSON.parse(input);
+ if (!Array.isArray(parsed)) return [];
+ return parsed.filter(
+ (item: unknown): item is ReferenceLine =>
+ typeof item === "object" &&
+ item !== null &&
+ "value" in item &&
+ typeof (item as ReferenceLine).value === "number",
+ );
+ } catch {
+ return [];
+ }
+}
+
+/**
+ * Build ECharts markLine data from reference lines.
+ */
+export function buildMarkLineFromRefs(lines: ReferenceLine[]) {
+ if (!lines.length) return undefined;
+ return {
+ silent: true,
+ symbol: "none",
+ data: lines.map((line) => ({
+ yAxis: line.value,
+ label: {
+ formatter: line.label ?? String(line.value),
+ position: "insideEndTop" as const,
+ },
+ lineStyle: {
+ color: line.color ?? "#888",
+ type: "dashed" as const,
+ },
+ })),
+ };
+}
+
/** Detect whether the document is currently in dark mode. */
export function isDark(): boolean {
if (typeof document === "undefined") return false;
diff --git a/component/src/charts/line-chart.tsx b/component/src/charts/line-chart.tsx
index 6db021d3..b4d83b10 100644
--- a/component/src/charts/line-chart.tsx
+++ b/component/src/charts/line-chart.tsx
@@ -9,6 +9,8 @@ import {
resolveShowLegend,
buildCompactGrid,
resolveItemColor,
+ parseReferenceLines,
+ buildMarkLineFromRefs,
} from "./chart-utils";
import { parseColorThresholds } from "./color-threshold";
import type { StylingRule } from "./styling-rule";
@@ -34,6 +36,8 @@ export interface LineChartProps extends Omit
{
showGridLines?: boolean;
/** Use stepped line style */
stepped?: boolean;
+ /** JSON string of reference lines: [{ value, label?, color? }] */
+ referenceLines?: string;
/** @deprecated Use stylingRules instead. JSON string of thresholds */
colorThresholds?: string;
/** Rule-based styling rules */
@@ -62,6 +66,7 @@ function LineChart({
lineWidth = 2,
showGridLines = true,
stepped = false,
+ referenceLines: referenceLinesJson,
colorThresholds,
stylingRules,
paramValues,
@@ -76,6 +81,8 @@ function LineChart({
const seriesKeys = Object.keys(data[0]).filter((k) => k !== "x");
const effectiveShowLegend = resolveShowLegend(showLegend, seriesKeys.length, hideLegend);
const thresholds = stylingRules ? [] : parseColorThresholds(colorThresholds ?? "");
+ const refLines = parseReferenceLines(referenceLinesJson);
+ const markLine = buildMarkLineFromRefs(refLines);
return {
tooltip: { trigger: "axis" },
@@ -100,7 +107,7 @@ function LineChart({
axisLabel: { show: !compact },
splitLine: { show: showGridLines },
},
- series: seriesKeys.map((key) => {
+ series: seriesKeys.map((key, idx) => {
let lastValue: number | undefined;
for (let i = data.length - 1; i >= 0; i -= 1) {
const candidate = data[i][key];
@@ -123,10 +130,12 @@ function LineChart({
showSymbol: showPoints,
areaStyle: area ? {} : undefined,
emphasis: seriesKeys.length > 1 ? { focus: "series" as const } : {},
+ // Attach reference lines to the first series only
+ ...(idx === 0 && markLine ? { markLine } : {}),
};
}),
};
- }, [data, xAxisLabel, yAxisLabel, smooth, area, showLegend, showPoints, lineWidth, showGridLines, stepped, colorThresholds, stylingRules, paramValues, compact, hideLegend]);
+ }, [data, xAxisLabel, yAxisLabel, smooth, area, showLegend, showPoints, lineWidth, showGridLines, stepped, referenceLinesJson, colorThresholds, stylingRules, paramValues, compact, hideLegend]);
return (
diff --git a/component/src/components/composed/chart-options-schema.ts b/component/src/components/composed/chart-options-schema.ts
index 384d21eb..bb228f0a 100644
--- a/component/src/components/composed/chart-options-schema.ts
+++ b/component/src/components/composed/chart-options-schema.ts
@@ -33,6 +33,7 @@ const barOptions: ChartOptionDef[] = [
{ key: "xAxisLabel", label: "X-Axis Label", type: "text", default: "", category: "Labels", description: "Custom label displayed below the horizontal axis." },
{ key: "yAxisLabel", label: "Y-Axis Label", type: "text", default: "", category: "Labels", description: "Custom label displayed beside the vertical axis." },
{ key: "showGridLines", label: "Show Grid Lines", type: "boolean", default: true, category: "Style", description: "Show faint horizontal reference lines behind the bars." },
+ { key: "referenceLines", label: "Reference Lines (JSON)", type: "text", default: "", category: "Annotations", description: 'Horizontal reference lines as JSON: [{"value":50,"label":"Target","color":"#ff0000"}]' },
];
const lineOptions: ChartOptionDef[] = [
@@ -45,6 +46,7 @@ const lineOptions: ChartOptionDef[] = [
{ key: "xAxisLabel", label: "X-Axis Label", type: "text", default: "", category: "Labels", description: "Custom label displayed below the horizontal axis." },
{ key: "yAxisLabel", label: "Y-Axis Label", type: "text", default: "", category: "Labels", description: "Custom label displayed beside the vertical axis." },
{ key: "showLegend", label: "Show Legend", type: "boolean", default: true, category: "Labels", description: "Show the chart legend identifying each data series." },
+ { key: "referenceLines", label: "Reference Lines (JSON)", type: "text", default: "", category: "Annotations", description: 'Horizontal reference lines as JSON: [{"value":50,"label":"Target","color":"#ff0000"}]' },
];
const pieOptions: ChartOptionDef[] = [
diff --git a/component/vitest.setup.ts b/component/vitest.setup.ts
index f8443727..48e07097 100644
--- a/component/vitest.setup.ts
+++ b/component/vitest.setup.ts
@@ -41,6 +41,8 @@ vi.mock("echarts/components", () => ({
DataZoomComponent: vi.fn(),
AriaComponent: vi.fn(),
RadarComponent: vi.fn(),
+ MarkLineComponent: vi.fn(),
+ GraphicComponent: vi.fn(),
}));
vi.mock("echarts/renderers", () => ({