${commitShort}
· workflow ${meta.workflow_id} · branch ${meta.branch}`,
+ ].join("");
+ }) as any;
+
+ function resetSelection() {
+ setSelectedSeriesIdx(null);
+ setLeftIdx(null);
+ setRightIdx(null);
+ }
+
+ function handlePointClick(seriesIndex: number, dataIndex: number) {
+ if (!selectMode) return;
+ // Lock to a series on first click
+ if (selectedSeriesIdx == null) {
+ setSelectedSeriesIdx(seriesIndex);
+ setLeftIdx(dataIndex);
+ setRightIdx(null);
+ return;
+ }
+
+ // Must stay within the locked series
+ if (seriesIndex !== selectedSeriesIdx) return;
+
+ if (leftIdx == null) {
+ setLeftIdx(dataIndex);
+ } else if (rightIdx == null) {
+ // keep chronological order L <= R
+ if (dataIndex < leftIdx) {
+ setRightIdx(leftIdx);
+ setLeftIdx(dataIndex);
+ } else {
+ setRightIdx(dataIndex);
+ }
+ } else {
+ // replace the closer one
+ const dL = Math.abs(dataIndex - leftIdx);
+ const dR = Math.abs(dataIndex - rightIdx);
+ if (dL <= dR) setLeftIdx(dataIndex);
+ else setRightIdx(dataIndex);
+ }
+ }
+
+ // Build line series first (indices 0..N-1 map to logical timeseries)
+ const lineSeries: echarts.SeriesOption[] = useMemo(() => {
+ return seriesDatas.map((data, idx) => {
+ const isSelected = selectedSeriesIdx === idx;
+ const mlData: any[] = [];
+
+ const isOther = selectedSeriesIdx != null && !isSelected;
+ const baseOpacity = selectedSeriesIdx == null ? 1 : isSelected ? 1 : 0.12;
+ if (isSelected && leftIdx != null && data[leftIdx]) {
+ mlData.push({
+ xAxis: data[leftIdx].value[0],
+ label: { formatter: "L", position: "insideEndTop" },
+ lineStyle: { type: "solid", width: 2 },
+ });
+ }
+
+ if (isSelected && rightIdx != null && data[rightIdx]) {
+ mlData.push({
+ xAxis: data[rightIdx].value[0],
+ label: { formatter: "R", position: "insideEndTop" },
+ lineStyle: { type: "solid", width: 2 },
+ });
+ }
+
+ return {
+ name: timeseries[idx]?.legend_name ?? `Series ${idx + 1}`,
+ type: "line",
+ showSymbol: true,
+ symbolSize: 4,
+ data,
+ silent: !!isOther,
+ lineStyle: {
+ opacity: baseOpacity, // line transparency
+ },
+ itemStyle: {
+ opacity: baseOpacity, // dot transparency
+ },
+ ...(mlData.length
+ ? { markLine: { data: mlData, symbol: "none" } }
+ : {}),
+ } as echarts.SeriesOption;
+ });
+ }, [seriesDatas, timeseries, selectedSeriesIdx, leftIdx, rightIdx]);
+
+ // Highlight overlays appended after all lines
+ const overlaySeries: echarts.SeriesOption[] = useMemo(() => {
+ if (selectedSeriesIdx == null) return [];
+ const data = seriesDatas[selectedSeriesIdx] || [];
+ const sel: any[] = [];
+ if (leftIdx != null && data[leftIdx]) sel.push(data[leftIdx]);
+ if (rightIdx != null && data[rightIdx]) sel.push(data[rightIdx]);
+ if (!sel.length) return [];
+ return [
+ {
+ name: `sel-${selectedSeriesIdx}`,
+ type: "effectScatter",
+ z: 5,
+ rippleEffect: { scale: 2.1 },
+ symbolSize: 4,
+ data: sel,
+ } as echarts.SeriesOption,
+ ];
+ }, [seriesDatas, selectedSeriesIdx, leftIdx, rightIdx]);
+
+ const legendSelected = useMemo(() => {
+ if (selectedSeriesIdx == null) return undefined; // 不锁定时不干预 legend
+ const m: Record{leftMeta.commit.slice(0, 7)}
+ >
+ ) : (
+ —
+ )}
+ {rightMeta.commit.slice(0, 7)}
+ >
+ ) : (
+ —
+ )}
+