dynamic-systems-testing-framework 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dstf/__init__.py +41 -0
- dstf/__main__.py +10 -0
- dstf/authoring/__init__.py +13 -0
- dstf/authoring/builtin/__init__.py +32 -0
- dstf/authoring/builtin/accept.py +205 -0
- dstf/authoring/builtin/edit_criteria.py +479 -0
- dstf/authoring/builtin/edit_family.py +228 -0
- dstf/authoring/builtin/edit_on_plot.py +1354 -0
- dstf/authoring/builtin/edit_tree.py +1399 -0
- dstf/authoring/builtin/enqueue.py +144 -0
- dstf/authoring/capabilities.py +463 -0
- dstf/authoring/errors.py +49 -0
- dstf/authoring/family_rows.py +526 -0
- dstf/authoring/forms.py +631 -0
- dstf/authoring/layout.py +104 -0
- dstf/authoring/live_rows.py +337 -0
- dstf/authoring/live_runs.py +385 -0
- dstf/authoring/needle.py +127 -0
- dstf/authoring/picker_rows.py +92 -0
- dstf/authoring/queue_rows.py +354 -0
- dstf/authoring/session.py +3016 -0
- dstf/authoring/shell/__init__.py +9 -0
- dstf/authoring/shell/actions.py +702 -0
- dstf/authoring/shell/app.py +53 -0
- dstf/authoring/shell/drawer_controls.py +190 -0
- dstf/authoring/shell/family_fold.py +468 -0
- dstf/authoring/shell/figure_hook.py +249 -0
- dstf/authoring/shell/grips.js +1000 -0
- dstf/authoring/shell/header.py +783 -0
- dstf/authoring/shell/live_view.py +93 -0
- dstf/authoring/shell/picker.py +267 -0
- dstf/authoring/shell/placing.py +122 -0
- dstf/authoring/shell/plot_card.py +1171 -0
- dstf/authoring/shell/queue_panel.py +433 -0
- dstf/authoring/shell/rail.py +593 -0
- dstf/authoring/shell/shell.py +528 -0
- dstf/authoring/shell/stats_card.py +501 -0
- dstf/authoring/shell/strip.py +204 -0
- dstf/authoring/shell/style.py +721 -0
- dstf/authoring/shell/suite.py +277 -0
- dstf/authoring/shell/suite_grid.js +414 -0
- dstf/authoring/shell/suite_grid.py +373 -0
- dstf/authoring/shell/tab_theme.py +110 -0
- dstf/authoring/shell/tabstate.py +146 -0
- dstf/authoring/shell/test_list.py +179 -0
- dstf/authoring/shell/test_page.py +1404 -0
- dstf/authoring/shell/tree_card.py +1171 -0
- dstf/authoring/shell/variables_drawer.py +589 -0
- dstf/authoring/shell/widgets.py +188 -0
- dstf/authoring/situation.py +287 -0
- dstf/authoring/spec_document.py +215 -0
- dstf/authoring/spec_preview.py +83 -0
- dstf/authoring/suite_rows.py +99 -0
- dstf/authoring/tree_move.py +516 -0
- dstf/authoring/tree_pointer.py +33 -0
- dstf/authoring/tree_text.py +61 -0
- dstf/authoring/typed_number.js +64 -0
- dstf/authoring/typed_number.py +76 -0
- dstf/authoring/unscorable.py +141 -0
- dstf/authoring/variable_rows.py +216 -0
- dstf/backends/__init__.py +283 -0
- dstf/backends/base.py +1117 -0
- dstf/backends/common/__init__.py +1 -0
- dstf/backends/common/json_driver.py +484 -0
- dstf/backends/common/live_mat.py +369 -0
- dstf/backends/common/mat_reader.py +632 -0
- dstf/backends/common/proc_kill.py +238 -0
- dstf/backends/common/proc_output.py +20 -0
- dstf/backends/common/result_files.py +109 -0
- dstf/backends/common/result_variables.py +329 -0
- dstf/backends/common/statistics.py +39 -0
- dstf/backends/cross_backend.py +193 -0
- dstf/backends/dymola/__init__.py +5 -0
- dstf/backends/dymola/backend.py +776 -0
- dstf/backends/dymola/interface_loader.py +241 -0
- dstf/backends/dymola/log_parser.py +396 -0
- dstf/backends/dymola/persistent_backend.py +949 -0
- dstf/backends/fmpy/__init__.py +11 -0
- dstf/backends/fmpy/backend.py +443 -0
- dstf/backends/julia/__init__.py +27 -0
- dstf/backends/julia/backend.py +156 -0
- dstf/backends/julia/persistent_backend.py +541 -0
- dstf/backends/julia/run_persistent.jl +167 -0
- dstf/backends/julia/run_test.jl +146 -0
- dstf/backends/openmodelica/__init__.py +21 -0
- dstf/backends/openmodelica/backend.py +368 -0
- dstf/backends/openmodelica/init_xml.py +76 -0
- dstf/backends/openmodelica/log_parser.py +119 -0
- dstf/backends/openmodelica/mos_generator.py +228 -0
- dstf/backends/openmodelica/persistent_backend.py +1045 -0
- dstf/backends/openmodelica/session_loader.py +63 -0
- dstf/backends/persistent.py +1811 -0
- dstf/backends/pool.py +602 -0
- dstf/backends/progress.py +395 -0
- dstf/backends/python/__init__.py +11 -0
- dstf/backends/python/backend.py +141 -0
- dstf/backends/python/run_test.py +141 -0
- dstf/backends/run_metadata.py +103 -0
- dstf/backends/types.py +310 -0
- dstf/cli/__init__.py +12 -0
- dstf/cli/author_cmd.py +61 -0
- dstf/cli/baseline_cmds.py +341 -0
- dstf/cli/common.py +231 -0
- dstf/cli/diag_cmds.py +350 -0
- dstf/cli/main.py +561 -0
- dstf/cli/review.py +398 -0
- dstf/cli/run_cmds.py +589 -0
- dstf/cli/spec_cmds.py +123 -0
- dstf/comparison/__init__.py +13 -0
- dstf/comparison/algorithms.py +2127 -0
- dstf/comparison/builtins.py +291 -0
- dstf/comparison/comparator.py +1189 -0
- dstf/comparison/geometry.py +164 -0
- dstf/comparison/handles.py +1194 -0
- dstf/comparison/metric_tree.py +332 -0
- dstf/comparison/mode_schema.py +176 -0
- dstf/comparison/modes.py +788 -0
- dstf/comparison/registry.py +230 -0
- dstf/comparison/seeds.py +281 -0
- dstf/comparison/sides.py +158 -0
- dstf/comparison/snap.py +62 -0
- dstf/comparison/strict_keys.py +246 -0
- dstf/comparison/summaries.py +354 -0
- dstf/comparison/tree_eval.py +868 -0
- dstf/comparison/tree_spec.py +678 -0
- dstf/comparison/types.py +194 -0
- dstf/comparison/validator.py +232 -0
- dstf/config.py +815 -0
- dstf/discovery/__init__.py +0 -0
- dstf/discovery/json_recognizer.py +702 -0
- dstf/discovery/library_state.py +248 -0
- dstf/discovery/mo_parser.py +794 -0
- dstf/discovery/patch_apply.py +308 -0
- dstf/discovery/patch_envelope.py +63 -0
- dstf/discovery/plots.py +318 -0
- dstf/discovery/recognizer.py +103 -0
- dstf/discovery/scan_cache.py +154 -0
- dstf/discovery/spec_parser.py +538 -0
- dstf/discovery/spec_shape.py +122 -0
- dstf/discovery/spec_update.py +493 -0
- dstf/discovery/test_registry.py +488 -0
- dstf/discovery/unit_tests.py +95 -0
- dstf/figures/__init__.py +14 -0
- dstf/figures/assemble.py +48 -0
- dstf/figures/decimate.py +150 -0
- dstf/figures/families.py +289 -0
- dstf/figures/palette.py +170 -0
- dstf/figures/plotly.py +1413 -0
- dstf/figures/view.py +233 -0
- dstf/pipeline.py +435 -0
- dstf/plugins.py +104 -0
- dstf/reporting/__init__.py +0 -0
- dstf/reporting/console_report.py +187 -0
- dstf/reporting/context_builder.py +979 -0
- dstf/reporting/dashboard_render.py +534 -0
- dstf/reporting/junit_report.py +192 -0
- dstf/reporting/overlay_loader.py +708 -0
- dstf/reporting/report_suite.py +1059 -0
- dstf/reporting/rows.py +458 -0
- dstf/reporting/schema_export.py +413 -0
- dstf/reporting/stats_rows.py +321 -0
- dstf/reporting/status.py +388 -0
- dstf/reporting/templates/axes.js +61 -0
- dstf/reporting/templates/dashboard.html +284 -0
- dstf/reporting/templates/dashboard.js +604 -0
- dstf/reporting/templates/filters.js +100 -0
- dstf/reporting/templates/interactive.html +420 -0
- dstf/reporting/templates/report.js +116 -0
- dstf/reporting/templates/vendor/plotly.min.js +8 -0
- dstf/reporting/theme.py +324 -0
- dstf/reporting/tree_rows.py +73 -0
- dstf/reporting/verdicts.py +124 -0
- dstf/storage/__init__.py +0 -0
- dstf/storage/accept.py +216 -0
- dstf/storage/baseline.py +43 -0
- dstf/storage/batch_manifest.py +204 -0
- dstf/storage/ref_index.py +295 -0
- dstf/storage/reference_store.py +661 -0
- dstf/storage/roles.py +576 -0
- dstf/storage/run_stats.py +85 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/METADATA +266 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/RECORD +185 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/WHEEL +4 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/entry_points.txt +2 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// The typed column filters: one module the dashboard and (S2) the tool's
|
|
2
|
+
// Suite table both load, so a filter means the same thing on each.
|
|
3
|
+
//
|
|
4
|
+
// DstfFilters.compile(kind, query, {regex, case}) returns null for an empty
|
|
5
|
+
// query (no filter), a predicate over a cell's value, or throws
|
|
6
|
+
// DstfFilters.BadFilter for a query it cannot read.
|
|
7
|
+
// text — regex on: the query is a regex, searched. Else a * or ? makes
|
|
8
|
+
// an anchored glob with every other character literal (so
|
|
9
|
+
// `x[1]` is an array index — the rule a spec's `variables` list
|
|
10
|
+
// and the tool's Python `_glob` use); else a substring.
|
|
11
|
+
// Case-insensitive unless `case`.
|
|
12
|
+
// number — `>1e-4`, `>=`, `<`, `<=`, `=`, a bare number (equal), or an
|
|
13
|
+
// inclusive range `1..10`. The predicate is given the raw value
|
|
14
|
+
// (a row's data-sort-<key>), never its display text. A missing
|
|
15
|
+
// value (null / '') never matches.
|
|
16
|
+
// A status column is not a typed filter: the count pills filter it.
|
|
17
|
+
//
|
|
18
|
+
// DstfFilters.known(stored, keys) is what a page keeps of a stored map of
|
|
19
|
+
// filters: the entries whose key is one of its columns.
|
|
20
|
+
//
|
|
21
|
+
// Same semantics as the Python `_glob`, not the same cost: JS globs and
|
|
22
|
+
// regexes backtrack (`_glob` exists because `*a*a*a*b` took >20 s there),
|
|
23
|
+
// so a runaway pattern can freeze the browser tab it is typed in — only
|
|
24
|
+
// that tab.
|
|
25
|
+
//
|
|
26
|
+
// dashboard.html inlines this file with a Jinja include, so it must never
|
|
27
|
+
// contain a Jinja delimiter: two open braces, or an open brace followed by
|
|
28
|
+
// a percent sign or a hash.
|
|
29
|
+
(function() {
|
|
30
|
+
class BadFilter extends Error {}
|
|
31
|
+
|
|
32
|
+
const NUM = '[+-]?(?:\\d+\\.?\\d*|\\.\\d+)(?:[eE][+-]?\\d+)?';
|
|
33
|
+
const CMP = new RegExp('^(>=|<=|>|<|=)?\\s*(' + NUM + ')$');
|
|
34
|
+
const RANGE = new RegExp('^(' + NUM + ')\\s*\\.\\.\\s*(' + NUM + ')$');
|
|
35
|
+
|
|
36
|
+
function str(v) {
|
|
37
|
+
return v === null || v === undefined ? '' : String(v);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function compileText(q, opts) {
|
|
41
|
+
const flags = opts.case ? '' : 'i';
|
|
42
|
+
if (opts.regex) {
|
|
43
|
+
let re;
|
|
44
|
+
try { re = new RegExp(q, flags); }
|
|
45
|
+
catch (e) { throw new BadFilter(e.message); }
|
|
46
|
+
return (v) => re.test(str(v));
|
|
47
|
+
}
|
|
48
|
+
if (q.includes('*') || q.includes('?')) {
|
|
49
|
+
const body = Array.from(q).map(ch =>
|
|
50
|
+
ch === '*' ? '.*' : ch === '?' ? '.' : ch.replace(/[.*+?^$()|[\]\\{}]/g, '\\$&')
|
|
51
|
+
).join('');
|
|
52
|
+
const re = new RegExp('^' + body + '$', flags);
|
|
53
|
+
return (v) => re.test(str(v));
|
|
54
|
+
}
|
|
55
|
+
const needle = opts.case ? q : q.toLowerCase();
|
|
56
|
+
return (v) => (opts.case ? str(v) : str(v).toLowerCase()).indexOf(needle) !== -1;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function toNumber(v) {
|
|
60
|
+
if (v === null || v === undefined || v === '') return null;
|
|
61
|
+
const n = typeof v === 'number' ? v : parseFloat(v);
|
|
62
|
+
return isNaN(n) ? null : n;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function compileNumber(q) {
|
|
66
|
+
const t = q.trim();
|
|
67
|
+
let m = RANGE.exec(t);
|
|
68
|
+
if (m) {
|
|
69
|
+
const lo = parseFloat(m[1]), hi = parseFloat(m[2]);
|
|
70
|
+
return (v) => { const n = toNumber(v); return n !== null && n >= lo && n <= hi; };
|
|
71
|
+
}
|
|
72
|
+
m = CMP.exec(t);
|
|
73
|
+
if (!m) throw new BadFilter('Not a comparison (>1e-4) or a range (1..10): ' + q);
|
|
74
|
+
const x = parseFloat(m[2]);
|
|
75
|
+
const test = {
|
|
76
|
+
'>': (n) => n > x, '>=': (n) => n >= x, '<': (n) => n < x,
|
|
77
|
+
'<=': (n) => n <= x, '=': (n) => n === x,
|
|
78
|
+
}[m[1] || '='];
|
|
79
|
+
return (v) => { const n = toNumber(v); return n !== null && test(n); };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function compile(kind, query, opts) {
|
|
83
|
+
if (!query) return null;
|
|
84
|
+
if (kind === 'number') return compileNumber(query);
|
|
85
|
+
return compileText(query, opts || {});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// A stored view is older than the page that reads it: `known(stored,
|
|
89
|
+
// keys)` is the part of a stored map (column -> query) whose key is a
|
|
90
|
+
// column the page has. A column renamed since would otherwise filter over
|
|
91
|
+
// nothing, with no input on the page to empty.
|
|
92
|
+
function known(stored, keys) {
|
|
93
|
+
const has = new Set(keys), out = {};
|
|
94
|
+
if (!stored || typeof stored !== 'object') return out;
|
|
95
|
+
for (const [k, v] of Object.entries(stored)) if (has.has(k)) out[k] = v;
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
window.DstfFilters = { compile, known, BadFilter };
|
|
100
|
+
})();
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>{{ model_id }} — Interactive</title>
|
|
6
|
+
{# Plotly ships with the report (report_suite.write_report_assets), not
|
|
7
|
+
from a CDN: an air-gapped lab rendered a plot-less report with no error
|
|
8
|
+
at all. Pinned to the version the CDN tag used to name — plotly.js
|
|
9
|
+
v2.35.0. ONE copy per suite, at the reports root — hence ``../``; a
|
|
10
|
+
copy per test dir cost 1.25 MB × N. Moving a single test's report dir
|
|
11
|
+
elsewhere means bringing ``plotly.min.js`` along beside it. #}
|
|
12
|
+
<script src="../plotly.min.js" charset="utf-8"></script>
|
|
13
|
+
<style>
|
|
14
|
+
{{ dstf_css|safe }}
|
|
15
|
+
* { box-sizing: border-box; }
|
|
16
|
+
body { font-family: var(--dstf-font); margin: 0; padding: 1.5em; background: var(--dstf-page); color: var(--dstf-text); }
|
|
17
|
+
h1 { margin-bottom: 0.2em; }
|
|
18
|
+
h2 { color: var(--dstf-text-secondary); border-bottom: 1px solid var(--dstf-border); padding-bottom: 0.3em; }
|
|
19
|
+
|
|
20
|
+
.summary { font-size: 1.1em; margin-bottom: 0.5em; }
|
|
21
|
+
.pass { color: var(--dstf-pass-fill); } .fail { color: var(--dstf-fail-fill); } .info { color: var(--dstf-pending-fill); }
|
|
22
|
+
/* Neutral headline for a root with no verdict to show — matches its pill,
|
|
23
|
+
which reads "—". */
|
|
24
|
+
.summary .pending { color: var(--dstf-pending-fill); }
|
|
25
|
+
|
|
26
|
+
/* Key stats */
|
|
27
|
+
.key-stats { display: flex; flex-wrap: wrap; gap: 0.8em; margin-bottom: 1.5em; }
|
|
28
|
+
.key-stat { background: var(--dstf-surface); border: 1px solid var(--dstf-border); border-radius: 4px; padding: 0.5em 1em; min-width: 120px; }
|
|
29
|
+
.key-stat.changed { border-color: var(--dstf-warn-border); background: var(--dstf-warn-tint); }
|
|
30
|
+
.key-stat .label { font-size: 0.75em; color: var(--dstf-text-muted); text-transform: uppercase; }
|
|
31
|
+
.key-stat .value { font-size: 1.1em; font-weight: 600; }
|
|
32
|
+
.key-stat .ref { font-size: 0.75em; color: var(--dstf-text-disabled); }
|
|
33
|
+
|
|
34
|
+
/* Tables */
|
|
35
|
+
table { border-collapse: collapse; width: 100%; background: var(--dstf-surface); margin-bottom: 1.5em; }
|
|
36
|
+
th, td { border: 1px solid var(--dstf-border); padding: 5px 8px; text-align: left; font-size: 0.85em; }
|
|
37
|
+
th { background: var(--dstf-surface-hover); font-weight: 600; }
|
|
38
|
+
tr:hover { background: var(--dstf-surface-hover); }
|
|
39
|
+
|
|
40
|
+
/* Plots */
|
|
41
|
+
.plot-section { margin-bottom: 1.5em; background: var(--dstf-surface); padding: 1em; border: 1px solid var(--dstf-border-hairline); border-radius: 4px; }
|
|
42
|
+
.plot-section h3 { margin-top: 0; }
|
|
43
|
+
.plotly-chart { height: 350px; --toolbar-gutter: 34px; margin-right: var(--toolbar-gutter); }
|
|
44
|
+
/* The plot's vertical toolbar stands in a gutter of its own, right of the
|
|
45
|
+
figure, instead of over the error axes' tick labels and titles: a margin
|
|
46
|
+
reserved inside the figure could not know how wide those labels grow (an
|
|
47
|
+
error around 1e-5 outgrew every reserve measured). The chart is
|
|
48
|
+
narrower than its section's content box by the gutter: its width is the
|
|
49
|
+
block's own `auto`, which the margin narrows (a `width: 100%` beside the
|
|
50
|
+
margin hung the toolbar 15 px past the section). Plotly pins
|
|
51
|
+
the toolbar's container with an inline `right: 0px`, hence the
|
|
52
|
+
`!important`. The authoring tool's plots carry the same gutter
|
|
53
|
+
(shell/style.py). The width is not a `--dstf-` token: those are the
|
|
54
|
+
theme's table (theme.py). */
|
|
55
|
+
.plotly-chart .modebar-container { right: calc(-1 * var(--toolbar-gutter)) !important; }
|
|
56
|
+
|
|
57
|
+
/* Collapsible */
|
|
58
|
+
details { margin-bottom: 1.5em; }
|
|
59
|
+
details summary { cursor: pointer; color: var(--dstf-text-secondary); font-weight: 500; padding: 0.3em 0; }
|
|
60
|
+
details summary:hover { color: var(--dstf-text); }
|
|
61
|
+
|
|
62
|
+
/* Artifacts */
|
|
63
|
+
.artifacts { margin-bottom: 1.5em; }
|
|
64
|
+
.artifacts summary { cursor: pointer; color: var(--dstf-text-secondary); font-weight: 500; }
|
|
65
|
+
.artifacts p { margin: 0.5em 0 0 1em; }
|
|
66
|
+
.artifacts a { margin-right: 0.3em; }
|
|
67
|
+
.artifacts-prominent { margin-bottom: 1.5em; padding: 1em; background: var(--dstf-warn-tint); border: 1px solid var(--dstf-warn-border); border-radius: 4px; }
|
|
68
|
+
.artifacts-prominent h2 { margin-top: 0; border: none; padding: 0; }
|
|
69
|
+
.warnings-prominent { margin-bottom: 1.5em; padding: 1em; background: var(--dstf-warn-tint); border: 1px solid var(--dstf-warn-border); border-radius: 4px; }
|
|
70
|
+
.warnings-prominent h2 { margin-top: 0; border: none; padding: 0 0 0.4em 0; }
|
|
71
|
+
.warnings-prominent table { margin: 0; }
|
|
72
|
+
.warnings-prominent .warning-statement { margin: 0 0 0.6em 0; }
|
|
73
|
+
.meta-table { width: auto; min-width: 400px; }
|
|
74
|
+
.meta-table td:first-child { font-weight: 500; white-space: nowrap; }
|
|
75
|
+
/* Advisory (warn-wrapped) failure marker — C-v item (c). A leaf under a
|
|
76
|
+
warn wrapper cannot fail the root, so it must not paint the summary or
|
|
77
|
+
the variable pill red; it gets this amber chip instead. */
|
|
78
|
+
.advisory-chip { display: inline-block; margin-left: 0.5em; padding: 0.05em 0.45em; border-radius: 3px; background: var(--dstf-warn-tint); border: 1px solid var(--dstf-warn-border); color: var(--dstf-warn-dark); font-size: 0.75em; font-weight: 600; vertical-align: middle; }
|
|
79
|
+
/* A2 / idea #50 — per-plot overlay picker (companions + soft_checks) */
|
|
80
|
+
.overlay-picker { display: flex; flex-wrap: wrap; gap: 0.5em 1em; align-items: center; font-size: 0.8em; margin: 0.2em 0 0.5em 0; padding: 0.3em 0.6em; background: var(--dstf-surface-hover); border: 1px dashed var(--dstf-border-hairline); border-radius: 4px; }
|
|
81
|
+
.overlay-picker .ov-label { color: var(--dstf-text-secondary); font-weight: 500; }
|
|
82
|
+
.overlay-picker .ov-item { display: inline-flex; align-items: center; gap: 0.25em; cursor: pointer; }
|
|
83
|
+
.overlay-picker .ov-role { display: inline-block; padding: 0 0.3em; border-radius: 3px; font-size: 0.72em; text-transform: uppercase; }
|
|
84
|
+
.overlay-picker .ov-soft_check { background: var(--dstf-role-soft_check-bg); color: var(--dstf-role-soft_check-fg); border: 1px solid var(--dstf-role-soft_check-border); }
|
|
85
|
+
.overlay-picker .ov-companion { background: var(--dstf-role-companion-bg); color: var(--dstf-role-companion-fg); border: 1px solid var(--dstf-role-companion-border); }
|
|
86
|
+
/* A spelling that could stand for two of an overlay's variables: nothing
|
|
87
|
+
is drawn for it, and this row says which mapping would settle it. */
|
|
88
|
+
.ov-warning { margin-top: 0.2em; }
|
|
89
|
+
/* Stage 2 — recursive SpecNodeView */
|
|
90
|
+
.variable-section { margin-bottom: 2em; padding: 1em; background: var(--dstf-surface); border: 1px solid var(--dstf-border); border-radius: 4px; }
|
|
91
|
+
.variable-section h2 { margin-top: 0; border-bottom: 1px solid var(--dstf-border-hairline); padding-bottom: 0.3em; }
|
|
92
|
+
.variable-section .var-status { display: inline-block; padding: 0.1em 0.5em; border-radius: 3px; font-size: 0.7em; font-weight: 600; letter-spacing: 0.05em; margin-right: 0.5em; vertical-align: middle; }
|
|
93
|
+
.variable-section .var-status.pass { background: var(--dstf-pass-pill); color: var(--dstf-pass-dark); border: 1px solid var(--dstf-pass-border); }
|
|
94
|
+
.variable-section .var-status.fail { background: var(--dstf-fail-pill); color: var(--dstf-fail-dark); border: 1px solid var(--dstf-fail-border); }
|
|
95
|
+
.node-tree { margin-top: 0.8em; }
|
|
96
|
+
.node { margin: 0.3em 0; padding: 0.4em 0.6em; border-left: 3px solid var(--dstf-border); background: var(--dstf-page); border-radius: 0 3px 3px 0; }
|
|
97
|
+
.node-leaf { border-left-color: var(--dstf-accent); }
|
|
98
|
+
.node-combinator { border-left-color: var(--dstf-text-secondary); background: var(--dstf-surface-hover); }
|
|
99
|
+
.node-children { margin-left: 1.2em; margin-top: 0.3em; }
|
|
100
|
+
.node-header { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4em; font-size: 0.9em; }
|
|
101
|
+
.node-label { font-weight: 500; color: var(--dstf-text); }
|
|
102
|
+
.node-score { font-size: 0.85em; color: var(--dstf-text-secondary); font-family: var(--dstf-mono); }
|
|
103
|
+
/* One width, its text centred: PASS, FAIL and the pending dash, so a plot
|
|
104
|
+
section's leaf names start in one column (D140; the tool's `.dstf-pill`
|
|
105
|
+
keeps the same). */
|
|
106
|
+
.node-status { display: inline-block; padding: 0.05em 0.4em; border-radius: 3px; font-size: 0.7em; font-weight: 600; letter-spacing: 0.05em; min-width: 4em; text-align: center; }
|
|
107
|
+
.node-status.pass { background: var(--dstf-pass-pill); color: var(--dstf-pass-dark); }
|
|
108
|
+
.node-status.fail { background: var(--dstf-fail-pill); color: var(--dstf-fail-dark); }
|
|
109
|
+
.node-status.pending { background: var(--dstf-pending-pill); color: var(--dstf-pending-fill); }
|
|
110
|
+
.node-child-count { font-size: 0.8em; color: var(--dstf-text-muted); font-family: var(--dstf-mono); }
|
|
111
|
+
.node-criterion { font-size: 0.82em; color: var(--dstf-text-secondary); margin-top: 0.15em; padding-left: 0.2em; }
|
|
112
|
+
.node-details { display: grid; grid-template-columns: auto 1fr; gap: 0.05em 0.6em; margin: 0.3em 0 0 0.2em; font-size: 0.8em; }
|
|
113
|
+
.node-details dt { color: var(--dstf-text-muted); }
|
|
114
|
+
.node-details dd { margin: 0; color: var(--dstf-text); font-family: var(--dstf-mono); }
|
|
115
|
+
.node-combinator-params { display: inline-flex; gap: 0.4em; align-items: center; margin-left: 0.3em; padding-left: 0.4em; border-left: 1px dashed var(--dstf-text-muted); font-size: 0.82em; }
|
|
116
|
+
.metric-tree-section { margin-bottom: 1.5em; padding: 1em; background: var(--dstf-surface); border: 1px solid var(--dstf-border); border-radius: 4px; }
|
|
117
|
+
.metric-tree-section summary { font-weight: 600; cursor: pointer; }
|
|
118
|
+
.node-tree-hint { font-weight: 400; color: var(--dstf-text-muted); font-size: 0.85em; }
|
|
119
|
+
/* Reason a sentinel leaf could not be scored (misconfigured metric, empty window) */
|
|
120
|
+
.node-error { margin-top: 0.3em; padding-left: 1.4em; color: var(--dstf-sim-fail-dark); font-size: 0.8em; }
|
|
121
|
+
/* Per-plot controls row */
|
|
122
|
+
.plot-controls { display: flex; gap: 1em; align-items: center; margin-bottom: 0.5em; font-size: 0.85em; color: var(--dstf-text-secondary); }
|
|
123
|
+
.plot-controls label { display: inline-flex; align-items: center; gap: 0.4em; }
|
|
124
|
+
.error-overlay-select { padding: 0.15em 0.3em; border: 1px solid var(--dstf-border); border-radius: 3px; font-family: inherit; font-size: 0.85em; }
|
|
125
|
+
/* A line the plot itself cannot say: a missing series, or a watch plot
|
|
126
|
+
no criterion scores yet. */
|
|
127
|
+
.plot-note { font-size: 0.85em; color: var(--dstf-text-secondary); margin-bottom: 0.5em; }
|
|
128
|
+
/* A plot's context series — drawn grey beside its subject, never scored on
|
|
129
|
+
this plot — listed muted above the plot. */
|
|
130
|
+
.plot-context { font-size: 0.85em; color: var(--dstf-text-muted); margin-bottom: 0.5em; }
|
|
131
|
+
/* A plot's criteria, in tree order: each row led by its ancestors' kinds
|
|
132
|
+
(`and › k-of-n ›`), muted — where in the tree the leaf sits. Each crumb's
|
|
133
|
+
minimum width is its plot's longest crumb, in ch (the section's
|
|
134
|
+
`crumb_ch`), so a section's leaf names start in one column whatever its
|
|
135
|
+
rows' depths — a fixed width was outgrown by a fourth level (the tool's
|
|
136
|
+
`.dstf-crumb` keeps the same). */
|
|
137
|
+
.criteria-row { display: flex; align-items: baseline; gap: 0.5em; }
|
|
138
|
+
.criteria-row > .node { flex: 1; min-width: 0; }
|
|
139
|
+
.node-crumbs { color: var(--dstf-text-muted); font-size: 0.8em; white-space: nowrap; }
|
|
140
|
+
/* A variable reads as the expression its model wrote (`pipe.T[1]`); its
|
|
141
|
+
result-file name — what a criterion, an override or a --filter is written
|
|
142
|
+
against — rides beside the title, muted. Absent when the two agree. */
|
|
143
|
+
.dstf-varname { color: var(--dstf-text-muted); font-weight: 400; font-size: 0.8em; font-family: var(--dstf-mono); margin-left: 0.4em; }
|
|
144
|
+
/* Decimation disclosure — which traces the page draws at reduced
|
|
145
|
+
resolution, and where the full-resolution data lives. */
|
|
146
|
+
#decimation-banner { background: var(--dstf-warn-tint); border: 1px solid var(--dstf-warn-border); border-radius: 4px; color: var(--dstf-warn-dark); padding: 8px 12px; margin: 0 0 12px 0; font-size: 0.85em; }
|
|
147
|
+
#decimation-banner .dec-trace { font-family: var(--dstf-mono); white-space: nowrap; }
|
|
148
|
+
</style>
|
|
149
|
+
</head>
|
|
150
|
+
<body>
|
|
151
|
+
|
|
152
|
+
{# === SERVER-RENDERED METRIC TREE ==========================================
|
|
153
|
+
The tree is a read-only view of what the CLI already decided: every pill,
|
|
154
|
+
score, criterion and detail pair below comes from the evaluated tree view.
|
|
155
|
+
No form controls — editing a spec is the authoring tool's job, not the
|
|
156
|
+
report's. #}
|
|
157
|
+
{% macro status_pill(passed) %}{% if passed is sameas true %}<span class="node-status pass">PASS</span>{% elif passed is sameas false %}<span class="node-status fail">FAIL</span>{% else %}<span class="node-status pending">—</span>{% endif %}{% endmacro %}
|
|
158
|
+
{% macro advisory_chip(count, vidx=None) %}<span class="advisory-chip"{% if vidx is not none %} data-vidx="{{ vidx }}"{% endif %} title="{% if count == 1 %}One failing leaf sits under a warn wrapper — advisory only; it does not fail the test.{% else %}{{ count }} failing leaves sit under a warn wrapper — advisory only; they do not fail the test.{% endif %}">{% if count == 1 %}1 advisory{% else %}{{ count }} advisory{% endif %}</span>{% endmacro %}
|
|
159
|
+
{% macro node(n) %}
|
|
160
|
+
{%- set combinator_help = {
|
|
161
|
+
'and': 'All children must pass.',
|
|
162
|
+
'or': 'At least one child must pass.',
|
|
163
|
+
'warn': 'Single-child wrapper; never fails its parent (advisory-only scoring).',
|
|
164
|
+
'k-of-n': 'At least k of N children must pass.',
|
|
165
|
+
'weighted': 'Weighted sum of child scores passes a threshold.'} -%}
|
|
166
|
+
{%- if n.kind == 'leaf' -%}
|
|
167
|
+
<div class="node node-leaf" data-path="{{ n.path }}">
|
|
168
|
+
<div class="node-header">{{ status_pill(n.passed) }}<span class="node-label">{{ n.metric }} · {{ n.label or n.variable }}{% if n.against and n.against != 'primary' %} [against={{ n.against }}]{% endif %}{% if n.window and (n.window.get('start') is not none or n.window.get('end') is not none) %} [window{% if n.window.get('start') is not none %} start={{ n.window.get('start') }}{% endif %}{% if n.window.get('end') is not none %} end={{ n.window.get('end') }}{% endif %}]{% endif %}</span>{% if n.score_display %}<span class="node-score"{% if n.error %} title="{{ n.error }}"{% endif %}>{{ n.score_display }}</span>{% endif %}</div>
|
|
169
|
+
{%- if n.criterion %}<div class="node-criterion">{{ n.criterion }}</div>{% endif %}
|
|
170
|
+
{%- if n.error %}<div class="node-error">{{ n.error }}</div>{% endif %}
|
|
171
|
+
{%- if n.details %}<dl class="node-details">{% for pair in n.details %}<dt>{{ pair[0] }}</dt><dd>{{ pair[1] }}</dd>{% endfor %}</dl>{% endif %}
|
|
172
|
+
</div>
|
|
173
|
+
{%- else -%}
|
|
174
|
+
<div class="node node-combinator" data-path="{{ n.path }}">
|
|
175
|
+
<div class="node-header">{{ status_pill(n.passed) }}<span class="node-label" title="{{ combinator_help.get(n.combinator, '') }}">{{ n.combinator }}</span><span class="node-child-count">[{{ (n.children or []) | length }}]</span>{% if n.get('k') is not none or n.get('threshold') is not none or n.get('weights') %}<span class="node-combinator-params">{% if n.get('k') is not none %}k={{ n.get('k') }}{% endif %}{% if n.get('weights') %} weights={{ n.get('weights') | join(', ') }}{% endif %}{% if n.get('threshold') is not none %} threshold={{ n.get('threshold') }}{% endif %}</span>{% endif %}</div>
|
|
176
|
+
<div class="node-children">{% for child in n.children or [] %}{{ node(child) }}{% endfor %}</div>
|
|
177
|
+
</div>
|
|
178
|
+
{%- endif -%}
|
|
179
|
+
{% endmacro %}
|
|
180
|
+
|
|
181
|
+
{# === DECIMATION DISCLOSURE ===
|
|
182
|
+
Server-rendered from the figures' ``full_samples`` stamps: a trace whose
|
|
183
|
+
stamp exceeds the samples it carries was thinned to fit the page. The
|
|
184
|
+
sidecar holds the full-resolution INPUT trajectories (actual, reference,
|
|
185
|
+
overlays) — derived traces such as bands, error series and spectra are
|
|
186
|
+
not in it, so the banner promises only the inputs. #}
|
|
187
|
+
{% if decimated %}
|
|
188
|
+
<div id="decimation-banner">
|
|
189
|
+
<strong>{{ decimated | length }}</strong> trace{{ '' if decimated | length == 1 else 's' }} decimated to fit this page — the full-resolution input trajectories are in <code>comparison_data.json</code>:
|
|
190
|
+
{% for d in decimated %}<span class="dec-trace">{{ d.variable }} {{ d.trace }} {{ d.full }}→{{ d.embedded }}</span>{% if not loop.last %}, {% endif %}{% endfor %}
|
|
191
|
+
</div>
|
|
192
|
+
{% endif %}
|
|
193
|
+
|
|
194
|
+
<h1>{{ model_id }}</h1>
|
|
195
|
+
{% if run_metadata %}<div class="run-meta" style="font-size:0.85em; color: var(--dstf-text-muted); margin-bottom:0.25em;"><strong>{{ run_metadata.label }}</strong>{% if run_metadata.tool_version and run_metadata.tool_version != run_metadata.label %} <span title="version reported by the backend">({{ run_metadata.tool_version }})</span>{% endif %}{% if run_metadata.msl %} · MSL {{ run_metadata.msl }}{% endif %}{% if run_metadata.os %} · {{ run_metadata.os }}{% endif %}{% if run_metadata.dstf_version %} · DSTF {{ run_metadata.dstf_version }}{% endif %}{% if run_metadata.generated_str %} · {{ run_metadata.generated_str }}{% endif %}</div>{% endif %}
|
|
196
|
+
{% if last_run_str %}<div style="font-size:0.85em; color: var(--dstf-text-muted); margin-bottom:0.5em;">Last run: {{ last_run_str }}</div>{% endif %}
|
|
197
|
+
|
|
198
|
+
{# === SUMMARY === #}
|
|
199
|
+
<div class="summary" id="summary">
|
|
200
|
+
{# A refused test FIRST: it has no scored variable, so the branches below
|
|
201
|
+
read it as a test with no baseline — whether or not it has one. -#}
|
|
202
|
+
{% if refused %}
|
|
203
|
+
<span class="fail"><strong>Not scored:</strong> {{ refused }}</span>
|
|
204
|
+
{% elif is_simulate_only %}
|
|
205
|
+
<span class="pass"><strong>Simulate-only:</strong> simulation succeeded</span>
|
|
206
|
+
{% elif nobaseline_trajectories %}
|
|
207
|
+
<strong>{{ nobaseline_trajectories | length }}</strong> variables (no baseline)
|
|
208
|
+
{% elif sim_failed %}
|
|
209
|
+
<span class="fail"><strong>Simulation failed</strong></span>
|
|
210
|
+
{% else %}
|
|
211
|
+
{% if tree_summary %}<span id="summary-text" class="{{ 'pending' if tree_summary.root_passed is not sameas true and tree_summary.root_passed is not sameas false else ('pass' if tree_summary.root_passed else 'fail') }}"><strong>{{ tree_summary.n_pass }}</strong> / <strong>{{ tree_summary.n_total }}</strong> leaves passed{% if tree_summary.root_passed is not sameas true and tree_summary.root_passed is not sameas false %} · root verdict pending — run dstf run to evaluate{% endif %}{% if tree_summary.n_advisory_failed %}{{ advisory_chip(tree_summary.n_advisory_failed) }}{% endif %}</span>{% else %}<span id="summary-text"></span>{% endif %}
|
|
212
|
+
{% endif %}
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
{# === KEY STATS === #}
|
|
216
|
+
{% if key_stats %}
|
|
217
|
+
<div class="key-stats">
|
|
218
|
+
{% for s in key_stats %}
|
|
219
|
+
<div class="key-stat{{ ' changed' if s.changed | default(false) else '' }}" {% if s.label == 'Worst NRMSE' %}id="key-stat-nrmse"{% endif %}>
|
|
220
|
+
<div class="label">{{ s.label }}</div>
|
|
221
|
+
<div class="value">{{ s.current or "\u2014" }}</div>
|
|
222
|
+
{% if s.reference %}<div class="ref">ref: {{ s.reference }}</div>{% endif %}
|
|
223
|
+
</div>
|
|
224
|
+
{% endfor %}
|
|
225
|
+
</div>
|
|
226
|
+
{% endif %}
|
|
227
|
+
|
|
228
|
+
{# === STRUCTURAL WARNINGS === #}
|
|
229
|
+
{% if warnings %}
|
|
230
|
+
<div class="warnings-prominent">
|
|
231
|
+
<h2>Structural Warnings ({{ warnings | length }})</h2>
|
|
232
|
+
{# A statement about this run is a sentence; only a change from the
|
|
233
|
+
baseline has a Reference and a Current to put in columns. #}
|
|
234
|
+
{% for w in warnings if not w.is_change %}
|
|
235
|
+
<p class="warning-statement">{{ w.text }}</p>
|
|
236
|
+
{% endfor %}
|
|
237
|
+
{% set changes = warnings | selectattr('is_change') | list %}
|
|
238
|
+
{% if changes %}
|
|
239
|
+
<table>
|
|
240
|
+
<tr><th>Field</th><th>Reference</th><th>Current</th></tr>
|
|
241
|
+
{% for w in changes %}
|
|
242
|
+
<tr><td>{{ w.field }}</td><td>{{ w.reference }}</td><td>{{ w.current }}</td></tr>
|
|
243
|
+
{% endfor %}
|
|
244
|
+
</table>
|
|
245
|
+
{% endif %}
|
|
246
|
+
</div>
|
|
247
|
+
{% endif %}
|
|
248
|
+
|
|
249
|
+
{# === REFERENCE INFO (moved up, after summary) === #}
|
|
250
|
+
{% if ref_info %}
|
|
251
|
+
<details class="artifacts">
|
|
252
|
+
<summary>Reference Info</summary>
|
|
253
|
+
<table class="meta-table">
|
|
254
|
+
<tr><th>Field</th><th>Value</th></tr>
|
|
255
|
+
{% for row in ref_info %}<tr><td>{{ row.label }}</td><td>{% if row.link %}<a href="{{ row.link }}">{{ row.value }}</a>{% else %}{{ row.value }}{% endif %}</td></tr>{% endfor %}
|
|
256
|
+
</table>
|
|
257
|
+
</details>
|
|
258
|
+
{% endif %}
|
|
259
|
+
|
|
260
|
+
{# === ARTIFACT LINKS === #}
|
|
261
|
+
{% if artifacts %}
|
|
262
|
+
{% if sim_failed %}
|
|
263
|
+
<div class="artifacts-prominent">
|
|
264
|
+
<h2>Simulation Artifacts</h2>
|
|
265
|
+
<p>{% for a in artifacts %}<a href="{{ a.uri }}">{{ a.label }}</a>{% if not loop.last %} · {% endif %}{% endfor %}</p>
|
|
266
|
+
</div>
|
|
267
|
+
{% else %}
|
|
268
|
+
<details class="artifacts">
|
|
269
|
+
<summary>Simulation Artifacts</summary>
|
|
270
|
+
<p>{% for a in artifacts %}<a href="{{ a.uri }}">{{ a.label }}</a>{% if not loop.last %} · {% endif %}{% endfor %}</p>
|
|
271
|
+
</details>
|
|
272
|
+
{% endif %}
|
|
273
|
+
{% endif %}
|
|
274
|
+
|
|
275
|
+
{# === SIM PARAMS (collapsible, moved up) === #}
|
|
276
|
+
{% if sim_params %}
|
|
277
|
+
<details>
|
|
278
|
+
<summary>Simulation Parameters</summary>
|
|
279
|
+
<table class="meta-table"><tr><th>Parameter</th><th>Current</th><th>Reference</th></tr>
|
|
280
|
+
{% for row in sim_params %}
|
|
281
|
+
<tr{% if row.changed %} style="background: var(--dstf-warn-tint)"{% endif %}><td>{{ row.label }}</td><td>{{ row.current }}</td><td>{{ row.reference }}</td></tr>
|
|
282
|
+
{% endfor %}
|
|
283
|
+
</table>
|
|
284
|
+
</details>
|
|
285
|
+
{% endif %}
|
|
286
|
+
|
|
287
|
+
{# === OVERLAYS SUMMARY — A2 / idea #50 === #}
|
|
288
|
+
{% if overlay_rows %}
|
|
289
|
+
<details class="artifacts">
|
|
290
|
+
<summary>Overlays ({{ overlay_rows | length }}) — companion + soft_check</summary>
|
|
291
|
+
<table class="meta-table">
|
|
292
|
+
<tr><th>Name</th><th>Role</th><th>Status</th><th>Variables</th><th>Note</th></tr>
|
|
293
|
+
{% for ov in overlay_rows %}
|
|
294
|
+
<tr{% if ov.status != 'loaded' or ov.warnings %} style="background: var(--dstf-warn-tint)"{% endif %}>
|
|
295
|
+
<td>{{ ov.name }}</td>
|
|
296
|
+
<td>{{ ov.role }}{% if ov.kind %} ({{ ov.kind }}){% endif %}</td>
|
|
297
|
+
<td>{{ ov.status }}</td>
|
|
298
|
+
<td>{% if ov.variables %}{{ ov.variables | join(", ") }}{% else %}—{% endif %}</td>
|
|
299
|
+
<td style="font-size:0.8em; color: var(--dstf-text-secondary)">{{ ov.note }}{% for warning in ov.warnings %}<div class="ov-warning">{{ warning }}</div>{% endfor %}</td>
|
|
300
|
+
</tr>
|
|
301
|
+
{% endfor %}
|
|
302
|
+
</table>
|
|
303
|
+
<p style="font-size:0.8em; color: var(--dstf-text-muted); margin-top:0.5em;">
|
|
304
|
+
Toggle per-variable visibility with the <code>Overlays:</code> checkboxes above each plot.
|
|
305
|
+
Companions are visual-only; soft_checks are warn-wrapped scorable baselines.
|
|
306
|
+
</p>
|
|
307
|
+
</details>
|
|
308
|
+
{% endif %}
|
|
309
|
+
|
|
310
|
+
{# === STATISTICS (collapsible, moved up) === #}
|
|
311
|
+
{% if statistics_sections %}
|
|
312
|
+
<details>
|
|
313
|
+
<summary>Statistics ({{ statistics_sections | length }} sections)</summary>
|
|
314
|
+
{% for section in statistics_sections %}
|
|
315
|
+
<h3>{{ section.title }}</h3>
|
|
316
|
+
<table>
|
|
317
|
+
<tr><th>Metric</th><th>Current</th><th>Reference</th></tr>
|
|
318
|
+
{% for row in section.rows %}
|
|
319
|
+
<tr{% if row.changed %} style="background: var(--dstf-warn-tint)"{% endif %}><td>{{ row.label }}</td><td>{{ row.current }}</td><td>{{ row.reference }}</td></tr>
|
|
320
|
+
{% endfor %}
|
|
321
|
+
</table>
|
|
322
|
+
{% endfor %}
|
|
323
|
+
</details>
|
|
324
|
+
{% endif %}
|
|
325
|
+
|
|
326
|
+
{# === DIAGNOSTIC PLOTS (moved up, above variables) === #}
|
|
327
|
+
{% if diag_trajectories %}
|
|
328
|
+
<details>
|
|
329
|
+
<summary>Diagnostics ({{ diag_trajectories | length }} variables)</summary>
|
|
330
|
+
{% for traj in diag_trajectories %}
|
|
331
|
+
<div class="plot-section">
|
|
332
|
+
<h3><span class="info">INFO</span> {{ traj.name }}</h3>
|
|
333
|
+
<div class="plotly-chart" id="diag-plot-{{ loop.index0 }}"></div>
|
|
334
|
+
</div>
|
|
335
|
+
{% endfor %}
|
|
336
|
+
</details>
|
|
337
|
+
{% endif %}
|
|
338
|
+
|
|
339
|
+
{# === METRIC TREE (Stage 3 — full unfiltered SpecNodeView mount) === #}
|
|
340
|
+
{% if tree_view %}
|
|
341
|
+
<details class="metric-tree-section" open>
|
|
342
|
+
<summary>Metric Tree <span class="node-tree-hint">— every leaf + combinator, as the CLI scored it</span></summary>
|
|
343
|
+
<div class="node-tree" id="nodes-full">{{ node(tree_view) }}</div>
|
|
344
|
+
</details>
|
|
345
|
+
{% endif %}
|
|
346
|
+
|
|
347
|
+
{# === PLOT SECTIONS — one per plot, derived from the criteria tree ===
|
|
348
|
+
One per scored variable or array family, in the order the tree first
|
|
349
|
+
names it, then the watch plots; nothing scored is one per tracked
|
|
350
|
+
variable. Each lists the criteria on its subject in tree order, each led
|
|
351
|
+
by its ancestors' kinds, and its context series (drawn grey; a
|
|
352
|
+
context series may be scored on a plot of its own). A family's badges
|
|
353
|
+
name their members, as the tool's pills do. The error select shows when
|
|
354
|
+
the subject is ONE scored variable the result carries, the overlay
|
|
355
|
+
picker when that one variable has overlays. #}
|
|
356
|
+
{% if plot_sections %}
|
|
357
|
+
{% for plot in plot_sections %}
|
|
358
|
+
<div class="variable-section plot-section-block" data-plot="{{ plot.index }}" id="variable-section-{{ plot.index }}">
|
|
359
|
+
<h2>
|
|
360
|
+
{%- for v in plot.variables -%}
|
|
361
|
+
<span class="var-status {{ 'pass' if v.passed else 'fail' }}" data-vidx="{{ v.vidx }}">{% if not plot.single %}{{ v.text or v.name }} {% endif %}{{ 'PASS' if v.passed else 'FAIL' }}</span>{% if v.n_advisory_failed %}{{ advisory_chip(v.n_advisory_failed, v.vidx) }}{% endif %}
|
|
362
|
+
{%- endfor %}
|
|
363
|
+
{{ plot.title }}{% if plot.names %}<span class="dstf-varname">{{ plot.names }}</span>{% endif %}
|
|
364
|
+
</h2>
|
|
365
|
+
{% if plot.errors %}
|
|
366
|
+
<div class="plot-controls">
|
|
367
|
+
<label class="error-overlay-control" title="Overlay an error trace on the secondary y-axis">
|
|
368
|
+
Error overlay
|
|
369
|
+
<select class="error-overlay-select" data-plot="{{ plot.index }}">
|
|
370
|
+
<option value="none">none</option>
|
|
371
|
+
<option value="signed">signed error (act - ref)</option>
|
|
372
|
+
<option value="abs">abs error</option>
|
|
373
|
+
<option value="nrmse">NRMSE</option>
|
|
374
|
+
</select>
|
|
375
|
+
</label>
|
|
376
|
+
</div>
|
|
377
|
+
{% endif %}
|
|
378
|
+
{% if plot.overlays %}
|
|
379
|
+
<div class="overlay-picker" data-plot-id="plot-{{ plot.index }}" title="Companion + soft_check overlays (view-only; never scored unless declared in a warn-wrapped leaf)">
|
|
380
|
+
<span class="ov-label">Overlays:</span>
|
|
381
|
+
{% for ov in plot.overlays %}
|
|
382
|
+
<label class="ov-item"><input type="checkbox" class="overlay-toggle" data-ov-name="{{ ov.name }}" data-ov-role="{{ ov.role }}"> <span class="ov-role ov-{{ ov.role }}">{{ ov.role }}</span> {{ ov.name }}</label>
|
|
383
|
+
{% endfor %}
|
|
384
|
+
</div>
|
|
385
|
+
{% endif %}
|
|
386
|
+
{% if plot.context %}<div class="plot-context">Context: {{ plot.context | join(", ") }}</div>{% endif %}
|
|
387
|
+
{% if plot.missing %}<div class="plot-note">Not in this result: {{ plot.missing | join(", ") }}</div>{% endif %}
|
|
388
|
+
{#- A subject the result lacks entirely draws no chart: an empty one
|
|
389
|
+
said nothing the note does not. #}
|
|
390
|
+
{% if plot.subject %}<div class="plotly-chart plot-main" id="plot-{{ plot.index }}" style="height:300px"></div>{% endif %}
|
|
391
|
+
{% if plot.criteria %}
|
|
392
|
+
<div class="node-tree plot-criteria" id="nodes-{{ plot.index }}">
|
|
393
|
+
{%- for row in plot.criteria %}
|
|
394
|
+
<div class="criteria-row" data-variable="{{ row.variable }}">{% if row.crumbs %}<span class="node-crumbs" style="min-width: {{ plot.crumb_ch }}ch">{{ row.crumbs }}</span>{% endif %}{{ node(row.leaf) }}</div>
|
|
395
|
+
{%- endfor %}
|
|
396
|
+
</div>
|
|
397
|
+
{% elif plot.watch %}
|
|
398
|
+
<div class="plot-note">No criteria yet.</div>
|
|
399
|
+
{% endif %}
|
|
400
|
+
</div>
|
|
401
|
+
{% endfor %}
|
|
402
|
+
{% endif %}
|
|
403
|
+
|
|
404
|
+
<script>
|
|
405
|
+
// Data marshalling only — the figure dicts and the order the plot elements
|
|
406
|
+
// were numbered in. Everything else on this page is server-rendered markup.
|
|
407
|
+
window.MT_REPORT = {
|
|
408
|
+
{# review 2026-07-06 finding 76h: a LIST, one entry per plot section, in
|
|
409
|
+
the template's own order — JS Object.keys() reorders integer-like keys
|
|
410
|
+
numerically, which desynced plot element IDs from that order. #}
|
|
411
|
+
PLOT_SERIES: {{ plot_series | tojson }},
|
|
412
|
+
{# Figure dicts from dstf.figures — one per plot section, verbatim. #}
|
|
413
|
+
PLOT_FIGURES: {{ (plot_figures or []) | tojson }},
|
|
414
|
+
DIAG_FIGURES: {{ (diag_figures or []) | tojson }},
|
|
415
|
+
};
|
|
416
|
+
</script>
|
|
417
|
+
<script src="interactive.js"></script>
|
|
418
|
+
|
|
419
|
+
</body>
|
|
420
|
+
</html>
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// ---- interactive.js (report.js) ----
|
|
2
|
+
// The report page's ONE script. It paints the figure dicts Python built
|
|
3
|
+
// and wires the two view controls that need JavaScript at all — the
|
|
4
|
+
// overlay picker and the error-overlay dropdown, both of which only
|
|
5
|
+
// toggle the visibility of traces that are already in the figure.
|
|
6
|
+
//
|
|
7
|
+
// It computes nothing. No verdict, no bound, no box, no spectrum, no
|
|
8
|
+
// event: every number on the page is either inside a figure dict or was
|
|
9
|
+
// rendered into the markup by Jinja. Anything that needs to be computed
|
|
10
|
+
// belongs in `dstf.figures` (drawing) or the authoring tool (editing).
|
|
11
|
+
|
|
12
|
+
// ---- Embedded data ----
|
|
13
|
+
// interactive.html inlines the figure dicts into window.MT_REPORT (one
|
|
14
|
+
// <script> block), then loads this file via <script src>. Keeps the JS
|
|
15
|
+
// pure-static and the template responsible for data marshalling.
|
|
16
|
+
const MT_REPORT = (typeof window !== 'undefined' && window.MT_REPORT) || {};
|
|
17
|
+
// One figure per PLOT SECTION, in the template's own order — the page is
|
|
18
|
+
// laid out per plot, not per variable, and a plot may draw several series.
|
|
19
|
+
const PLOT_FIGURES = MT_REPORT.PLOT_FIGURES || [];
|
|
20
|
+
// Each plot's series names, in order (the browser tests read it).
|
|
21
|
+
const PLOT_SERIES = MT_REPORT.PLOT_SERIES || [];
|
|
22
|
+
const DIAG_FIGURES = MT_REPORT.DIAG_FIGURES || [];
|
|
23
|
+
|
|
24
|
+
// ---- Plotly rendering ----
|
|
25
|
+
const PLOT_CFG = {
|
|
26
|
+
responsive: true,
|
|
27
|
+
displaylogo: false,
|
|
28
|
+
scrollZoom: true,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function paintFigure(el, fig) {
|
|
32
|
+
if (!el || !fig) return;
|
|
33
|
+
Plotly.newPlot(el, fig.data, fig.layout, PLOT_CFG);
|
|
34
|
+
// The marker the browser tests read: this plot is the Python figure
|
|
35
|
+
// dict, verbatim, and nothing rebuilt it.
|
|
36
|
+
el._mt_python = true;
|
|
37
|
+
dstfWireSecondaryAxes(el);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function renderAllPlots() {
|
|
41
|
+
PLOT_FIGURES.forEach((fig, idx) => {
|
|
42
|
+
paintFigure(document.getElementById(`plot-${idx}`), fig);
|
|
43
|
+
});
|
|
44
|
+
DIAG_FIGURES.forEach((fig, idx) => {
|
|
45
|
+
paintFigure(document.getElementById(`diag-plot-${idx}`), fig);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---- Overlay picker ----
|
|
50
|
+
// Each .overlay-picker div declares which Plotly chart it drives via its
|
|
51
|
+
// data-plot-id attribute (plot-N), so one handler serves every plot.
|
|
52
|
+
function overlayTraceName(role, name) {
|
|
53
|
+
return `Overlay: ${role}/${name}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function wireOverlayPickers() {
|
|
57
|
+
document.querySelectorAll('.overlay-picker .overlay-toggle').forEach(cb => {
|
|
58
|
+
cb.addEventListener('change', (e) => {
|
|
59
|
+
const picker = e.target.closest('.overlay-picker');
|
|
60
|
+
if (!picker) return;
|
|
61
|
+
const plotId = picker.dataset.plotId;
|
|
62
|
+
if (!plotId) return;
|
|
63
|
+
const name = e.target.dataset.ovName;
|
|
64
|
+
const role = e.target.dataset.ovRole;
|
|
65
|
+
setOverlayVisible(plotId, role, name, e.target.checked);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function setOverlayVisible(plotId, role, name, visible) {
|
|
71
|
+
const el = document.getElementById(plotId);
|
|
72
|
+
if (!el || !el.data) return;
|
|
73
|
+
const traceName = overlayTraceName(role, name);
|
|
74
|
+
for (let i = 0; i < el.data.length; i++) {
|
|
75
|
+
if (el.data[i].name === traceName) {
|
|
76
|
+
Plotly.restyle(el, { visible: visible ? true : 'legendonly' }, [i]);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---- Error overlays (signed / abs on the error axis, NRMSE on its own; axes.js shows the axis) ----
|
|
83
|
+
// The three error series ride into the figure as legend-only traces on
|
|
84
|
+
// yaxis3 / yaxis4; the dropdown only decides which one is visible.
|
|
85
|
+
function wireErrorOverlays() {
|
|
86
|
+
document.querySelectorAll('.error-overlay-select').forEach(sel => {
|
|
87
|
+
sel.addEventListener('change', (e) => {
|
|
88
|
+
// `data-plot`, not `data-vidx`: the select belongs to a PLOT. On this
|
|
89
|
+
// page `data-vidx` means a variable's index (var-status), and on a
|
|
90
|
+
// plot of one subject the two only happen to coincide.
|
|
91
|
+
const plotIdx = parseInt(e.target.dataset.plot);
|
|
92
|
+
if (isNaN(plotIdx)) return;
|
|
93
|
+
setErrorOverlay(plotIdx, e.target.value);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function setErrorOverlay(plotIdx, mode) {
|
|
99
|
+
const el = document.getElementById(`plot-${plotIdx}`);
|
|
100
|
+
if (!el || !el.data) return;
|
|
101
|
+
const idx = [];
|
|
102
|
+
const vis = [];
|
|
103
|
+
el.data.forEach((tr, i) => {
|
|
104
|
+
if (!tr.meta || tr.meta.kind !== 'error') return;
|
|
105
|
+
idx.push(i);
|
|
106
|
+
vis.push(mode !== 'none' && tr.meta.id === `error/${mode}` ? true : 'legendonly');
|
|
107
|
+
});
|
|
108
|
+
if (idx.length) Plotly.restyle(el, { visible: vis }, idx);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---- Init ----
|
|
112
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
113
|
+
renderAllPlots();
|
|
114
|
+
wireOverlayPickers();
|
|
115
|
+
wireErrorOverlays();
|
|
116
|
+
});
|