diff --git a/web/frontend/src/Analysis.root.svelte b/web/frontend/src/Analysis.root.svelte index 2e6f5b5..1cfadc9 100644 --- a/web/frontend/src/Analysis.root.svelte +++ b/web/frontend/src/Analysis.root.svelte @@ -1,10 +1,11 @@ + +{#if data.length > 0} +
+{:else} + Cannot render histogram: No data! +{/if} + + diff --git a/web/frontend/src/plots/MetricPlot.svelte b/web/frontend/src/plots/MetricPlot.svelte index c0440fd..509672c 100644 --- a/web/frontend/src/plots/MetricPlot.svelte +++ b/web/frontend/src/plots/MetricPlot.svelte @@ -26,16 +26,17 @@ import { getContext, onMount, onDestroy } from 'svelte' import { Card } from 'sveltestrap' + export let metric + export let scope = 'node' + export let resources = [] export let width export let height export let timestep export let series + export let useStatsSeries = null export let statisticsSeries = null export let cluster export let subCluster - export let metric - export let useStatsSeries = null - export let scope = 'node' export let isShared = false if (useStatsSeries == null) @@ -53,6 +54,70 @@ const backgroundColors = { normal: 'rgba(255, 255, 255, 1.0)', caution: 'rgba(255, 128, 0, 0.3)', alert: 'rgba(255, 0, 0, 0.3)' } const thresholds = findThresholds(metricConfig, scope, typeof subCluster == 'string' ? cluster.subClusters.find(sc => sc.name == subCluster) : subCluster) + // converts the legend into a simple tooltip + function legendAsTooltipPlugin({ className, style = { backgroundColor:"rgba(255, 249, 196, 0.92)", color: "black" } } = {}) { + let legendEl; + const dataSize = series.length + + function init(u, opts) { + legendEl = u.root.querySelector(".u-legend"); + + legendEl.classList.remove("u-inline"); + className && legendEl.classList.add(className); + + uPlot.assign(legendEl.style, { + textAlign: "left", + pointerEvents: "none", + display: "none", + position: "absolute", + left: 0, + top: 0, + zIndex: 100, + boxShadow: "2px 2px 10px rgba(0,0,0,0.5)", + ...style + }); + + // conditional hide series color markers: + if (useStatsSeries === true || // Min/Max/Avg Self-Explanatory + dataSize === 1 || // Only one Y-Dataseries + dataSize > 6 ){ // More than 6 Y-Dataseries + const idents = legendEl.querySelectorAll(".u-marker"); + for (let i = 0; i < idents.length; i++) + idents[i].style.display = "none"; + } + + const overEl = u.over; + overEl.style.overflow = "visible"; + + // move legend into plot bounds + overEl.appendChild(legendEl); + + // show/hide tooltip on enter/exit + overEl.addEventListener("mouseenter", () => {legendEl.style.display = null;}); + overEl.addEventListener("mouseleave", () => {legendEl.style.display = "none";}); + + // let tooltip exit plot + // overEl.style.overflow = "visible"; + } + + function update(u) { + const { left, top } = u.cursor; + const width = u.over.querySelector(".u-legend").offsetWidth; + legendEl.style.transform = "translate(" + (left - width - 15) + "px, " + (top + 15) + "px)"; + } + + if (dataSize <= 12 || useStatsSeries === true) { + return { + hooks: { + init: init, + setCursor: update, + } + } + } else { // Setting legend-opts show/live as object with false here will not work ... + return {} + } + } + function backgroundColor() { if (clusterCockpitConfig.plot_general_colorBackground == false || !thresholds @@ -93,7 +158,7 @@ ? (statisticsSeries.max.reduce((max, x) => Math.max(max, x), thresholds.normal) || thresholds.normal) : (series.reduce((max, series) => Math.max(max, series.statistics?.max), thresholds.normal) || thresholds.normal) : null - const plotSeries = [{}] + const plotSeries = [{label: 'Runtime', value: (u, ts, sidx, didx) => didx == null ? null : formatTime(ts)}] const plotData = [new Array(longestSeries)] for (let i = 0; i < longestSeries; i++) // TODO: Cache/Reuse this array? plotData[0][i] = i * timestep @@ -103,9 +168,9 @@ plotData.push(statisticsSeries.min) plotData.push(statisticsSeries.max) plotData.push(statisticsSeries.mean) - plotSeries.push({ scale: 'y', width: lineWidth, stroke: 'red' }) - plotSeries.push({ scale: 'y', width: lineWidth, stroke: 'green' }) - plotSeries.push({ scale: 'y', width: lineWidth, stroke: 'black' }) + plotSeries.push({ label: 'min', scale: 'y', width: lineWidth, stroke: 'red' }) + plotSeries.push({ label: 'max', scale: 'y', width: lineWidth, stroke: 'green' }) + plotSeries.push({ label: 'mean', scale: 'y', width: lineWidth, stroke: 'black' }) plotBands = [ { series: [2,3], fill: 'rgba(0,255,0,0.1)' }, { series: [3,1], fill: 'rgba(255,0,0,0.1)' } @@ -114,6 +179,9 @@ for (let i = 0; i < series.length; i++) { plotData.push(series[i].data) plotSeries.push({ + label: scope === 'node' ? resources[i].hostname : + // scope === 'accelerator' ? resources[0].accelerators[i] : + scope + ' #' + (i+1), scale: 'y', width: lineWidth, stroke: lineColor(i, series.length) @@ -124,6 +192,9 @@ const opts = { width, height, + plugins: [ + legendAsTooltipPlugin() + ], series: plotSeries, axes: [ { @@ -177,8 +248,11 @@ x: { time: false }, y: maxY ? { range: [0., maxY * 1.1] } : {} }, - cursor: { show: false }, - legend: { show: false, live: false } + legend : { // Display legend until max 12 Y-dataseries + show: (series.length <= 12 || useStatsSeries === true) ? true : false, + live: (series.length <= 12 || useStatsSeries === true) ? true : false + }, + cursor: { drag: { x: true, y: true } } } // console.log(opts) @@ -249,16 +323,21 @@ }