diff --git a/examples/monitor-web/monitor_web.html b/examples/monitor-web/monitor_web.html index 144c5a2..832f24d 100644 --- a/examples/monitor-web/monitor_web.html +++ b/examples/monitor-web/monitor_web.html @@ -132,12 +132,27 @@ font-weight: 600; } .legend-item { + background: transparent; + border: 0; + color: inherit; + cursor: pointer; display: inline-flex; + font: inherit; align-items: center; gap: 6px; min-width: 0; + padding: 0; white-space: nowrap; } + .legend-item.hidden { + color: var(--muted); + opacity: 0.55; + } + .legend-item:focus-visible { + border-radius: 2px; + outline: 1px solid var(--accent); + outline-offset: 2px; + } .legend-swatch { width: 28px; height: 2px; @@ -276,6 +291,11 @@ — +
@@ -322,6 +342,7 @@ let historyAbortController = null; let historyRequestId = 0; const chartRelayoutHandlersAttached = new Set(); + const hiddenChartTraces = new Map(); const $ = (id) => document.getElementById(id); const setText = (id, text) => { @@ -505,17 +526,66 @@ `${label} ${fmtMibUsage(usage)} (${fmtPercentOne(pct)})`; const tracePowerName = (label, watts) => `${label} ${fmtW(watts)}`; - const setChartLegend = (legend, items) => { + const isTraceHidden = (chartId, key) => + hiddenChartTraces.get(chartId)?.has(key) || false; + + const setTraceHidden = (chartId, key, hidden) => { + const hiddenTraces = hiddenChartTraces.get(chartId) || new Set(); + if (hidden) { + hiddenTraces.add(key); + hiddenChartTraces.set(chartId, hiddenTraces); + } else { + hiddenTraces.delete(key); + if (hiddenTraces.size === 0) { + hiddenChartTraces.delete(chartId); + } else { + hiddenChartTraces.set(chartId, hiddenTraces); + } + } + }; + + const syncLegendItemState = (item, hidden) => { + item.classList.toggle("hidden", hidden); + item.setAttribute("aria-pressed", String(!hidden)); + item.title = `${hidden ? "Show" : "Hide"} ${item.dataset.traceLabel}`; + }; + + const toggleChartTrace = (chartId, key, traceIndex) => { + const hidden = !isTraceHidden(chartId, key); + setTraceHidden(chartId, key, hidden); + + const legendItem = document.querySelector( + `[data-chart-legend="${chartId}"] [data-trace="${key}"]`, + ); + if (legendItem) syncLegendItemState(legendItem, hidden); + + if (window.Plotly && $(chartId)) { + Plotly.restyle( + chartId, + { visible: hidden ? "legendonly" : true }, + [traceIndex], + ); + } + }; + + const setChartLegend = (legend, chartId, items) => { legend.replaceChildren( - ...items.map((item) => { - const entry = document.createElement("span"); + ...items.map((item, traceIndex) => { + const entry = document.createElement("button"); entry.className = "legend-item"; + entry.type = "button"; + entry.dataset.trace = item.key; + entry.dataset.traceLabel = item.name; const swatch = document.createElement("span"); swatch.className = "legend-swatch"; swatch.style.background = item.color; const label = document.createElement("span"); label.textContent = item.label; entry.append(swatch, label); + syncLegendItemState(entry, isTraceHidden(chartId, item.key)); + entry.addEventListener("click", () => { + toggleChartTrace(chartId, item.key, traceIndex); + }); return entry; }), ); @@ -617,18 +687,36 @@ if (chartXRange !== null) { xaxis.range = chartXRange; } + const legendItems = [ + { + color: "#4ade80", + key: "cpu", + label: tracePercentName( + "CPU", + latestVisiblePercent("cpu_percent"), + ), + name: "CPU", + }, + { + color: "#38bdf8", + key: "memory", + label: traceUsageName( + "Host Memory", + latestVisibleHostMemoryUsage(), + ), + name: "Host Memory", + }, + ]; + setChartLegend($("host-legend"), "host-chart", legendItems); + const traceVisible = (key) => + isTraceHidden("host-chart", key) ? "legendonly" : true; const layout = { autosize: true, font: { color: "#e2e8f0", size: 12 }, hoverlabel: plotHoverLabel, hovermode: "x unified", - legend: { - bgcolor: "rgba(0,0,0,0)", - orientation: "h", - x: 0, - y: 1.15, - }, + showlegend: false, margin: { b: 34, l: 42, r: 16, t: 8 }, paper_bgcolor: "rgba(0,0,0,0)", plot_bgcolor: "#0b0d12", @@ -649,11 +737,9 @@ hovertemplate: "CPU %{y:.1f}%