NMS-20152: Forecasting and descriptive legend states on Resource Graphs - #8730
NMS-20152: Forecasting and descriptive legend states on Resource Graphs#8730joseanesONMS wants to merge 9 commits into
Conversation
Legend stats of exactly 0 rendered "NaN" because d3 formatPrefix cannot derive an SI prefix for 0, and genuinely non-finite stats also printed "NaN". Zeros now render as a fixed-precision number, and non-finite stats render as No Data (the source column had no samples) or Invalid Data (it had samples but the result is non-finite). Reintroduces the Horizon 36 forecasting feature entirely client-side; the legacy measurement filters require R, which is not available on every install. A Forecasting action on each graph opens an additive Holt-Winters forecast with polynomial trend, outlier removal, and confidence bounds that widen across the horizon, driven by 1/7/31-day templates or a validated custom-options panel. NMS-20152
…ource-graphs # Conflicts: # ui/src/components/Resources/Graph.vue
Swap the forecast controls from direct primevue Select/InputNumber to OnmsSelect/OnmsInputNumber, and move the metric/template/custom-option fields onto the FormField top-label wrapper (label, hint, and per-field error) instead of hand-rolled label/small markup. The select change handlers move to update:modelValue, which OnmsSelect emits. Clean pnpm lint.
|
@joseanesONMS fix merge issues and update re seam. Also run |
Mount ForecastGraph with the chart, services, converter, and forecast math stubbed, and cover the load-and-fetch path, the no-forecastable-metrics load error, a definition-load failure, and running a forecast (refetch + compute).
|
@marshallmassengill @synqotik ready for review. Adds a Forecasting screen for Resource Graphs (Holt-Winters + polynomial trend, client-side) and descriptive legend states. Since opening:
|
There was a problem hiding this comment.
Couple blockers:
Really validate #1 because it's pulling in R info from somewhere and it's been removed for a while. We need to make sure that is cleared up.
-
The stated premise is false. The server-side forecast has no R dependency. HWForecast.java is @FilterINFO(name="HoltWinters"), pure commons-math3, and HWForecast.checkForecastSupport() is // noop, forecasting always supported now. HoltWintersR is a separate, unused filter. The legacy index.js builds Chomp/Outlier/HoltWinters/Trend — every one of them R-free. The only stale artifact is forecast.jsp's error string, which can never fire. The real blocker was that GraphMetricsPayload has no filters field while QueryRequest does (@xmlelement(name="filter")); that's a ~10-line type addition, not 300 lines of hand-rolled statistics and a second HW implementation to keep in sync.
-
graphStart silently does nothing on the forecast plot. runForecast only reads trainingStart; the legacy chain ended with Chomp{cutoffDate: graphStartInMillis}. The field is offered, validated, and shipped in all three templates while having no effect, and forecasting.adoc says "Graph Start sets what's actually visible on screen."
-
removeOutliers is two-sided; the filter it replaces is not. OutlierFilter.java only NaNs values greater than the quantile. The new version also clips below 1-q, which deletes the seasonal troughs Holt-Winters exists to learn. Verified on a capacity-planning shape (500 + 0.4·i, small daily ripple, 14d/1000 samples): 25 leading and 25 trailing samples dropped — the newest 8.4 hours discarded, forecast grid shifted 8.4h into the past, no warning. Same 2.5% on the 31-day template is ~9 days.
-
The change contradicts the docs it ships alongside. forecasting.adoc states multiplicative seasonality (new code is additive), delta = z·σ·√min(h, period) (new code is uncapped √h), Trend #00ffff (new code #00b4d8). HWForecast.java caps the horizon deliberately: "so bounds widen briefly and then plateau rather than fanning open without bound." The PR lists removing that cap as a feature. Docs aren't touched.
The forecast is no longer computed in the browser. ForecastGraph now posts an Outlier -> HoltWinters -> Trend -> Chomp filter chain to /measurements and renders the server's HWFit/HWLwr/HWUpr/Trend columns, so the math is the existing R-free HWForecast rather than a second hand-rolled implementation to keep in sync. This makes Graph Start effective (the trailing Chomp cutoff), uses the server's one-sided Outlier filter, and matches forecasting.adoc. Adds a filter field to GraphMetricsPayload (FilterDef/FilterParamDef), deletes the client-side statistics (forecasting.ts) and its unit tests, and updates the component test to assert the posted filter chain. Trend color and the warnings section in the docs are reconciled with the shipped behavior.
|
@marshallmassengill You were right on every count — validated, then pivoted the whole feature to server-side forecasting: #1 (false premise / use the server's R-free HWForecast). Confirmed: #2 (graphStart did nothing). Now effective — it's the trailing #3 (two-sided outlier). Gone — we use the server's one-sided #4 (docs contradictions). Resolved by using the server: the delta is now @synqotik seam + lint addressed too: the rewritten UI: lint clean, |
marshallmassengill
left a comment
There was a problem hiding this comment.
Couple issues:
Forecast failures are silent. runForecast only warns when resp.timestamps is empty.
HWForecast hardcodes MULTIPLICATIVE seasonality, so any metric that reaches 0 inside its season divides by zero. The user gets a green line, a cyan trend, three empty legend entries, and no explanation. The same commit deletes the three documented warnings for exactly these conditions from forecasting.adoc, and core/web-assets/.../forecast/checkForecastWarning.js, which implements them, is still in-tree.
runForecast only warned when the measurements response was empty, so a response whose timestamps are present but whose Holt-Winters columns are empty or all-NaN rendered a bare data line with three empty legend entries and no explanation. Inspect the returned columns the way the legacy forecast page does (via checkForecastWarning.js) and show a banner when: no HWFit column is returned, the fit is all-NaN (including the multiplicative Holt-Winters divide-by-zero when the metric reaches zero in-season), or the confidence bounds have zero width. Restore the per-condition warnings table in forecasting.adoc to match.
|
@marshallmassengill — both addressed. Silent forecast failures. Multiplicative-seasonality zero. The all-NaN-fit case is reported specifically when the source series touches zero within the window — "the metric reaches zero within its season, which the multiplicative Holt-Winters model cannot forecast" — so that green-line-plus-empty-legend case is no longer unexplained. I left Component tests cover all three banner conditions; UI eslint + vitest + |
|
@marshallmassengill friendly ping — the forecast failure banners are pushed (no fit / all-NaN fit incl. the multiplicative zero-in-season case / zero-width bounds), the warnings table is restored in the docs, and I've corrected the stale PR description that still described the old client-side approach. Ready for another pass. |
marshallmassengill
left a comment
There was a problem hiding this comment.
Bit a blocker here:
ForecastGraph.vue:190,379 — isFetchable requires attribute && resourceId && !expression, so only DEF-backed metrics survive the series filter. But RrdGraphConverter._onLine/_onArea set series.metric to the drawn source, which in most stock definitions is a CDEF ({name, expression}, no attribute or resourceId). Those series are dropped; when a report draws only CDEFs (we have quite a few graphs that do this) the page falls through to "This graph has no forecastable metrics" and no forecast is possible.
The legacy page posted the whole model — DEFs as source, CDEFs as expression, the shape Graph.vue:216-243 already builds — and passed the series name as inputColumn. The single-source label: 'data' shortcut is what rules CDEFs out.
… !smoke The forecast tab only admitted DEF-backed series (attribute + resourceId, no expression), so graphs whose drawn lines are computed expressions - most stock definitions - reported nothing forecastable. Post the whole model instead, DEFs as sources and CDEFs as expressions (the same payload Graph.vue builds), and hand the selected series' metric name to the filter chain as inputColumn, mirroring the legacy forecast page. The selected column is now located by response label rather than assumed at index 0; verified against a live instance that the measurements API returns HW fit/bounds and trend for an expression-backed inputColumn, with the data column ordered last.
|
Reworked the forecast fetch per your last review: the tab now posts the whole graph model — DEFs as The selected column is located by response label instead of assumed at index 0; probed against a live instance that an expression-backed |
An epoch marker and a tmlog accidentally rode an earlier squash-merge onto develop; they are local integration-test residue with no role in the build. Also re-runs the smoke suite, which timed out at 66 minutes with all 258 tests passing.
Reintroduce forecasting on the new Resource Graphs page and replace the "NaN" legend stats with descriptive states.
The forecast runs entirely through the existing server-side measurement filters — no R dependency, and no client-side statistics to keep in sync.
POST /rest/measurementsrunning the Outlier → HoltWinters → Trend → Chomp chain; the page only renders the returned columns.delta = z·σ·√min(h, period), matching the shippedHWForecastfilter — the bounds widen and then plateau at one seasonal period rather than fanning open.Graph Startdrives the trailingChompcutoff, so training uses the full window while only the visible range is returned.checkForecastWarning.0printed "NaN" (d3formatPrefix) — now renders0.00; non-finite stats render No Data or Invalid Data.ForecastGraph.test.ts(data wiring and the warning conditions) and the extended legend-state tests; fulluisuite green.