From 3d7b262fd3a4fcf6f034b902c4973af3090ad800 Mon Sep 17 00:00:00 2001 From: mikub97 Date: Thu, 10 Sep 2026 16:26:08 -0300 Subject: [PATCH] Render the documentation at v1.4.3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also gives headings an id, so an in-page link resolves here as it does on GitHub — every anchor in the generated pages was dead until now. --- docs/analyses-plots.js | 239 ++++++++++++++++-------- docs/analyses.html | 28 +-- docs/analysis-common.html | 46 ++--- docs/analysis-output.html | 33 ++-- docs/architecture.html | 14 +- docs/assets.html | 20 +- docs/builder-api.html | 22 +-- docs/builder.html | 33 ++-- docs/case.html | 12 +- docs/cli-dims-analysis.html | 18 +- docs/cli-dims-builder.html | 12 +- docs/cli-dims-case.html | 34 ++-- docs/coherence.html | 2 +- docs/config.html | 43 +++-- docs/crosswavelet.html | 115 ++++++++---- docs/crqa.html | 24 +-- docs/data-visibility.html | 20 +- docs/dims-api.html | 39 ++-- docs/docs.js | 2 +- docs/figure-geometry.html | 20 +- docs/getting-started.html | 30 +-- docs/host-runtime.html | 34 ++-- docs/index.html | 24 +-- docs/rqa.html | 28 +-- docs/step.html | 18 +- docs/tab.html | 25 ++- docs/tabs-crosswavelet.html | 39 ++-- docs/tabs-crqa.html | 16 +- docs/tabs-elan.html | 20 +- docs/tabs-network.html | 245 +++++++++++++++++-------- docs/tabs-rqa.html | 18 +- docs/tabs-timeseries.html | 18 +- docs/testing.html | 18 +- docs/versioning.html | 18 +- images/walkthrough/SHOTS.md | 4 +- images/walkthrough/opt-cw-tab.png | Bin 488767 -> 574733 bytes images/walkthrough/opt-network-tab.png | Bin 313426 -> 165129 bytes index.html | 2 +- setup.html | 18 +- tools/SOURCE.json | 6 +- tools/render_docs.py | 31 ++++ tutorial.html | 23 ++- 42 files changed, 853 insertions(+), 558 deletions(-) diff --git a/docs/analyses-plots.js b/docs/analyses-plots.js index b19866d..6509b1f 100644 --- a/docs/analyses-plots.js +++ b/docs/analyses-plots.js @@ -70,14 +70,6 @@ var FONT = { family: "-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif", size: 12, color: C.text }; - /* Sequential, light-to-dark, for power and coherence. Perceptually ordered so - "more" reads as "darker" in greyscale too, which a printed page needs. */ - var SEQ = [[0, "#f7fbfc"], [0.25, "#bfe3e0"], [0.5, "#5fbfb6"], - [0.75, "#0d9488"], [1, "#134e4a"]]; - /* Cyclic, for phase: -pi and +pi are the same angle and must be the same colour. */ - var CYCLIC = [[0, "#7c3aed"], [0.25, "#0d9488"], [0.5, "#f0f6f6"], - [0.75, "#9a6700"], [1, "#7c3aed"]]; - function layout(extra) { var base = { font: FONT, @@ -118,25 +110,74 @@ /* ---------- shared trace builders ----------------------------------------- */ - /* The cone of influence, drawn where the analysis actually applies it. + /* The period axis, laid out the way the dashboard's cross-wavelet tab lays it + out, so that a reader who follows this page and then opens a real study is + looking at the same picture: a linear axis carrying log2(period), ticked at + the octaves and labelled in seconds. Long periods are at the top. - The step masks a cell when `scale > coi[t]`, and these axes are in period, - so the boundary is coi scaled by the same period/scale ratio the transform - used. Drawing raw `coi` on a period axis would put the line in the wrong - place by 3.3% -- small, but wrong in the direction that matters. */ - function coiTrace(time, coi, period, scales, yTop) { - var ratio = period[0] / scales[0]; - var x = [time[0]].concat(time, [time[time.length - 1]]); - var y = [yTop].concat(coi.map(function (c) { return c * ratio; }), [yTop]); + Not `type: "log"` with a reversed autorange, which is the other convention + and puts the same data upside down. */ + function log2Period(period) { + return period.map(function (p) { return Math.log2(p); }); + } + + function periodAxis(period) { + var lo = Math.ceil(Math.log2(Math.min.apply(null, period))); + var hi = Math.floor(Math.log2(Math.max.apply(null, period))); + var vals = [], text = [], i, p; + for (i = lo; i <= hi; i++) { + p = Math.pow(2, i); + vals.push(i); + text.push(p < 1 ? p.toFixed(1) : p.toFixed(0)); + } return { - x: x, y: y, type: "scatter", mode: "lines", fill: "toself", - fillcolor: "rgba(31,35,40,0.13)", line: { color: C.text, width: 1, dash: "dot" }, - hoverinfo: "skip", name: "cone of influence" + title: { text: "Period (s)" }, tickmode: "array", + tickvals: vals, ticktext: text, + gridcolor: C.line, zeroline: false, linecolor: C.line }; } + /* A grid of the real period per cell, so the hover can name a period the + reader recognises while the axis carries its logarithm. `z` is 2-D, so a + 1-D customdata cannot be indexed against it. */ + function periodGrid(z, period) { + return z.map(function (row, i) { + return row.map(function () { return period[i]; }); + }); + } + + /* The cone of influence: a shaded region plus its boundary, as the tab draws + it. + + Where it departs from the tab: the step masks a cell when `scale > coi[t]`, + and these axes are in period, so the boundary is coi scaled by the same + period/scale ratio the transform used. Drawing raw `coi` on a period axis -- + which the tab does -- puts the line in the wrong place by 3.3%, small but + wrong in the direction that flatters the result. */ + function coiTraces(time, coi, period, scales) { + var ratio = period[0] / scales[0]; + var top = Math.log2(Math.max.apply(null, period)); + var floorP = period[0]; + var y = coi.map(function (c) { return Math.log2(Math.max(c * ratio, floorP)); }); + return [ + { x: time.concat([time[time.length - 1], time[0]]), + y: y.concat([top, top]), + type: "scatter", mode: "none", fill: "toself", + fillcolor: "rgba(0, 0, 0, 0.08)", line: { width: 0 }, + hoverinfo: "skip", showlegend: false, name: "COI" }, + { x: time, y: y, type: "scatter", mode: "lines", + line: { color: C.text, width: 2, dash: "dash" }, + customdata: coi, showlegend: false, name: "cone of influence", + hovertemplate: "Time: %{x:.1f}s
COI period: %{customdata:.2f}s" } + ]; + } + /* The 95% boundary as a contour of ratio == 1, which is how the payload asks - to be read: `power / signif_xwt`, `coherence / sig95_wtc`. */ + to be read: `power / signif_xwt`, `coherence / sig95_wtc`. + + Where it departs from the tab: the tab asks for contours from 0.95 to 1.5 + every 0.5, which draws two lines, at 0.95 and 1.45. One line at the level + the text describes is what a figure explaining the level should show. */ function ratioContour(x, y, z, level) { var ratio = z.map(function (row, i) { return row.map(function (v) { @@ -147,22 +188,43 @@ x: x, y: y, z: ratio, type: "contour", showscale: false, autocontour: false, contours: { start: 1, end: 1, size: 1, coloring: "none", showlabels: false }, - line: { color: "#111827", width: 1.4 }, hoverinfo: "skip" + line: { color: "black", width: 2 }, hoverinfo: "skip" }; } function heat(x, y, z, opts) { return merge({ - x: x, y: y, z: z, type: "heatmap", colorscale: SEQ, - colorbar: { thickness: 11, outlinewidth: 0, len: 0.92, tickfont: { size: 10 } }, - hovertemplate: "%{x:.1f} s · %{y:.2f} s
%{z:.3f}" + x: x, y: y, z: z, type: "heatmap", colorscale: "Viridis", + colorbar: { titleside: "top", thickness: 10, outlinewidth: 0, + tickfont: { size: 9 } } }, opts || {}); } - var PERIOD_AXIS = { - type: "log", autorange: "reversed", title: { text: "period (s)" }, - gridcolor: C.line, zeroline: false, linecolor: C.line - }; + /* Phase as one of eight glyphs, binned as the tab bins it. Right is in phase, + and the angle turns anticlockwise from there. */ + var GLYPHS = ["→", "↗", "↑", "↖", "←", "↙", "↓", "↘"]; + + function phaseGlyph(deg) { + return GLYPHS[Math.floor(((deg + 22.5) % 360) / 45)]; + } + + /* The glyph trace itself: a dark disc under white text, because the glyphs sit + on the pale end of Viridis as often as the dark end and neither a light nor + a dark ink is readable on both. */ + function glyphTrace(x, y, text, customdata) { + return { + x: x, y: y, text: text, customdata: customdata, + type: "scatter", mode: "markers+text", + marker: { size: 15, color: "rgba(0,0,0,0.45)" }, + textfont: { + // Arial has no U+2196-2199, so the diagonals would arrive as tofu. + family: "Segoe UI Symbol, Apple Symbols, DejaVu Sans, sans-serif", + size: 14, color: "#ffffff" + }, + textposition: "middle center", showlegend: false, + hovertemplate: "%{customdata}" + }; + } /* ---------- data, fetched once each ---------------------------------------- */ @@ -212,17 +274,19 @@ return load("demo_crosswavelet.json").then(function (d) { var p = xwtPair(d), v = p.visualization; var z = decodeF32(v.power); + var y = log2Period(v.period); draw(el, [ - heat(v.time, v.period, z, { - colorbar: { title: { text: "|WXY|", side: "right" }, - thickness: 11, outlinewidth: 0, len: 0.92, - tickfont: { size: 10 } } }), - ratioContour(v.time, v.period, z, v.signif_xwt), - coiTrace(v.time, v.coi, v.period, v.scales, - v.period[v.period.length - 1]) - ], { - xaxis: { title: { text: "time (s)" }, range: [v.time[0], v.time[v.time.length - 1]] }, - yaxis: PERIOD_AXIS + heat(v.time, y, z, { + colorbar: { title: "Power" }, + customdata: periodGrid(z, v.period), + hovertemplate: "Time: %{x:.1f}s
Period: %{customdata:.2f}s
" + + "Power: %{z:.4f}" + }), + ratioContour(v.time, y, z, v.signif_xwt) + ].concat(coiTraces(v.time, v.coi, v.period, v.scales)), { + plot_bgcolor: C.panel, + xaxis: { title: { text: "Time (s)" }, range: [v.time[0], v.time[v.time.length - 1]] }, + yaxis: periodAxis(v.period) }, 380); caption(el, "Cross-wavelet power, visualization.power — a " + "f32-b64 grid of " + v.power.shape[0] + " periods × " + @@ -237,47 +301,55 @@ return load("demo_crosswavelet.json").then(function (d) { var p = xwtPair(d), v = p.visualization; var z = decodeF32(v.coherence), phase = decodeF32(v.phase); - var ratio = v.period[0] / v.scales[0]; - - /* Arrows only where the relationship is above chance and outside the - cone: a phase read off a cell that means nothing is a decoration. */ - var arrows = []; - for (var i = 0; i < v.period.length; i += 3) { - for (var j = 4; j < v.time.length; j += 10) { + var y = log2Period(v.period); + + /* Glyphs only where the relationship is above chance and outside the + cone: a phase read off a cell that means nothing is a decoration. + + Where this departs from the tab: the tab gates its glyphs on *power* + significance. Phase is only readable where the timing is consistent, + which is what coherence measures, so the gate here is `sig95_wtc`. */ + var gx = [], gy = [], gt = [], gd = []; + var skipT = Math.max(1, Math.floor(v.time.length / 20)); + var skipF = Math.max(1, Math.floor(v.period.length / 12)); + for (var i = 0; i < v.period.length; i += skipF) { + for (var j = 0; j < v.time.length; j += skipT) { var c = z[i][j], lvl = v.sig95_wtc ? v.sig95_wtc[i] : null; if (c === null || lvl === null || c <= lvl) { continue; } if (v.scales[i] > v.coi[j]) { continue; } var a = phase[i][j]; if (a === null) { continue; } - arrows.push({ - x: v.time[j], y: v.period[i], xref: "x", yref: "y", - ax: -9 * Math.cos(a), ay: 9 * Math.sin(a), - axref: "pixel", ayref: "pixel", - showarrow: true, arrowhead: 2, arrowsize: 1, arrowwidth: 1.1, - arrowcolor: "rgba(17,24,39,0.75)" - }); + var deg = (a * 180 / Math.PI + 360) % 360; + gx.push(v.time[j]); + gy.push(y[i]); + gt.push(phaseGlyph(deg)); + gd.push("Time: " + v.time[j].toFixed(1) + "s | Period: " + + v.period[i].toFixed(2) + "s
Phase: " + deg.toFixed(0) + + "°
Coherence: " + c.toFixed(3)); } } draw(el, [ - heat(v.time, v.period, z, { + heat(v.time, y, z, { zmin: 0, zmax: 1, - colorbar: { title: { text: "R²", side: "right" }, thickness: 11, - outlinewidth: 0, len: 0.92, tickfont: { size: 10 } } }), - ratioContour(v.time, v.period, z, v.sig95_wtc), - coiTrace(v.time, v.coi, v.period, v.scales, - v.period[v.period.length - 1]) - ], { - xaxis: { title: { text: "time (s)" }, range: [v.time[0], v.time[v.time.length - 1]] }, - yaxis: PERIOD_AXIS, - annotations: arrows + colorbar: { title: "Coherence" }, + customdata: periodGrid(z, v.period), + hovertemplate: "Time: %{x:.1f}s
Period: %{customdata:.2f}s
" + + "Coherence: %{z:.4f}" + }), + ratioContour(v.time, y, z, v.sig95_wtc) + ].concat(coiTraces(v.time, v.coi, v.period, v.scales), + [glyphTrace(gx, gy, gt, gd)]), { + plot_bgcolor: C.panel, + xaxis: { title: { text: "Time (s)" }, range: [v.time[0], v.time[v.time.length - 1]] }, + yaxis: periodAxis(v.period) }, 380); var frac = p.statistics.wtc_signif_fraction; caption(el, "Wavelet coherence, with the same cone and the 95% " + - "sig95_wtc boundary. Arrows are phase, drawn " + - "only on above-chance cells outside the cone: right means in phase, " + - "and here they lean consistently at the 4 s band, where " + + "sig95_wtc boundary. The glyphs are phase, " + + "drawn only on above-chance cells outside the cone: → means in phase, " + + "and here they point consistently at the 4 s band, where " + "sig_b lags by 0.7 s. The burst at 20–35 s is " + "bright in power and not here. " + (100 * frac).toFixed(1) + "% of usable cells are above chance, against " + @@ -288,16 +360,24 @@ "fig-xwt-global": function (el) { return load("demo_crosswavelet.json").then(function (d) { var v = xwtPair(d).visualization; + var y = log2Period(v.period); + /* The visualization copies, not the `statistics` ones: those are written + at full resolution and would be a different length from this axis. */ draw(el, [ - { x: v.global_power, y: v.period, type: "scatter", mode: "lines", - name: "global power", line: { color: C.teal, width: 2 } }, - { x: v.global_signif, y: v.period, type: "scatter", mode: "lines", - name: "95% level", line: { color: C.amber, width: 1.6, dash: "dash" } } + { x: v.global_power, y: y, type: "scatter", mode: "lines", + name: "global power", line: { color: C.text, width: 2 }, + customdata: v.period, + hovertemplate: "Power: %{x:.4f}
Period: %{customdata:.2f}s" }, + { x: v.global_signif, y: y, type: "scatter", mode: "lines", + name: "95% level", line: { color: C.text, width: 1, dash: "dash" }, + customdata: v.period, + hovertemplate: "95% level: %{x:.4f}
Period: %{customdata:.2f}s" } ], { + plot_bgcolor: C.panel, showlegend: true, legend: { orientation: "h", y: 1.14, x: 0 }, margin: { t: 34 }, - xaxis: { title: { text: "time-averaged |WXY|" } }, - yaxis: PERIOD_AXIS + xaxis: { title: { text: "Power" } }, + yaxis: periodAxis(v.period) }, 340); caption(el, "global_power against global_signif: " + "the whole record averaged over time, tested with the degrees of freedom " + @@ -311,18 +391,23 @@ return load("demo_crosswavelet.json").then(function (d) { var p = xwtPair(d), v = p.visualization; var lvl = p.statistics.scale_avg_signif; + var band = p.scale_avg_band; draw(el, [ { x: v.time, y: v.scale_avg_power, type: "scatter", mode: "lines", - fill: "tozeroy", fillcolor: "rgba(13,148,136,0.16)", - line: { color: C.teal, width: 1.6 }, name: "scale-averaged power" }, + line: { color: C.text, width: 2 }, name: "scale-averaged power", + hovertemplate: "Time: %{x:.1f}s
Power: %{y:.4f}" }, { x: [v.time[0], v.time[v.time.length - 1]], y: [lvl, lvl], type: "scatter", mode: "lines", name: "95% level", - line: { color: C.amber, width: 1.5, dash: "dash" } } + line: { color: C.text, width: 1, dash: "dash" }, + hovertemplate: "95% level: %{y:.4f}" } ], { + plot_bgcolor: C.panel, showlegend: true, legend: { orientation: "h", y: 1.14, x: 0 }, margin: { t: 34 }, - xaxis: { title: { text: "time (s)" } }, - yaxis: { title: { text: "scale-averaged power" } } + xaxis: { title: { text: "Time (s)" } }, + yaxis: { title: { text: band + ? band[0].toFixed(1) + "–" + band[1].toFixed(1) + "s avg" + : "scale-averaged power" } } }, 280); caption(el, "scale_avg_power over the band in " + "scale_avg_band, one number per time point, against the " + diff --git a/docs/analyses.html b/docs/analyses.html index bd63c91..9afd68a 100644 --- a/docs/analyses.html +++ b/docs/analyses.html @@ -29,7 +29,7 @@
Analyses
-

The analyses DIMS ships

+

The analyses DIMS ships

Three analyses come with the core. Each one reads prepared time series from assets/timeseries/, and each one writes a single JSON payload that a dashboard tab draws and a notebook can read back.

@@ -44,7 +44,7 @@

The analyses DIMS ships

- Cross-wavelet & coherence + Cross-wavelet analysis a pair at which timescales, and when, do two signals share energy — and do they keep a consistent timing relationship? assets/crosswavelet/{video}_crosswavelet_data.json @@ -74,11 +74,11 @@

The analyses DIMS ships

asset layout says where every file goes and what a visualization block holds; analysis output says what any analysis must record and why.

-

The running example

+

The running example

Every figure in this section is real output. A generator builds six synthetic signals whose answers are known before anything runs, feeds them to dims-analysis run, and the pages plot the payloads that come out — - docs/analyses/demo/make_demo_data.py, and the + docs/analyses/demo/make_demo_data.py, and the committed results beside it. Nothing here is an illustration of what the code is supposed to do.

Two of those six are the running example, carried through all three pages:

@@ -93,7 +93,7 @@

The running example

the kind of thing the chance level is estimated against.

That pair is the reason both cross-wavelet power and coherence exist: power finds the burst, coherence does not.

-

What goes in

+

What goes in

One CSV per recording per signal, at assets/timeseries/{video}_{type}.csv, with a Time column in seconds and one value column:

Time,sig_a
@@ -107,7 +107,7 @@ 

What goes in

series is sorted by time. What differs between them is the guard afterwards — cross-wavelet refuses fewer than 50 samples, the recurrence analyses refuse fewer than 10 and refuse a signal that does not vary at all.

-

Turning them on

+

Turning them on

Each analysis is gated by one key in the study's config.json, and the shape of that key is not the same for all three:

{
@@ -140,14 +140,14 @@ 

Turning them on

Tuning goes under analysis.<step id>, where the step id is rqa, crqa or crosswavelet — the names dims-analysis list prints, not the gate keys. Each page below documents its own parameters; the schema that validates them is - contracts/config.schema.json.

-

What comes out

+ contracts/config.schema.json.

+

What comes out

One file per recording per analysis, always this shape:

{
   "video_id": "demo",
   "payload_version": 2,
   "<container key>": { /* keyed by data type, or by "{a}_vs_{b}" */ },
-  "provenance": { "core_version": "1.4.2", "...": "the settings actually used" },
+  "provenance": { "core_version": "1.4.3", "...": "the settings actually used" },
   "precision": { "significant_figures": 6, "note": "..." }
 }
 
@@ -179,7 +179,7 @@

What comes out

Writing merges one level deep rather than replacing the file, so a study-owned analysis can add its own entries to rqa_data without erasing the core's.

-

Numbers are rounded, arrays are packed

+

Numbers are rounded, arrays are packed

Every small field is rounded to six significant figures — significant figures, not decimal places — and non-finite values become null, because json.dump writes a bare NaN token that JSON.parse rejects outright.

@@ -230,7 +230,7 @@

Numbers are rounded, arrays are packed

In Python, dims_analysis.common.arrays.unpack() walks a payload and returns whichever it finds, so you do not have to know in advance which fields grew large enough to be packed.

-

A picture and the analysis are not the same file

+

A picture and the analysis are not the same file

Every payload holds a visualization block that has been reduced to something a browser can draw, and records how:

"reduction": {
@@ -255,7 +255,7 @@ 

A picture and the analysis are not the same file

threshold, which each page shows.

The rule and its rationale are contract A3 in analysis output.

-

Where these figures came from

+

Where these figures came from

The demo payloads were produced by running the real steps over the synthetic signals, against the pinned dependencies. Re-generate them with:

python docs/analyses/demo/make_demo_data.py
@@ -264,8 +264,8 @@ 

Where these figures came from

coherence chance level — so two runs produce identical files, and a change in a figure means a change in the analysis.

Generated from - docs/analyses/index.md - at v1.4.2. Edit it there, not here.

+ docs/analyses/index.md + at v1.4.3. Edit it there, not here.