|
| 1 | +# Feature scout — ranked ideas (2026-06-28) |
| 2 | + |
| 3 | +Run context: 69 prior `auto-generated` enhancement issues already filed (#210–#283), |
| 4 | +heavily covering widget options, event queries, Tag stats/persistence, and engine |
| 5 | +time/page navigation. Backlog Phase 999.1 (unified in-app help/wiki system) is the only |
| 6 | +roadmap-pending feature and is treated as **roadmap-covered** for dedupe purposes. |
| 7 | + |
| 8 | +Discovery sources mined this run: TODO/FIXME (only vendored sqlite3 — none in app code), |
| 9 | +`.planning/ROADMAP.md` + `STATE.md` backlog, code asymmetries across Dashboard widgets, |
| 10 | +and the current-milestone headline features (tabs/groups, per-widget info, detached mirrors). |
| 11 | + |
| 12 | +--- |
| 13 | + |
| 14 | +## #1 — FILED THIS RUN |
| 15 | + |
| 16 | +### BarChartWidget & HistogramWidget: fixed axis limits for a stable live frame |
| 17 | +- **Signal (asymmetry):** ScatterWidget has `XLimits`/`YLimits` (#263) and HeatmapWidget has |
| 18 | + `ColorLimits` (#231) — both filed explicitly "for a stable live frame / live color scale." |
| 19 | + BarChartWidget and HistogramWidget have no equivalent. Verified in `render()`: both call plain |
| 20 | + `bar`/`barh` with no `ylim`/`xlim` clamp, so the value axis (and Histogram's bin/X range) |
| 21 | + auto-rescales on every live `DataFcn` refresh → bars and axes visibly jump. |
| 22 | +- **Value:** Operators watching a live dashboard get a stable visual frame; bar heights stay |
| 23 | + comparable refresh-to-refresh instead of re-normalizing each tick. Direct sibling of two |
| 24 | + already-accepted issues. |
| 25 | +- **Fit:** toolbox-free; pure MATLAB; backward-compatible (default `[]` = current auto behavior; |
| 26 | + `toStruct` omits when empty, exactly like the existing `NumBins`/`EdgeColor` pattern, so legacy |
| 27 | + serialized dashboards stay byte-identical); works through the `DashboardWidget` contract. |
| 28 | +- **Effort:** S — add property + one `ylim`/`xlim` clamp after the `bar()` call in each widget's |
| 29 | + `render()`, plus `toStruct`/`fromStruct` lines. |
| 30 | +- **Confidence:** High — established sibling pattern, no dedupe hit. |
| 31 | + |
| 32 | +--- |
| 33 | + |
| 34 | +## #2 — next candidate |
| 35 | + |
| 36 | +### ScatterWidget: XLabel / YLabel axis labels |
| 37 | +- **Signal (asymmetry):** #253 added `XLabel`/`YLabel` to BarChart & Histogram but **not** Scatter, |
| 38 | + which is the most axis-label-natural widget (explicit X-vs-Y sensor pairing). |
| 39 | +- **Value:** Medium — clearer scatter axes; today they rely on auto/sensor-derived labels only. |
| 40 | +- **Fit:** Clean additive, toolbox-free, backward-compatible. |
| 41 | +- **Effort:** S. **Confidence:** Medium-high. |
| 42 | + |
| 43 | +## #3 |
| 44 | + |
| 45 | +### DashboardEngine.setTheme(preset): runtime theme switch with live restyle |
| 46 | +- **Signal (asymmetry):** `Theme` is a construction-time preset string (`'light'`/`'dark'`) with a |
| 47 | + `ThemeCache_`; `applyThemeToChrome` exists but there is no single public call that swaps the |
| 48 | + preset AND re-themes all already-rendered widgets at runtime. |
| 49 | +- **Value:** Medium — runtime light↔dark toggle for demos/presentations without rebuilding. |
| 50 | +- **Fit:** Additive, toolbox-free. **Effort:** M (must fan out restyle across widgets + chrome). |
| 51 | +- **Confidence:** Medium. |
| 52 | + |
| 53 | +## Run 2 (later 2026-06-28) — updates |
| 54 | + |
| 55 | +- **#285 filed** (run 1) — now known ground. |
| 56 | +- **#286** appeared from a parallel loop run — `IconCardWidget: trend-direction arrow (ShowTrend)`. Noted. |
| 57 | +- **#2 (Scatter XLabel/YLabel) — DROPPED.** Verified `ScatterWidget.render` already auto-labels both |
| 58 | + axes from the bound sensors via `axisLabelForSensor_` (lines 100–105). An explicit override would be |
| 59 | + marginal, not a real gap. Removed from the active list. |
| 60 | +- **FILED THIS RUN: #3 promoted →** `DashboardEngine.setTheme(preset)` runtime theme switch (see below). |
| 61 | + |
| 62 | +### FILED RUN 2 — DashboardEngine.setTheme(preset): runtime theme switch with live restyle |
| 63 | +- **Signal (code asymmetry):** `Theme` is a public property (`'light'`/`'dark'`, default `'light'`), and the |
| 64 | + machinery exists — `getCachedTheme()` rebuilds `ThemeCache_` when the preset string changes, |
| 65 | + `applyThemeToChrome()` restyles the figure/chrome, `rerenderWidgets()` re-renders widgets with the new |
| 66 | + theme. But there is **no single public call** that ties them together: `applyThemeToChrome`'s own doc |
| 67 | + says "Widget panels are NOT touched here — call rerenderWidgets() after." So switching theme after |
| 68 | + render today requires manually chaining `d.Theme=…; d.applyThemeToChrome(); d.rerenderWidgets();` |
| 69 | + (and detached mirrors aren't re-themed at all). |
| 70 | +- **Value:** Medium — one-call runtime light↔dark for demos/screenshots and control-room night mode, |
| 71 | + without rebuilding the dashboard. No dedupe hit. |
| 72 | +- **Fit:** Additive, toolbox-free, backward-compatible (no serialization change). |
| 73 | +- **Effort:** S–M (`DashboardEngine.m`; validate preset, set `Theme`, restyle chrome + rerender + re-theme |
| 74 | + any `DetachedMirrors`; guard live mode). **Confidence:** High. |
| 75 | + |
| 76 | +## Run 3 (later 2026-06-28) — updates |
| 77 | + |
| 78 | +- **#287 filed** (run 2) — now known ground. |
| 79 | +- **#288** appeared from a parallel loop run — `Tag: exportCsv(path)`. Export vein now taken |
| 80 | + (joins #210 dashboard export, #220 event CSV, #272 PlantLogStore CSV). Noted. |
| 81 | +- **FILED THIS RUN:** `DashboardEngine.refresh()` — public one-shot data refresh (see below). |
| 82 | + |
| 83 | +### FILED RUN 3 — DashboardEngine.refresh(): public one-shot data refresh (no live timer) |
| 84 | +- **Signal (code asymmetry):** the per-widget data-refresh loop (mark dirty → `w.update()` for |
| 85 | + FastSenseWidget / `w.refresh()` otherwise, with realized/visible guards + detached-mirror tick) lives |
| 86 | + entirely **inside the private `onLiveTick`**, driven by the live timer. The public surface offers only |
| 87 | + `markAllDirty()` (flags dirty, no redraw), `rerenderWidgets()` (full tear-down + rebuild — loses zoom/ |
| 88 | + state, heavy), and `startLive`/`stopLive` (continuous polling). There is **no lightweight public call |
| 89 | + to pull latest data into all widgets and redraw exactly once.** |
| 90 | +- **Value:** Scripts that load/append data (re-run a batch analysis, append to a Tag/DataStore, swap the |
| 91 | + backing `.mat`) and want to update the dashboard **on demand once** must today either run continuous |
| 92 | + live mode (timer churn) or `rerenderWidgets()` (rebuild). A `d.refresh()` does one live-tick's worth of |
| 93 | + data update without the timer or the rebuild. No dedupe hit (distinct from #248 `setLiveInterval`). |
| 94 | +- **Fit:** toolbox-free; additive; backward-compatible (no serialization change); works through the |
| 95 | + existing `DashboardWidget.refresh()` / `FastSenseWidget.update()` contract. |
| 96 | +- **Effort:** S–M — extract the existing `onLiveTick` dirty→refresh loop into a private helper |
| 97 | + (`refreshWidgets_`) and expose a public `refresh()` wrapper (active page + detached mirrors; no-op |
| 98 | + before render). Mostly a refactor of code that already exists. **Confidence:** High. |
| 99 | + |
| 100 | +## Run 4 (later 2026-06-28) — updates |
| 101 | + |
| 102 | +- **#289 filed** (run 3) — now known ground. |
| 103 | +- **#290** appeared from a parallel loop run — `FastSenseWidget: forward DownsampleMethod/DownsampleFactor` |
| 104 | + (joins the FastSenseWidget-forwarding vein: #216, #227, #237, #290). Noted. |
| 105 | +- **Checked & cleared:** `TagRegistry` is NOT an add-only collection — it already has `unregister`, |
| 106 | + `clear`, `find`, `findByLabel`, `findByKind`, `list`, `printTable`, `viewer`. No gap there. |
| 107 | +- **FILED THIS RUN:** `TagRegistry.toStructs()` — catalog export, inverse of `loadFromStructs` (see below). |
| 108 | + |
| 109 | +### FILED RUN 4 — TagRegistry.toStructs(): serialize the whole catalog (parity with loadFromStructs) |
| 110 | +- **Signal (round-trip asymmetry):** `TagRegistry.loadFromStructs(structs)` does a full two-phase |
| 111 | + **import** of an entire catalog (kind-dispatch via `instantiateByKind`, duplicate detection, cross-ref |
| 112 | + resolution). Every Tag already implements `toStruct` and it emits the `kind`/`key` fields that |
| 113 | + `loadFromStructs` consumes (verified in `SensorTag.toStruct`). But there is **no registry-level |
| 114 | + `toStructs()` / export** — the outbound half of the round-trip is missing. To persist a catalog today |
| 115 | + you must manually `find(@(t)true)` and call `toStruct` on each tag yourself. |
| 116 | +- **Value:** Catalog persistence — the registry is an in-memory singleton with no built-in way to save |
| 117 | + the catalog *definitions* and reload them across sessions. A `structs = TagRegistry.toStructs()` (the |
| 118 | + exact inverse of `loadFromStructs`), optionally wrapped by `saveToFile(path)` / `loadFromFile(path)` |
| 119 | + (JSON via `jsonencode`), lets engineers maintain a standard tag set on disk. Distinct from #288 |
| 120 | + (per-tag resolved *data* CSV — values, not definitions). |
| 121 | +- **Fit:** toolbox-free; additive; backward-compatible; pure MATLAB/Octave. Each Tag's `toStruct` |
| 122 | + already produces `loadFromStructs`-ready output, so `toStructs` = iterate `catalog()` and collect. |
| 123 | +- **Effort:** S (`TagRegistry.m`; one static `toStructs()` + thin optional file wrappers). |
| 124 | + **Confidence:** High. |
| 125 | + |
| 126 | +### Recorded (NOT filed) — widget click / drill-down callback |
| 127 | +- **Signal (domain gap / asymmetry):** No dashboard widget exposes a user click/selection callback |
| 128 | + (`grep` for `ButtonDownFcn`/`OnClick`/`Callback` across `libs/Dashboard/*Widget.m` finds only |
| 129 | + `GroupWidget.ReflowCallback`, injected internally). A sensor engineer can't click a bar/point/cell to |
| 130 | + drill into detail. |
| 131 | +- **Value:** High (interactive drill-down is a real analysis workflow). |
| 132 | +- **Why not filed:** **Needs an architecture decision** — a base-class hit-testing/callback mechanism plus |
| 133 | + per-widget wiring. L effort, design-heavy, no concrete prior signal. Better as a planned phase than a |
| 134 | + crisp single auto-issue. Parked for human triage. |
| 135 | + |
| 136 | +--- |
| 137 | + |
| 138 | +## #4 — NOT FILED (roadmap-covered) |
| 139 | + |
| 140 | +### Per-widget Markdown info (render `Description` via MarkdownRenderer) |
| 141 | +- **Signal (asymmetry):** `DashboardEngine.showInfo` renders its `InfoFile` as rich Markdown via |
| 142 | + `MarkdownRenderer`; the per-widget info popup (`DashboardLayout.openInfoPopup`) shows |
| 143 | + `widget.Description` as plain text in a read-only `uicontrol` edit box. The headline |
| 144 | + "per-widget info tooltips" feature is therefore plain-text-only. |
| 145 | +- **Value:** High. **Fit:** Perfect (reuses existing `MarkdownRenderer`). **Effort:** S. |
| 146 | +- **Why not filed:** Backlog **Phase 999.1** (unified in-app help/wiki system) explicitly scopes |
| 147 | + "every pane/widget … render markdown … reusing `MarkdownRenderer`." Treated as roadmap-covered |
| 148 | + per the dedupe guardrail. Revisit only if 999.1 is descoped/declined. |
0 commit comments