mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-07-02 11:43:49 +02:00
Migrate MetricPlot component
This commit is contained in:
parent
56e3f2da5c
commit
aa8789f8f8
@ -201,7 +201,7 @@
|
|||||||
<!-- Subluster Metricconfig remove keyword for jobtables (joblist main, user joblist, project joblist) to be used here as toplevel case-->
|
<!-- Subluster Metricconfig remove keyword for jobtables (joblist main, user joblist, project joblist) to be used here as toplevel case-->
|
||||||
{#if metric.disabled == false && metric.data}
|
{#if metric.disabled == false && metric.data}
|
||||||
<MetricPlot
|
<MetricPlot
|
||||||
on:zoom={({detail}) => handleZoom(detail, metric.data.name)}
|
onZoom={(detail) => handleZoom(detail, metric.data.name)}
|
||||||
height={plotHeight}
|
height={plotHeight}
|
||||||
timestep={metric.data.metric.timestep}
|
timestep={metric.data.metric.timestep}
|
||||||
scope={metric.data.scope}
|
scope={metric.data.scope}
|
||||||
|
@ -20,7 +20,243 @@
|
|||||||
- `zoomState Object?`: The last zoom state to preserve on user zoom [Default: null]
|
- `zoomState Object?`: The last zoom state to preserve on user zoom [Default: null]
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<script context="module">
|
<script>
|
||||||
|
import uPlot from "uplot";
|
||||||
|
import { formatNumber, formatTime } from "../units.js";
|
||||||
|
import { getContext, onMount, onDestroy } from "svelte";
|
||||||
|
import { Card } from "@sveltestrap/sveltestrap";
|
||||||
|
|
||||||
|
/* Svelte 5 Props */
|
||||||
|
let {
|
||||||
|
metric,
|
||||||
|
scope = "node",
|
||||||
|
width = 0,
|
||||||
|
height = 300,
|
||||||
|
timestep,
|
||||||
|
series,
|
||||||
|
useStatsSeries = false,
|
||||||
|
statisticsSeries = null,
|
||||||
|
cluster = "",
|
||||||
|
subCluster,
|
||||||
|
isShared = false,
|
||||||
|
forNode = false,
|
||||||
|
numhwthreads = 0,
|
||||||
|
numaccs = 0,
|
||||||
|
zoomState = null,
|
||||||
|
thresholdState = null,
|
||||||
|
extendedLegendData = null,
|
||||||
|
onZoom
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
/* Const Init */
|
||||||
|
const clusterCockpitConfig = getContext("cc-config");
|
||||||
|
const resampleConfig = getContext("resampling");
|
||||||
|
const subClusterTopology = getContext("getHardwareTopology")(cluster, subCluster);
|
||||||
|
const metricConfig = getContext("getMetricConfig")(cluster, subCluster, metric);
|
||||||
|
const lineWidth = clusterCockpitConfig?.plot_general_lineWidth / window.devicePixelRatio || 2;
|
||||||
|
const lineColors = clusterCockpitConfig?.plot_general_colorscheme || ["#00bfff","#0000ff","#ff00ff","#ff0000","#ff8000","#ffff00","#80ff00"];
|
||||||
|
const cbmode = clusterCockpitConfig?.plot_general_colorblindMode || false;
|
||||||
|
const renderSleepTime = 200;
|
||||||
|
const normalLineColor = "#000000";
|
||||||
|
const backgroundColors = {
|
||||||
|
normal: "rgba(255, 255, 255, 1.0)",
|
||||||
|
caution: cbmode ? "rgba(239, 230, 69, 0.3)" : "rgba(255, 128, 0, 0.3)",
|
||||||
|
alert: cbmode ? "rgba(225, 86, 44, 0.3)" : "rgba(255, 0, 0, 0.3)",
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Var Init */
|
||||||
|
let timeoutId = null;
|
||||||
|
|
||||||
|
/* State Init */
|
||||||
|
let plotWrapper = $state(null);
|
||||||
|
let uplot = $state(null);
|
||||||
|
|
||||||
|
/* Derived */
|
||||||
|
const usesMeanStatsSeries = $derived((statisticsSeries?.mean && statisticsSeries.mean.length != 0));
|
||||||
|
const resampleTrigger = $derived(resampleConfig?.trigger ? Number(resampleConfig.trigger) : null);
|
||||||
|
const resampleResolutions = $derived(resampleConfig?.resolutions ? [...resampleConfig.resolutions] : null);
|
||||||
|
const resampleMinimum = $derived(resampleConfig?.resolutions ? Math.min(...resampleConfig.resolutions) : null);
|
||||||
|
const thresholds = $derived(findJobAggregationThresholds(
|
||||||
|
subClusterTopology,
|
||||||
|
metricConfig,
|
||||||
|
scope,
|
||||||
|
isShared,
|
||||||
|
numhwthreads,
|
||||||
|
numaccs
|
||||||
|
));
|
||||||
|
const longestSeries = $derived.by(() => {
|
||||||
|
if (useStatsSeries) {
|
||||||
|
return usesMeanStatsSeries ? statisticsSeries?.mean?.length : statisticsSeries?.median?.length;
|
||||||
|
} else {
|
||||||
|
return series.reduce((n, series) => Math.max(n, series.data.length), 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const maxX = $derived(longestSeries * timestep);
|
||||||
|
const maxY = $derived.by(() => {
|
||||||
|
let pendingY = 0;
|
||||||
|
if (useStatsSeries) {
|
||||||
|
pendingY = statisticsSeries.max.reduce(
|
||||||
|
(max, x) => Math.max(max, x),
|
||||||
|
thresholds?.normal,
|
||||||
|
) || thresholds?.normal
|
||||||
|
} else {
|
||||||
|
pendingY = series.reduce(
|
||||||
|
(max, series) => Math.max(max, series?.statistics?.max),
|
||||||
|
thresholds?.normal,
|
||||||
|
) || thresholds?.normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pendingY >= 10 * thresholds.peak) {
|
||||||
|
// Hard y-range render limit if outliers in series data
|
||||||
|
return (10 * thresholds.peak);
|
||||||
|
} else {
|
||||||
|
return pendingY;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const plotBands = $derived.by(() => {
|
||||||
|
if (useStatsSeries) {
|
||||||
|
return [
|
||||||
|
{ series: [2, 3], fill: cbmode ? "rgba(0,0,255,0.1)" : "rgba(0,255,0,0.1)" },
|
||||||
|
{ series: [3, 1], fill: cbmode ? "rgba(0,255,0,0.1)" : "rgba(255,0,0,0.1)" },
|
||||||
|
];
|
||||||
|
};
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
const plotData = $derived.by(() => {
|
||||||
|
let pendingData = [new Array(longestSeries)];
|
||||||
|
// X
|
||||||
|
if (forNode === true) {
|
||||||
|
// Negative Timestamp Buildup
|
||||||
|
for (let i = 0; i <= longestSeries; i++) {
|
||||||
|
pendingData[0][i] = (longestSeries - i) * timestep * -1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Positive Timestamp Buildup
|
||||||
|
for (let j = 0; j < longestSeries; j++) {
|
||||||
|
pendingData[0][j] = j * timestep;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
// Y
|
||||||
|
if (useStatsSeries) {
|
||||||
|
pendingData.push(statisticsSeries.min);
|
||||||
|
pendingData.push(statisticsSeries.max);
|
||||||
|
if (usesMeanStatsSeries) {
|
||||||
|
pendingData.push(statisticsSeries.mean);
|
||||||
|
} else {
|
||||||
|
pendingData.push(statisticsSeries.median);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
for (let i = 0; i < series.length; i++) {
|
||||||
|
pendingData.push(series[i].data);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return pendingData;
|
||||||
|
})
|
||||||
|
const plotSeries = $derived.by(() => {
|
||||||
|
let pendingSeries = [
|
||||||
|
// Note: X-Legend Will not be shown as soon as Y-Axis are in extendedMode
|
||||||
|
{
|
||||||
|
label: "Runtime",
|
||||||
|
value: (u, ts, sidx, didx) =>
|
||||||
|
(didx == null) ? null : formatTime(ts, forNode),
|
||||||
|
}
|
||||||
|
];
|
||||||
|
// Y
|
||||||
|
if (useStatsSeries) {
|
||||||
|
pendingSeries.push({
|
||||||
|
label: "min",
|
||||||
|
scale: "y",
|
||||||
|
width: lineWidth,
|
||||||
|
stroke: cbmode ? "rgb(0,255,0)" : "red",
|
||||||
|
});
|
||||||
|
pendingSeries.push({
|
||||||
|
label: "max",
|
||||||
|
scale: "y",
|
||||||
|
width: lineWidth,
|
||||||
|
stroke: cbmode ? "rgb(0,0,255)" : "green",
|
||||||
|
});
|
||||||
|
pendingSeries.push({
|
||||||
|
label: usesMeanStatsSeries ? "mean" : "median",
|
||||||
|
scale: "y",
|
||||||
|
width: lineWidth,
|
||||||
|
stroke: "black",
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
for (let i = 0; i < series?.length; i++) {
|
||||||
|
// Default
|
||||||
|
if (!extendedLegendData) {
|
||||||
|
pendingSeries.push({
|
||||||
|
label:
|
||||||
|
scope === "node"
|
||||||
|
? series[i].hostname
|
||||||
|
: scope === "accelerator"
|
||||||
|
? 'Acc #' + (i + 1) // series[i].id.slice(9, 14) | Too Hardware Specific
|
||||||
|
: scope + " #" + (i + 1),
|
||||||
|
scale: "y",
|
||||||
|
width: lineWidth,
|
||||||
|
stroke: lineColor(i, series?.length),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Extended Legend For NodeList
|
||||||
|
else {
|
||||||
|
pendingSeries.push({
|
||||||
|
label:
|
||||||
|
scope === "node"
|
||||||
|
? series[i].hostname
|
||||||
|
: scope === "accelerator"
|
||||||
|
? 'Acc #' + (i + 1) // series[i].id.slice(9, 14) | Too Hardware Specific
|
||||||
|
: scope + " #" + (i + 1),
|
||||||
|
scale: "y",
|
||||||
|
width: lineWidth,
|
||||||
|
stroke: lineColor(i, series?.length),
|
||||||
|
values: (u, sidx, idx) => {
|
||||||
|
// "i" = "sidx - 1" : sidx contains x-axis-data
|
||||||
|
if (idx == null)
|
||||||
|
return {
|
||||||
|
time: '-',
|
||||||
|
value: '-',
|
||||||
|
user: '-',
|
||||||
|
job: '-'
|
||||||
|
};
|
||||||
|
|
||||||
|
if (series[i].id in extendedLegendData) {
|
||||||
|
return {
|
||||||
|
time: formatTime(plotData[0][idx], forNode),
|
||||||
|
value: plotData[sidx][idx],
|
||||||
|
user: extendedLegendData[series[i].id].user,
|
||||||
|
job: extendedLegendData[series[i].id].job,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
time: formatTime(plotData[0][idx], forNode),
|
||||||
|
value: plotData[sidx][idx],
|
||||||
|
user: '-',
|
||||||
|
job: '-',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return pendingSeries;
|
||||||
|
})
|
||||||
|
|
||||||
|
/* Effects */
|
||||||
|
$effect(() => {
|
||||||
|
if (!useStatsSeries && statisticsSeries != null) useStatsSeries = true;
|
||||||
|
})
|
||||||
|
|
||||||
|
// This updates plot on all size changes if wrapper (== data) exists
|
||||||
|
$effect(() => {
|
||||||
|
if (plotWrapper) {
|
||||||
|
onSizeChange(width, height);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Functions */
|
||||||
function timeIncrs(timestep, maxX, forNode) {
|
function timeIncrs(timestep, maxX, forNode) {
|
||||||
if (forNode === true) {
|
if (forNode === true) {
|
||||||
return [60, 120, 240, 300, 360, 480, 600, 900, 1800, 3600, 7200, 14400, 21600]; // forNode fixed increments
|
return [60, 120, 240, 300, 360, 480, 600, 900, 1800, 3600, 7200, 14400, 21600]; // forNode fixed increments
|
||||||
@ -66,7 +302,6 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (metricConfig?.aggregation == "sum") {
|
if (metricConfig?.aggregation == "sum") {
|
||||||
let divisor;
|
let divisor;
|
||||||
if (isShared == true) { // Shared
|
if (isShared == true) { // Shared
|
||||||
@ -98,70 +333,6 @@
|
|||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
</script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import uPlot from "uplot";
|
|
||||||
import { formatNumber, formatTime } from "../units.js";
|
|
||||||
import { getContext, onMount, onDestroy, createEventDispatcher } from "svelte";
|
|
||||||
import { Card } from "@sveltestrap/sveltestrap";
|
|
||||||
|
|
||||||
export let metric;
|
|
||||||
export let scope = "node";
|
|
||||||
export let width = 0;
|
|
||||||
export let height = 300;
|
|
||||||
export let timestep;
|
|
||||||
export let series;
|
|
||||||
export let useStatsSeries = false;
|
|
||||||
export let statisticsSeries = null;
|
|
||||||
export let cluster = "";
|
|
||||||
export let subCluster;
|
|
||||||
export let isShared = false;
|
|
||||||
export let forNode = false;
|
|
||||||
export let numhwthreads = 0;
|
|
||||||
export let numaccs = 0;
|
|
||||||
export let zoomState = null;
|
|
||||||
export let thresholdState = null;
|
|
||||||
export let extendedLegendData = null;
|
|
||||||
|
|
||||||
if (!useStatsSeries && statisticsSeries != null) useStatsSeries = true;
|
|
||||||
|
|
||||||
const usesMeanStatsSeries = (statisticsSeries?.mean && statisticsSeries.mean.length != 0)
|
|
||||||
const dispatch = createEventDispatcher();
|
|
||||||
const subClusterTopology = getContext("getHardwareTopology")(cluster, subCluster);
|
|
||||||
const metricConfig = getContext("getMetricConfig")(cluster, subCluster, metric);
|
|
||||||
const clusterCockpitConfig = getContext("cc-config");
|
|
||||||
const renderSleepTime = 200;
|
|
||||||
const normalLineColor = "#000000";
|
|
||||||
const lineWidth =
|
|
||||||
clusterCockpitConfig.plot_general_lineWidth / window.devicePixelRatio;
|
|
||||||
const lineColors = clusterCockpitConfig.plot_general_colorscheme;
|
|
||||||
const cbmode = clusterCockpitConfig?.plot_general_colorblindMode || false;
|
|
||||||
|
|
||||||
const backgroundColors = {
|
|
||||||
normal: "rgba(255, 255, 255, 1.0)",
|
|
||||||
caution: cbmode ? "rgba(239, 230, 69, 0.3)" : "rgba(255, 128, 0, 0.3)",
|
|
||||||
alert: cbmode ? "rgba(225, 86, 44, 0.3)" : "rgba(255, 0, 0, 0.3)",
|
|
||||||
};
|
|
||||||
const thresholds = findJobAggregationThresholds(
|
|
||||||
subClusterTopology,
|
|
||||||
metricConfig,
|
|
||||||
scope,
|
|
||||||
isShared,
|
|
||||||
numhwthreads,
|
|
||||||
numaccs
|
|
||||||
);
|
|
||||||
|
|
||||||
const resampleConfig = getContext("resampling");
|
|
||||||
let resampleTrigger;
|
|
||||||
let resampleResolutions;
|
|
||||||
let resampleMinimum;
|
|
||||||
|
|
||||||
if (resampleConfig) {
|
|
||||||
resampleTrigger = Number(resampleConfig.trigger)
|
|
||||||
resampleResolutions = [...resampleConfig.resolutions];
|
|
||||||
resampleMinimum = Math.min(...resampleConfig.resolutions);
|
|
||||||
}
|
|
||||||
|
|
||||||
// UPLOT PLUGIN // converts the legend into a simple tooltip
|
// UPLOT PLUGIN // converts the legend into a simple tooltip
|
||||||
function legendAsTooltipPlugin({
|
function legendAsTooltipPlugin({
|
||||||
@ -266,284 +437,139 @@
|
|||||||
return backgroundColors.normal;
|
return backgroundColors.normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
// PREPARE UPLOT ...
|
|
||||||
function lineColor(i, n) {
|
function lineColor(i, n) {
|
||||||
if (n >= lineColors.length) return lineColors[i % lineColors.length];
|
if (n && n >= lineColors.length) return lineColors[i % lineColors.length];
|
||||||
else return lineColors[Math.floor((i / n) * lineColors.length)];
|
else return lineColors[Math.floor((i / n) * lineColors.length)];
|
||||||
}
|
}
|
||||||
|
|
||||||
const longestSeries = useStatsSeries
|
function render(ren_width, ren_height) {
|
||||||
? (usesMeanStatsSeries ? statisticsSeries.mean.length : statisticsSeries.median.length)
|
// Set Options
|
||||||
: series.reduce((n, series) => Math.max(n, series.data.length), 0);
|
const opts = {
|
||||||
const maxX = longestSeries * timestep;
|
width,
|
||||||
let maxY = null;
|
height,
|
||||||
|
plugins: [legendAsTooltipPlugin()],
|
||||||
if (thresholds !== null) {
|
series: plotSeries,
|
||||||
maxY = useStatsSeries
|
axes: [
|
||||||
? statisticsSeries.max.reduce(
|
{
|
||||||
(max, x) => Math.max(max, x),
|
scale: "x",
|
||||||
thresholds.normal,
|
space: 35,
|
||||||
) || thresholds.normal
|
incrs: timeIncrs(timestep, maxX, forNode),
|
||||||
: series.reduce(
|
values: (_, vals) => vals.map((v) => formatTime(v, forNode)),
|
||||||
(max, series) => Math.max(max, series.statistics?.max),
|
},
|
||||||
thresholds.normal,
|
{
|
||||||
) || thresholds.normal;
|
|
||||||
|
|
||||||
if (maxY >= 10 * thresholds.peak) {
|
|
||||||
// Hard y-range render limit if outliers in series data
|
|
||||||
maxY = 10 * thresholds.peak;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const plotData = [new Array(longestSeries)];
|
|
||||||
if (forNode === true) {
|
|
||||||
// Negative Timestamp Buildup
|
|
||||||
for (let i = 0; i <= longestSeries; i++) {
|
|
||||||
plotData[0][i] = (longestSeries - i) * timestep * -1;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Positive Timestamp Buildup
|
|
||||||
for (
|
|
||||||
let j = 0;
|
|
||||||
j < longestSeries;
|
|
||||||
j++ // TODO: Cache/Reuse this array?
|
|
||||||
)
|
|
||||||
plotData[0][j] = j * timestep;
|
|
||||||
}
|
|
||||||
|
|
||||||
const plotSeries = [
|
|
||||||
// Note: X-Legend Will not be shown as soon as Y-Axis are in extendedMode
|
|
||||||
{
|
|
||||||
label: "Runtime",
|
|
||||||
value: (u, ts, sidx, didx) =>
|
|
||||||
(didx == null) ? null : formatTime(ts, forNode),
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
let plotBands = undefined;
|
|
||||||
if (useStatsSeries) {
|
|
||||||
plotData.push(statisticsSeries.min);
|
|
||||||
plotData.push(statisticsSeries.max);
|
|
||||||
if (usesMeanStatsSeries) {
|
|
||||||
plotData.push(statisticsSeries.mean);
|
|
||||||
} else {
|
|
||||||
plotData.push(statisticsSeries.median);
|
|
||||||
}
|
|
||||||
|
|
||||||
plotSeries.push({
|
|
||||||
label: "min",
|
|
||||||
scale: "y",
|
|
||||||
width: lineWidth,
|
|
||||||
stroke: cbmode ? "rgb(0,255,0)" : "red",
|
|
||||||
});
|
|
||||||
plotSeries.push({
|
|
||||||
label: "max",
|
|
||||||
scale: "y",
|
|
||||||
width: lineWidth,
|
|
||||||
stroke: cbmode ? "rgb(0,0,255)" : "green",
|
|
||||||
});
|
|
||||||
plotSeries.push({
|
|
||||||
label: usesMeanStatsSeries ? "mean" : "median",
|
|
||||||
scale: "y",
|
|
||||||
width: lineWidth,
|
|
||||||
stroke: "black",
|
|
||||||
});
|
|
||||||
|
|
||||||
plotBands = [
|
|
||||||
{ series: [2, 3], fill: cbmode ? "rgba(0,0,255,0.1)" : "rgba(0,255,0,0.1)" },
|
|
||||||
{ series: [3, 1], fill: cbmode ? "rgba(0,255,0,0.1)" : "rgba(255,0,0,0.1)" },
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
for (let i = 0; i < series.length; i++) {
|
|
||||||
plotData.push(series[i].data);
|
|
||||||
// Default
|
|
||||||
if (!extendedLegendData) {
|
|
||||||
plotSeries.push({
|
|
||||||
label:
|
|
||||||
scope === "node"
|
|
||||||
? series[i].hostname
|
|
||||||
: scope === "accelerator"
|
|
||||||
? 'Acc #' + (i + 1) // series[i].id.slice(9, 14) | Too Hardware Specific
|
|
||||||
: scope + " #" + (i + 1),
|
|
||||||
scale: "y",
|
scale: "y",
|
||||||
width: lineWidth,
|
grid: { show: true },
|
||||||
stroke: lineColor(i, series.length),
|
labelFont: "sans-serif",
|
||||||
});
|
values: (u, vals) => vals.map((v) => formatNumber(v)),
|
||||||
}
|
},
|
||||||
// Extended Legend For NodeList
|
],
|
||||||
else {
|
bands: plotBands,
|
||||||
plotSeries.push({
|
padding: [5, 10, -20, 0],
|
||||||
label:
|
hooks: {
|
||||||
scope === "node"
|
init: [
|
||||||
? series[i].hostname
|
(u) => {
|
||||||
: scope === "accelerator"
|
/* IF Zoom Enabled */
|
||||||
? 'Acc #' + (i + 1) // series[i].id.slice(9, 14) | Too Hardware Specific
|
if (resampleConfig && !forNode) {
|
||||||
: scope + " #" + (i + 1),
|
u.over.addEventListener("dblclick", (e) => {
|
||||||
scale: "y",
|
// console.log('Dispatch: Zoom Reset')
|
||||||
width: lineWidth,
|
onZoom({
|
||||||
stroke: lineColor(i, series.length),
|
lastZoomState: {
|
||||||
values: (u, sidx, idx) => {
|
x: { time: false },
|
||||||
// "i" = "sidx - 1" : sidx contains x-axis-data
|
y: { auto: true }
|
||||||
if (idx == null)
|
}
|
||||||
return {
|
});
|
||||||
time: '-',
|
});
|
||||||
value: '-',
|
};
|
||||||
user: '-',
|
},
|
||||||
job: '-'
|
],
|
||||||
};
|
draw: [
|
||||||
|
(u) => {
|
||||||
|
// Draw plot type label:
|
||||||
|
let textl = `${scope}${plotSeries.length > 2 ? "s" : ""}${
|
||||||
|
useStatsSeries
|
||||||
|
? (usesMeanStatsSeries ? ": min/mean/max" : ": min/median/max")
|
||||||
|
: metricConfig != null && scope != metricConfig.scope
|
||||||
|
? ` (${metricConfig.aggregation})`
|
||||||
|
: ""
|
||||||
|
}`;
|
||||||
|
let textr = `${isShared && scope != "core" && scope != "accelerator" ? "[Shared]" : ""}`;
|
||||||
|
u.ctx.save();
|
||||||
|
u.ctx.textAlign = "start"; // 'end'
|
||||||
|
u.ctx.fillStyle = "black";
|
||||||
|
u.ctx.fillText(textl, u.bbox.left + 10, u.bbox.top + (forNode ? 0 : 10));
|
||||||
|
u.ctx.textAlign = "end";
|
||||||
|
u.ctx.fillStyle = "black";
|
||||||
|
u.ctx.fillText(
|
||||||
|
textr,
|
||||||
|
u.bbox.left + u.bbox.width - 10,
|
||||||
|
u.bbox.top + (forNode ? 0 : 10),
|
||||||
|
);
|
||||||
|
// u.ctx.fillText(text, u.bbox.left + u.bbox.width - 10, u.bbox.top + u.bbox.height - 10) // Recipe for bottom right
|
||||||
|
|
||||||
if (series[i].id in extendedLegendData) {
|
if (!thresholds) {
|
||||||
return {
|
u.ctx.restore();
|
||||||
time: formatTime(plotData[0][idx], forNode),
|
return;
|
||||||
value: plotData[sidx][idx],
|
|
||||||
user: extendedLegendData[series[i].id].user,
|
|
||||||
job: extendedLegendData[series[i].id].job,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
time: formatTime(plotData[0][idx], forNode),
|
|
||||||
value: plotData[sidx][idx],
|
|
||||||
user: '-',
|
|
||||||
job: '-',
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const opts = {
|
let y = u.valToPos(thresholds.normal, "y", true);
|
||||||
width,
|
u.ctx.save();
|
||||||
height,
|
u.ctx.lineWidth = lineWidth;
|
||||||
plugins: [legendAsTooltipPlugin()],
|
u.ctx.strokeStyle = normalLineColor;
|
||||||
series: plotSeries,
|
u.ctx.setLineDash([5, 5]);
|
||||||
axes: [
|
u.ctx.beginPath();
|
||||||
{
|
u.ctx.moveTo(u.bbox.left, y);
|
||||||
scale: "x",
|
u.ctx.lineTo(u.bbox.left + u.bbox.width, y);
|
||||||
space: 35,
|
u.ctx.stroke();
|
||||||
incrs: timeIncrs(timestep, maxX, forNode),
|
|
||||||
values: (_, vals) => vals.map((v) => formatTime(v, forNode)),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scale: "y",
|
|
||||||
grid: { show: true },
|
|
||||||
labelFont: "sans-serif",
|
|
||||||
values: (u, vals) => vals.map((v) => formatNumber(v)),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
bands: plotBands,
|
|
||||||
padding: [5, 10, -20, 0],
|
|
||||||
hooks: {
|
|
||||||
init: [
|
|
||||||
(u) => {
|
|
||||||
/* IF Zoom Enabled */
|
|
||||||
if (resampleConfig) {
|
|
||||||
u.over.addEventListener("dblclick", (e) => {
|
|
||||||
// console.log('Dispatch: Zoom Reset')
|
|
||||||
dispatch('zoom', {
|
|
||||||
lastZoomState: {
|
|
||||||
x: { time: false },
|
|
||||||
y: { auto: true }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
},
|
|
||||||
],
|
|
||||||
draw: [
|
|
||||||
(u) => {
|
|
||||||
// Draw plot type label:
|
|
||||||
let textl = `${scope}${plotSeries.length > 2 ? "s" : ""}${
|
|
||||||
useStatsSeries
|
|
||||||
? (usesMeanStatsSeries ? ": min/mean/max" : ": min/median/max")
|
|
||||||
: metricConfig != null && scope != metricConfig.scope
|
|
||||||
? ` (${metricConfig.aggregation})`
|
|
||||||
: ""
|
|
||||||
}`;
|
|
||||||
let textr = `${isShared && scope != "core" && scope != "accelerator" ? "[Shared]" : ""}`;
|
|
||||||
u.ctx.save();
|
|
||||||
u.ctx.textAlign = "start"; // 'end'
|
|
||||||
u.ctx.fillStyle = "black";
|
|
||||||
u.ctx.fillText(textl, u.bbox.left + 10, u.bbox.top + (forNode ? 0 : 10));
|
|
||||||
u.ctx.textAlign = "end";
|
|
||||||
u.ctx.fillStyle = "black";
|
|
||||||
u.ctx.fillText(
|
|
||||||
textr,
|
|
||||||
u.bbox.left + u.bbox.width - 10,
|
|
||||||
u.bbox.top + (forNode ? 0 : 10),
|
|
||||||
);
|
|
||||||
// u.ctx.fillText(text, u.bbox.left + u.bbox.width - 10, u.bbox.top + u.bbox.height - 10) // Recipe for bottom right
|
|
||||||
|
|
||||||
if (!thresholds) {
|
|
||||||
u.ctx.restore();
|
u.ctx.restore();
|
||||||
return;
|
},
|
||||||
}
|
],
|
||||||
|
setScale: [
|
||||||
let y = u.valToPos(thresholds.normal, "y", true);
|
(u, key) => { // If ZoomResample is Configured && Not System/Node View
|
||||||
u.ctx.save();
|
if (resampleConfig && !forNode && key === 'x') {
|
||||||
u.ctx.lineWidth = lineWidth;
|
const numX = (u.series[0].idxs[1] - u.series[0].idxs[0])
|
||||||
u.ctx.strokeStyle = normalLineColor;
|
if (numX <= resampleTrigger && timestep !== resampleMinimum) {
|
||||||
u.ctx.setLineDash([5, 5]);
|
/* Get closest zoom level; prevents multiple iterative zoom requests for big zoom-steps (e.g. 600 -> 300 -> 120 -> 60) */
|
||||||
u.ctx.beginPath();
|
// Which resolution to theoretically request to achieve 30 or more visible data points:
|
||||||
u.ctx.moveTo(u.bbox.left, y);
|
const target = (numX * timestep) / resampleTrigger
|
||||||
u.ctx.lineTo(u.bbox.left + u.bbox.width, y);
|
// Which configured resolution actually matches the closest to theoretical target:
|
||||||
u.ctx.stroke();
|
const closest = resampleResolutions.reduce(function(prev, curr) {
|
||||||
u.ctx.restore();
|
return (Math.abs(curr - target) < Math.abs(prev - target) ? curr : prev);
|
||||||
},
|
});
|
||||||
],
|
// Prevents non-required dispatches
|
||||||
setScale: [
|
if (timestep !== closest) {
|
||||||
(u, key) => { // If ZoomResample is Configured && Not System/Node View
|
// console.log('Dispatch: Zoom with Res from / to', timestep, closest)
|
||||||
if (resampleConfig && !forNode && key === 'x') {
|
onZoom({
|
||||||
const numX = (u.series[0].idxs[1] - u.series[0].idxs[0])
|
newRes: closest,
|
||||||
if (numX <= resampleTrigger && timestep !== resampleMinimum) {
|
lastZoomState: u?.scales,
|
||||||
/* Get closest zoom level; prevents multiple iterative zoom requests for big zoom-steps (e.g. 600 -> 300 -> 120 -> 60) */
|
lastThreshold: thresholds?.normal
|
||||||
// Which resolution to theoretically request to achieve 30 or more visible data points:
|
});
|
||||||
const target = (numX * timestep) / resampleTrigger
|
}
|
||||||
// Which configured resolution actually matches the closest to theoretical target:
|
} else {
|
||||||
const closest = resampleResolutions.reduce(function(prev, curr) {
|
// console.log('Dispatch: Zoom Update States')
|
||||||
return (Math.abs(curr - target) < Math.abs(prev - target) ? curr : prev);
|
onZoom({
|
||||||
});
|
|
||||||
// Prevents non-required dispatches
|
|
||||||
if (timestep !== closest) {
|
|
||||||
// console.log('Dispatch: Zoom with Res from / to', timestep, closest)
|
|
||||||
dispatch('zoom', {
|
|
||||||
newRes: closest,
|
|
||||||
lastZoomState: u?.scales,
|
lastZoomState: u?.scales,
|
||||||
lastThreshold: thresholds?.normal
|
lastThreshold: thresholds?.normal
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
} else {
|
|
||||||
// console.log('Dispatch: Zoom Update States')
|
|
||||||
dispatch('zoom', {
|
|
||||||
lastZoomState: u?.scales,
|
|
||||||
lastThreshold: thresholds?.normal
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
};
|
},
|
||||||
},
|
]
|
||||||
]
|
},
|
||||||
},
|
scales: {
|
||||||
scales: {
|
x: { time: false },
|
||||||
x: { time: false },
|
y: maxY ? { min: 0, max: (maxY * 1.1) } : {auto: true}, // Add some space to upper render limit
|
||||||
y: maxY ? { min: 0, max: (maxY * 1.1) } : {auto: true}, // Add some space to upper render limit
|
},
|
||||||
},
|
legend: {
|
||||||
legend: {
|
// Display legend until max 12 Y-dataseries
|
||||||
// Display legend until max 12 Y-dataseries
|
show: series.length <= 12 || useStatsSeries,
|
||||||
show: series.length <= 12 || useStatsSeries,
|
live: series.length <= 12 || useStatsSeries,
|
||||||
live: series.length <= 12 || useStatsSeries,
|
},
|
||||||
},
|
cursor: {
|
||||||
cursor: {
|
drag: { x: true, y: true },
|
||||||
drag: { x: true, y: true },
|
}
|
||||||
}
|
};
|
||||||
};
|
|
||||||
|
|
||||||
// RENDER HANDLING
|
// Handle Render
|
||||||
let plotWrapper = null;
|
|
||||||
let uplot = null;
|
|
||||||
let timeoutId = null;
|
|
||||||
|
|
||||||
function render(ren_width, ren_height) {
|
|
||||||
if (!uplot) {
|
if (!uplot) {
|
||||||
opts.width = ren_width;
|
opts.width = ren_width;
|
||||||
opts.height = ren_height;
|
opts.height = ren_height;
|
||||||
@ -570,22 +596,19 @@
|
|||||||
}, renderSleepTime);
|
}, renderSleepTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* On Mount */
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (plotWrapper) {
|
if (plotWrapper) {
|
||||||
render(width, height);
|
render(width, height);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* On Destroy */
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
if (timeoutId != null) clearTimeout(timeoutId);
|
if (timeoutId != null) clearTimeout(timeoutId);
|
||||||
if (uplot) uplot.destroy();
|
if (uplot) uplot.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
// This updates plot on all size changes if wrapper (== data) exists
|
|
||||||
$: if (plotWrapper) {
|
|
||||||
onSizeChange(width, height);
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Define $width Wrapper and NoData Card -->
|
<!-- Define $width Wrapper and NoData Card -->
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
import {
|
import {
|
||||||
minScope,
|
minScope,
|
||||||
} from "../generic/utils.js";
|
} from "../generic/utils.js";
|
||||||
import Timeseries from "../generic/plots/MetricPlot.svelte";
|
import MetricPlot from "../generic/plots/MetricPlot.svelte";
|
||||||
|
|
||||||
/* Svelte 5 Props */
|
/* Svelte 5 Props */
|
||||||
let {
|
let {
|
||||||
@ -173,8 +173,8 @@
|
|||||||
{:else if $metricData.error}
|
{:else if $metricData.error}
|
||||||
<Card body color="danger">{$metricData.error.message}</Card>
|
<Card body color="danger">{$metricData.error.message}</Card>
|
||||||
{:else if selectedSeries != null && !patternMatches}
|
{:else if selectedSeries != null && !patternMatches}
|
||||||
<Timeseries
|
<MetricPlot
|
||||||
on:zoom={({detail}) => handleZoom(detail)}
|
onZoom={(detail) => handleZoom(detail)}
|
||||||
cluster={job.cluster}
|
cluster={job.cluster}
|
||||||
subCluster={job.subCluster}
|
subCluster={job.subCluster}
|
||||||
timestep={selectedData.timestep}
|
timestep={selectedData.timestep}
|
||||||
@ -188,8 +188,8 @@
|
|||||||
{thresholdState}
|
{thresholdState}
|
||||||
/>
|
/>
|
||||||
{:else if statsSeries[selectedScopeIndex] != null && patternMatches}
|
{:else if statsSeries[selectedScopeIndex] != null && patternMatches}
|
||||||
<Timeseries
|
<MetricPlot
|
||||||
on:zoom={({detail}) => handleZoom(detail)}
|
onZoom={(detail) => handleZoom(detail)}
|
||||||
cluster={job.cluster}
|
cluster={job.cluster}
|
||||||
subCluster={job.subCluster}
|
subCluster={job.subCluster}
|
||||||
timestep={selectedData.timestep}
|
timestep={selectedData.timestep}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user