From e55c1d5efb095bd476045010b075814b20875253 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Mon, 23 Mar 2026 12:55:12 +0800 Subject: [PATCH] fix(chart): restore realtime updates and axes --- web/js/app.js | 3 +- web/js/chart.js | 92 ++++++++++++++++++++++++++++++++++++++++++++++-- web/js/points.js | 11 ++++++ 3 files changed, 102 insertions(+), 4 deletions(-) diff --git a/web/js/app.js b/web/js/app.js index d602063..05ba772 100644 --- a/web/js/app.js +++ b/web/js/app.js @@ -14,6 +14,7 @@ import { import { startLogs, startPointSocket } from "./logs.js"; import { clearBatchBinding, + browseAndLoadTree, clearSelectedPoints, createPoints, loadPoints, @@ -47,7 +48,7 @@ function bindEvents() { dom.nodeTree.innerHTML = '
Click "Load Nodes" to fetch node tree
'; dom.pointSourceNodeCount.textContent = "Nodes: 0"; }); - dom.browseNodesBtn.addEventListener("click", () => withStatus(loadTree())); + dom.browseNodesBtn.addEventListener("click", () => withStatus(browseAndLoadTree())); dom.refreshTreeBtn.addEventListener("click", () => withStatus(loadTree())); dom.createPointsBtn.addEventListener("click", () => withStatus(createPoints())); dom.closeModalBtn.addEventListener("click", () => dom.pointModal.classList.add("hidden")); diff --git a/web/js/chart.js b/web/js/chart.js index 8b0faae..111d631 100644 --- a/web/js/chart.js +++ b/web/js/chart.js @@ -3,13 +3,53 @@ import { dom } from "./dom.js"; import { state } from "./state.js"; function normalizeChartItem(item) { + let valueNumber = null; + if (typeof item?.value_number === "number" && Number.isFinite(item.value_number)) { + valueNumber = item.value_number; + } else if (typeof item?.value === "number" && Number.isFinite(item.value)) { + valueNumber = item.value; + } else if (typeof item?.value === "boolean") { + valueNumber = item.value ? 1 : 0; + } else if (typeof item?.value?.float === "number" && Number.isFinite(item.value.float)) { + valueNumber = item.value.float; + } else if (typeof item?.value?.int === "number" && Number.isFinite(item.value.int)) { + valueNumber = item.value.int; + } else if (typeof item?.value?.uint === "number" && Number.isFinite(item.value.uint)) { + valueNumber = item.value.uint; + } else if (typeof item?.value?.bool === "boolean") { + valueNumber = item.value.bool ? 1 : 0; + } else if (typeof item?.value_text === "string") { + const parsed = Number(item.value_text); + if (Number.isFinite(parsed)) { + valueNumber = parsed; + } + } + return { timestamp: item?.timestamp || "", - valueNumber: typeof item?.value_number === "number" ? item.value_number : null, - valueText: item?.value_text || "", + valueNumber, + valueText: item?.value_text || (valueNumber === null ? "" : String(valueNumber)), }; } +function formatAxisValue(value) { + if (!Number.isFinite(value)) { + return "--"; + } + if (Math.abs(value) >= 1000 || Math.abs(value) < 0.01) { + return value.toExponential(2); + } + return value.toFixed(2); +} + +function formatTimeLabel(timestamp) { + if (!timestamp) { + return "--"; + } + const match = String(timestamp).match(/(\d{2}:\d{2}:\d{2})/); + return match ? match[1] : String(timestamp); +} + export async function openChart(pointId, pointName) { state.chartPointId = pointId; state.chartPointName = pointName || "Point"; @@ -72,10 +112,56 @@ export function renderChart() { max += 1; } - const padding = { top: 20, right: 20, bottom: 36, left: 52 }; + const padding = { top: 20, right: 20, bottom: 42, left: 64 }; const plotWidth = width - padding.left - padding.right; const plotHeight = height - padding.top - padding.bottom; + ctx.strokeStyle = "#cbd5e1"; + ctx.lineWidth = 1; + + for (let i = 0; i <= 4; i += 1) { + const y = padding.top + (plotHeight / 4) * i; + ctx.beginPath(); + ctx.moveTo(padding.left, y); + ctx.lineTo(width - padding.right, y); + ctx.stroke(); + } + + ctx.beginPath(); + ctx.moveTo(padding.left, padding.top); + ctx.lineTo(padding.left, height - padding.bottom); + ctx.lineTo(width - padding.right, height - padding.bottom); + ctx.strokeStyle = "#94a3b8"; + ctx.stroke(); + + ctx.fillStyle = "#64748b"; + ctx.font = "12px Segoe UI"; + for (let i = 0; i <= 4; i += 1) { + const value = max - ((max - min) / 4) * i; + const y = padding.top + (plotHeight / 4) * i; + ctx.fillText(formatAxisValue(value), 8, y + 4); + } + + const firstLabel = formatTimeLabel(state.chartData[0]?.timestamp); + const middleLabel = formatTimeLabel( + state.chartData[Math.floor((state.chartData.length - 1) / 2)]?.timestamp, + ); + const lastLabel = formatTimeLabel(state.chartData[state.chartData.length - 1]?.timestamp); + + ctx.fillText(firstLabel, padding.left, height - 12); + const middleWidth = ctx.measureText(middleLabel).width; + ctx.fillText(middleLabel, padding.left + plotWidth / 2 - middleWidth / 2, height - 12); + const lastWidth = ctx.measureText(lastLabel).width; + ctx.fillText(lastLabel, width - padding.right - lastWidth, height - 12); + + ctx.save(); + ctx.translate(16, padding.top + plotHeight / 2); + ctx.rotate(-Math.PI / 2); + ctx.fillStyle = "#64748b"; + ctx.fillText("Value", 0, 0); + ctx.restore(); + ctx.fillText("Time", width / 2 - 12, height - 28); + ctx.strokeStyle = "#2563eb"; ctx.lineWidth = 2; ctx.beginPath(); diff --git a/web/js/points.js b/web/js/points.js index 426afff..989aa5d 100644 --- a/web/js/points.js +++ b/web/js/points.js @@ -123,6 +123,17 @@ export async function loadTree() { updatePointSourceNodeCount(); } +export async function browseAndLoadTree() { + const sourceId = dom.pointSourceSelect.value || state.selectedSourceId; + if (!sourceId) { + throw new Error("Select a source first"); + } + + state.selectedSourceId = sourceId; + await apiFetch(`/api/source/${sourceId}/browse`, { method: "POST" }); + await loadTree(); +} + export async function createPoints() { if (!state.selectedNodeIds.size) { return;