refactor: 트렌드 듀얼커서를 graphic 수동배치 → 레이어(markLine/markPoint)로 재구현
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
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 } }]] };
|
||||
}
|
||||
}
|
||||
];
|
||||
} 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 }
|
||||
}];
|
||||
}
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user