From 01ed4527d379737160217faef26dbfc8dafa7bd5 Mon Sep 17 00:00:00 2001 From: windpacer Date: Tue, 26 May 2026 09:57:21 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20=ED=8A=B8=EB=A0=8C=EB=93=9C=20?= =?UTF-8?q?=EB=93=80=EC=96=BC=EC=BB=A4=EC=84=9C=EB=A5=BC=20graphic=20?= =?UTF-8?q?=EC=88=98=EB=8F=99=EB=B0=B0=EC=B9=98=20=E2=86=92=20=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=96=B4(markLine/markPoint)=EB=A1=9C=20=EC=9E=AC?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TR_LAYERS에 cursor 레이어(global) 추가, cursorMode일 때만 활성 - 기존 graphic group 수동 픽셀배치(setOption replaceMerge) 제거 → layerCursor가 A/B 마커(markPoint)·연결선·Δ라벨(markLine)을 데이터좌표로 반환, 픽셀변환은 ECharts에 위임 - trRenderCursor()는 trRender() 위임으로 단순화 - 기울기 단위 보정: /s → /min (분당 변화율) Co-Authored-By: Claude Opus 4.7 --- src/Web/wwwroot/js/trend.js | 106 +++++++++--------------------------- 1 file changed, 26 insertions(+), 80 deletions(-) diff --git a/src/Web/wwwroot/js/trend.js b/src/Web/wwwroot/js/trend.js index b16e5a0..b7dc3d3 100644 --- a/src/Web/wwwroot/js/trend.js +++ b/src/Web/wwwroot/js/trend.js @@ -39,6 +39,7 @@ const TR_LAYERS = [ { id:'events', global: layerEvents, when: s => s.layers.events }, { id:'limits', perSeries: layerLimits, when: s => s.layers.limits }, { id:'runstate', global: layerRunState, when: s => s.layers.runstate }, + { id:'cursor', global: layerCursor, when: s => s.cursorMode }, ]; /* ── 차트 초기화 ─────────────────────────────────────────────── */ @@ -541,88 +542,33 @@ function trToggleCursorMode() { if (!trState.cursorMode) { trState.cursor = { a: null, b: null }; trRenderCursor(); } } -function trRenderCursor() { - if (!trChart) return; - const { a, b } = trState.cursor; - let data = []; +function trRenderCursor() { trRender(); } // 커서는 레이어(layerCursor)로 그린다 + +// 듀얼 커서 레이어 (global) — A/B 점 + 연결선 + Δ 라벨. 데이터좌표를 ECharts가 자동 픽셀변환. +function layerCursor(s) { + const { a, b } = s.cursor; + if (!a && !b) return null; + const mp = []; + if (a) mp.push({ coord: [a.ms, a.val], symbol: 'circle', symbolSize: 10, itemStyle: { color: '#f59e0b' }, + label: { show: true, formatter: 'A', position: 'top', color: '#f59e0b', fontSize: 11, fontWeight: 'bold' } }); + if (b) mp.push({ coord: [b.ms, b.val], symbol: 'circle', symbolSize: 10, itemStyle: { color: '#3b82f6' }, + label: { show: true, formatter: 'B', position: 'top', color: '#3b82f6', fontSize: 11, fontWeight: 'bold' } }); + let ml; if (a && b) { - const dtMs = b.ms - a.ms; - const dtS = dtMs / 1000; - const dy = b.val != null && a.val != null ? b.val - a.val : null; - const slope = dy != null && dtS !== 0 ? dy / dtS : null; - const fmtVal = v => v == null ? '—' : (+v).toFixed(3); - const fmtS = s => s == null ? '—' : s.toFixed(4); - let lines = [`Δt = ${fmtS(dtS)}s`]; - if (dy != null) lines.push(`Δy = ${fmtVal(dy)}`); - if (slope != null) lines.push(`기울기 = ${fmtS(slope)}/min`); - data = [ - { - xAxis: a.ms, yAxis: a.val, - symbol: 'circle', symbolSize: 8, - itemStyle: { color: '#f59e0b' }, - label: { show: true, position: 'right', formatter: 'A', color: '#f59e0b', fontSize: 10 } - }, - { - xAxis: b.ms, yAxis: b.val, - symbol: 'circle', symbolSize: 8, - itemStyle: { color: '#3b82f6' }, - label: { show: true, position: 'right', formatter: 'B', color: '#3b82f6', fontSize: 10 } - }, - { - xAxis: [a.ms, b.ms], - lineStyle: { color: '#8b5cf6', type: 'dashed', width: 1 }, - symbol: 'none' - }, - { - xAxis: b.ms, yAxis: a.val, - lineStyle: { color: '#8b5cf6', type: 'dotted', width: 1 }, - symbol: 'none' - }, - { - coord: [b.ms, a.val], - symbol: 'none', - label: { - show: true, position: 'inside', - formatter: lines.join('\n'), - fontSize: 10, color: '#c084fc', - backgroundColor: 'rgba(15,15,25,.75)', - padding: [4, 6], borderRadius: 4 - } - } - ]; - } else if (a) { - data = [{ - xAxis: a.ms, yAxis: a.val, - symbol: 'circle', symbolSize: 8, - itemStyle: { color: '#f59e0b' }, - label: { show: true, position: 'right', formatter: 'A', color: '#f59e0b', fontSize: 10 } - }]; + const dtS = (b.ms - a.ms) / 1000; + const dy = (a.val != null && b.val != null) ? b.val - a.val : null; + const slope = (dy != null && dtS !== 0) ? dy / (dtS / 60) : null; // 분당 변화율 + const txt = [`Δt=${Math.abs(dtS).toFixed(0)}s`, + dy != null ? `Δy=${dy.toFixed(2)}` : null, + slope != null ? `기울기=${slope.toFixed(2)}/min` : null].filter(Boolean).join(' '); + ml = { symbol: 'none', silent: true, lineStyle: { color: '#8b5cf6', type: 'dashed', width: 1.5 }, + data: [[{ coord: [a.ms, a.val] }, + { coord: [b.ms, b.val], + label: { show: true, formatter: txt, fontSize: 11, color: '#fff', + backgroundColor: 'rgba(124,58,237,.9)', padding: [4, 7], borderRadius: 4 } }]] }; } - trChart.setOption({ - graphic: [{ - type: 'group', - bounding: 'raw', - children: data.length ? data.map(d => { - if (d.coord) { - return { - type: 'text', - left: d.coord[0], top: d.coord[1], - style: { text: d.label.formatter, fontSize: d.label.fontSize, fill: d.label.color, - backgroundColor: d.label.backgroundColor, padding: d.label.padding, - borderRadius: d.label.borderRadius, boxShadow: '0 2px 8px rgba(0,0,0,.3)' } - }; - } - return { - type: 'circle', - shape: { cx: d.xAxis, cy: d.yAxis, r: d.symbolSize / 2 }, - style: { fill: d.itemStyle.color, stroke: '#fff', lineWidth: 1 }, - ...(d.label ? { label: { show: d.label.show, position: d.label.position, - formatter: d.label.formatter, color: d.label.color, fontSize: d.label.fontSize } } : {}), - silent: true - }; - }) : [] - }] - }, { replaceMerge: ['graphic'] }); + return { name: '__cursor', type: 'line', data: [], silent: true, showSymbol: false, z: 20, + markPoint: { silent: true, data: mp }, markLine: ml }; } function trOnCursorClick(e) {