mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2026-08-31 00:47:15 +02:00
feat(frontend): add display smoothing filter for metric plots
cc-lib's resamplers cannot smooth: validateFrequency bails out when the new frequency is not coarser than the old one, and every algorithm produces an output strictly shorter than its input. They consolidate for transport, which is a separate job from filtering for display, and they only act at all once a series exceeds the target point count - shorter jobs arrive raw. Add a centered, NaN-aware moving average in the plot layer, applied after whatever downsampling the backend performed. It preserves length and index alignment, so the X array, the visible point count and the zoom-resampling hook are unaffected. NaN and null samples are skipped, an all-NaN window stays NaN so gaps keep rendering, and the window shrinks at the series edges rather than introducing new gaps there. The window is a user setting in data points, defaulting to 3, and is orthogonal to the resample algorithm - it stacks on top of average consolidation rather than replacing it. All series of a stats plot are smoothed together, since smoothing only some would break the min <= mid <= max invariant the plot bands rely on. Smoothing is display only: reported statistics and job footprints come from JobMetric.Statistics and are untouched. The uPlot cursor readout does show the smoothed value. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -177,6 +177,11 @@ const configSchema = `{
|
||||
"description": "Initial thickness of rendered plotlines. Applies to metric plot, job compare plot and roofline.",
|
||||
"type": "integer"
|
||||
},
|
||||
"smoothing-window": {
|
||||
"description": "Initial display smoothing window in data points, applied after downsampling. 0 disables smoothing. Display-only: reported statistics and footprints are unaffected.",
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"color-scheme": {
|
||||
"description": "Initial colorScheme to be used for metric plots.",
|
||||
"type": "array",
|
||||
|
||||
@@ -311,4 +311,55 @@
|
||||
</form>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<!-- SMOOTHING WINDOW -->
|
||||
<Col>
|
||||
<Card class="h-100">
|
||||
<form
|
||||
id="smoothing-window-form"
|
||||
method="post"
|
||||
action="/frontend/configuration/"
|
||||
class="card-body"
|
||||
onsubmit={(e) => updateSetting(e, {
|
||||
selector: "#smoothing-window-form",
|
||||
target: "sw",
|
||||
})}
|
||||
>
|
||||
<CardTitle
|
||||
style="margin-bottom: 1em; display: flex; align-items: center;"
|
||||
>
|
||||
<div>Smoothing Window</div>
|
||||
{#if displayMessage && message.target == "sw"}
|
||||
<div style="margin-left: auto; font-size: 0.9em;">
|
||||
<code style="color: {message.color};" out:fade>
|
||||
Update: {message.msg}
|
||||
</code>
|
||||
</div>
|
||||
{/if}
|
||||
</CardTitle>
|
||||
<input type="hidden" name="key" value="plotConfiguration_smoothingWindow" />
|
||||
<div class="mb-3">
|
||||
<label for="value" class="form-label">Smoothing Window</label>
|
||||
<input
|
||||
type="number"
|
||||
class="form-control"
|
||||
id="swvalue"
|
||||
name="value"
|
||||
aria-describedby="smoothingWindowHelp"
|
||||
value={config?.plotConfiguration_smoothingWindow}
|
||||
min="0"
|
||||
/>
|
||||
<div id="smoothingWindowHelp" class="form-text">
|
||||
Width of the moving average applied to plotted lines, in data
|
||||
points. 0 disables it, the default is 3. It is applied after
|
||||
downsampling and is display-only: the reported min/avg/max
|
||||
statistics and the job footprint are unaffected. Combined with the
|
||||
Average algorithm it is a second, milder smoothing pass on top of
|
||||
the interval means.
|
||||
</div>
|
||||
</div>
|
||||
<Button color="primary" type="submit">Submit</Button>
|
||||
</form>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -17,6 +17,7 @@
|
||||
- `numhwthreads Number?`: Number of job HWThreads [Default: 0]
|
||||
- `numaccs Number?`: Number of job Accelerators [Default: 0]
|
||||
- `zoomState Object?`: The last zoom state to preserve on user zoom [Default: null]
|
||||
- `smoothingWindow Number?`: Display smoothing window in data points; overrides the user setting. 0 or 1 disables [Default: null]
|
||||
- `thersholdState Object?`: The last threshold state to preserve on user zoom [Default: null]
|
||||
- `extendedLegendData Object?`: Additional information to be rendered in an extended legend [Default: null]
|
||||
- `onZoom Func`: Callback function to handle zoom-in event
|
||||
@@ -25,6 +26,7 @@
|
||||
<script>
|
||||
import uPlot from "uplot";
|
||||
import { formatNumber, formatDurationTime } from "../units.js";
|
||||
import { movingAverage } from "./smoothing.js";
|
||||
import { getContext, onMount, onDestroy } from "svelte";
|
||||
import { Card, CardBody, CardHeader } from "@sveltestrap/sveltestrap";
|
||||
|
||||
@@ -43,6 +45,7 @@
|
||||
forNode = false,
|
||||
zoomState = null,
|
||||
thresholdState = null,
|
||||
smoothingWindow = null,
|
||||
extendedLegendData = null,
|
||||
plotSync = null,
|
||||
enableFlip = false,
|
||||
@@ -74,6 +77,11 @@
|
||||
const metricConfig = $derived(getContext("getMetricConfig")(cluster, subCluster, metric));
|
||||
const usesMeanStatsSeries = $derived((statisticsSeries?.mean && statisticsSeries.mean.length != 0));
|
||||
const nativeTimestep = $derived(metricConfig?.timestep || timestep);
|
||||
// Display-only smoothing, applied after backend downsampling. Does not affect
|
||||
// the reported statistics or the job footprint.
|
||||
const smoothing = $derived(
|
||||
Number(smoothingWindow ?? clusterCockpitConfig?.plotConfiguration_smoothingWindow ?? 0)
|
||||
);
|
||||
const resampleTargetPoints = $derived(resampleConfig?.targetPoints ? Number(resampleConfig.targetPoints) : null);
|
||||
// Zoom in far enough that fewer than a quarter of the target point count is
|
||||
// visible, and a finer resolution is requested from the backend.
|
||||
@@ -137,18 +145,20 @@
|
||||
};
|
||||
};
|
||||
// Y
|
||||
// Smooth every series of a stats plot: smoothing only some of them would
|
||||
// break the min <= mid <= max invariant the plot bands rely on.
|
||||
if (useStatsSeries) {
|
||||
pendingData.push(statisticsSeries.min);
|
||||
pendingData.push(statisticsSeries.max);
|
||||
pendingData.push(movingAverage(statisticsSeries.min, smoothing));
|
||||
pendingData.push(movingAverage(statisticsSeries.max, smoothing));
|
||||
if (usesMeanStatsSeries) {
|
||||
pendingData.push(statisticsSeries.mean);
|
||||
pendingData.push(movingAverage(statisticsSeries.mean, smoothing));
|
||||
} else {
|
||||
pendingData.push(statisticsSeries.median);
|
||||
pendingData.push(movingAverage(statisticsSeries.median, smoothing));
|
||||
}
|
||||
|
||||
} else {
|
||||
for (let i = 0; i < series.length; i++) {
|
||||
pendingData.push(series[i].data);
|
||||
pendingData.push(movingAverage(series[i].data, smoothing));
|
||||
};
|
||||
};
|
||||
return pendingData;
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Display-only smoothing for metric plots.
|
||||
*
|
||||
* This is deliberately separate from the backend resampling in cc-lib: those
|
||||
* algorithms decimate (they reduce the point count and their output length is
|
||||
* always shorter than their input), which makes them unusable as a filter.
|
||||
* A moving average has to keep every point in place, so it lives here and runs
|
||||
* after whatever downsampling the backend applied.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Centered, NaN-aware moving average. Preserves length and index alignment, so
|
||||
* the caller's X array and all derived index math stay valid.
|
||||
*
|
||||
* Windows are given in data points, not seconds. Values that are null or NaN
|
||||
* are skipped; a window containing nothing else yields NaN so uPlot keeps
|
||||
* rendering the gap. At the series edges the window shrinks instead of
|
||||
* producing NaN, which avoids introducing new gaps at the start and end.
|
||||
*
|
||||
* @param {Array<number|null>} data Input samples
|
||||
* @param {number} window Window width in data points; <= 1 disables smoothing
|
||||
* @returns {Array<number|null>} Smoothed copy, or `data` itself if disabled
|
||||
*/
|
||||
export function movingAverage(data, window) {
|
||||
if (!data || data.length === 0) return data;
|
||||
|
||||
let w = Math.floor(Number(window));
|
||||
if (!Number.isFinite(w) || w <= 1) return data;
|
||||
if (w > data.length) w = data.length;
|
||||
// Force odd width so the window stays centered on the sample.
|
||||
if (w % 2 === 0) w -= 1;
|
||||
if (w <= 1) return data;
|
||||
|
||||
const n = data.length;
|
||||
const h = (w - 1) / 2;
|
||||
const out = new Array(n);
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const start = i - h < 0 ? 0 : i - h;
|
||||
const end = i + h > n - 1 ? n - 1 : i + h;
|
||||
let sum = 0;
|
||||
let count = 0;
|
||||
for (let j = start; j <= end; j++) {
|
||||
const v = data[j];
|
||||
if (v == null || Number.isNaN(v)) continue;
|
||||
sum += v;
|
||||
count++;
|
||||
}
|
||||
out[i] = count === 0 ? NaN : sum / count;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
@@ -74,6 +74,7 @@ type PlotConfiguration struct {
|
||||
ColorScheme []string `json:"color-scheme"`
|
||||
ResampleAlgo string `json:"resample-algo"`
|
||||
ResamplePolicy string `json:"resample-policy"`
|
||||
SmoothingWindow int `json:"smoothing-window"`
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -126,6 +127,7 @@ var UIDefaults = WebConfig{
|
||||
PlotsPerRow: 3,
|
||||
LineWidth: 3,
|
||||
ColorScheme: []string{"#00bfff", "#0000ff", "#ff00ff", "#ff0000", "#ff8000", "#ffff00", "#80ff00"},
|
||||
SmoothingWindow: 3,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -174,6 +176,7 @@ func Init(rawConfig json.RawMessage) error {
|
||||
UIDefaultsMap["plotConfiguration_colorScheme"] = UIDefaults.PlotConfiguration.ColorScheme
|
||||
UIDefaultsMap["plotConfiguration_resampleAlgo"] = UIDefaults.PlotConfiguration.ResampleAlgo
|
||||
UIDefaultsMap["plotConfiguration_resamplePolicy"] = UIDefaults.PlotConfiguration.ResamplePolicy
|
||||
UIDefaultsMap["plotConfiguration_smoothingWindow"] = UIDefaults.PlotConfiguration.SmoothingWindow
|
||||
|
||||
for _, c := range UIDefaults.MetricConfig.Clusters {
|
||||
if c.JobListMetrics != nil {
|
||||
|
||||
Reference in New Issue
Block a user