Skip to content

fix(logged-data): the empty plot region is a black void — the axes and the card were both there, both gated off - #259

Merged
tylerkron merged 3 commits into
mainfrom
fix/251-loggeddata-empty-plot
Sep 5, 2026
Merged

tylerkron merged 3 commits into
mainfrom
fix/251-loggeddata-empty-plot

Conversation

@tylerkron

@tylerkron tylerkron commented Sep 5, 2026 •

Copy link
Copy Markdown
Contributor

Closes #251. Not merging — for review.

On a fresh install the Logged Data pane's upper ~57% rendered as a featureless black
rectangle: no axes, no gridlines, no copy, while the list below it correctly said
NO SESSIONS. Live Graph handles the identical "nothing to plot yet" state well, drawing
labelled axes plus a small card. This makes Logged Data do the same thing — by deleting the
two conditions that were suppressing the empty state it already had.

The cause was narrower than "nobody built one"

The pane already had both halves of a proper empty state. Each was gated off in exactly
this case:

element gate on main effect with zero sessions
oxy:PlotView (main plot) IsVisible="{Binding DbLogger.HasSessionData}" axes collapsed
empty-state card MultiBinding: !HasSessionData AND HasLoggingSessions card suppressed

The second condition is the interesting one: the card was shown only when sessions exist but
none is picked, so it was silenced in the one state that needed it most.

DatabaseLogger's constructor already assigns
PlotModel = _plotModelFactory.CreateMainPlotModel() unconditionally, and that model is
built with the same three titled axes Live Graph shows — Analog (V), Digital,
Time (ms). Nothing adds a series until DisplayLoggingSession runs. So the axes were
there the whole time; the view was just hiding them.

The fix is subtraction

Delete the PlotView's gate, and delete the second condition from the card's. That
satisfies both remedies the issue offers at once, and leaves the pane structurally
identical to LiveGraphPane.axaml's empty state: plot always drawn, card overlaid.

Ignoring comments, the markup is net −5 lines — a 7-line StackPanel.IsVisible
MultiBinding block and one IsVisible attribute removed, two plain IsVisible attributes
added, and one Text string swapped 1-for-1. No new style, converter, resource or panel
level.
Two converter references (BoolAndToVis, BooleanToInverse) drop out of this file;
both stay registered and in use by LiveGraphPane.axaml, so nothing is orphaned.

The card's copy, which took two passes to get right

Making the card visible in the zero-session state exposed a wording problem it never had
before, and the fix is now split the way LiveGraphPane's card is — a line that names
the state and a line that instructs:

line text shown when
primary No session selected always (true whether or not sessions exist)
secondary Choose a session from the list below HasLoggingSessions only

The secondary line is gated because with no sessions it would point at an empty list and
repeat guidance the pane already gives 300px lower. The primary line was Select a session to view data until Qodo round 1 correctly pointed out that this still requested an
impossible action on a fresh install — conceded and changed. HasLoggingSessions therefore
survives as a gate, but on the one element that actually depends on a non-empty list rather
than on the whole card.

Evidence

A layout/empty-state fix is not meaningfully unit-testable, so the claim rests on these
images, not on an assertion.
Saying that plainly rather than rounding it up to "verified".

Before — origin/main @ b924261, unmodified. --check-baseline matched the committed
manifest on all 24 screens, so this is what ships today, not a harness artifact:

Logged Data with no session selected on main: the upper 57% is a featureless black rectangle

After — the same capture site on this branch:

Logged Data with no session selected after the fix: labelled empty axes with a centered card reading No session selected

  • Blast radius: exactly 1 of 24 screens. --check-captured against the manifest with only
    desktop-2-loggeddata.png updated reports the other 23 OK — byte-for-byte identical to
    what main has recorded.
  • parity: the capture harness flips the SplitView pane edge on GitHub macOS runners, ~1 run in 5 #201 defused: --determinism ran 5 passes, 24/24 screens byte-identical across 5 runs.
    No edge-column delta appeared. (Re-run from scratch after the round-1 copy change.)
  • Re-recorded, and only that one line: desktop-2-loggeddata.png →
    b8d9c8d4ca0af2b4fbc2a270bded2beda5f350383d3731b34fbe50afa8b812e3 in
    tools/parity-audit/baselines/macos-arm64.sha256. The updated manifest then verified clean
    over determinism/r1, which is the same chain CI runs. Harness README updated with the
    re-recording note the Baselines section expects.
  • The recording is not single-host. Desktop head on macOS is green on this PR, and that
    job runs its own five-pass --determinism on the macos-latest runner and then verifies
    determinism/r1 against this manifest — so the new hash reproduces on a second machine, not
    only on the Mac that recorded it. (Not added to the README note: the review had settled by
    the time CI confirmed it, and this is not worth another round-trip. Worth folding in if the
    file is touched again.)

What this evidence does NOT cover

The committed harness only renders the zero-session state. The "sessions exist but none
selected" state also changes — it gains the empty axes behind the card, and its primary line
is reworded — and is not part of the committed capture set; in this PR's own evidence that
behaviour is reasoned from the code, not photographed. There is no automated assertion that
the pane renders correctly, because views here carry no x:DataType and their bindings
resolve by reflection at run time.

Update: the shepherd has since photographed that state on main and on this branch, using
a temporary extra screen in AvaloniaCapture that was reverted rather than committed — see
the shepherd verification comment.
It renders as reasoned. The same comment covers the mid-load state, which turns out to be
hidden behind the pane's existing IsLoggedDataBusy scrim in both the old and the new
behaviour.

Tests

5 new cases in 3 methods (EmptyLoggedPlotFrameTests); suite goes 530 → 535, 0 failures.

They pin the one genuinely new assumption this fix introduces: that the model handed to the
now-always-visible PlotView is a complete labelled empty frame — three titled axes at the
right positions, gridlines on analog and time and deliberately none on digital, zero series.
If those axes were ever moved out of construction and built on first data instead, the pane
would silently revert to the void this issue is about, both heads would still build green,
and nothing else would notice.

Scoped deliberately to PlotModelFactory, which is pure by design — no app host, no DB, no
NullDialogService — so it cannot collide with #250/#258's test-file refactor.

I did not add view-model tests, because unlike #258 this diff adds no view-model surface at
all
. Both members it binds were already bound by this same view before the change, and both
were verified to exist and change-notify: DbLogger.HasSessionData is [ObservableProperty],
and HasLoggingSessions is re-raised by NotifyLoggingSessionsChanged.

Coordination with the other two baseline-mutating PRs

Three open PRs move the capture baseline at once. Checked with gh pr diff <N> --name-only:

PR source files manifest effect
#256 DevicesPanePrototype.axaml, FirmwareDialog.axaml, DaqifiViewModel.cs, AvaloniaCapture/Program.cs adds a screen (24 → 25)
#258 NotificationsFlyout.axaml, DaqifiViewModel.cs, 2 test files re-records desktop-7-notifications-flyout.png
this LoggedDataPanePrototype.axaml, 1 new test file re-records desktop-2-loggeddata.png

This PR is disjoint from both. No source file it touches is touched by either of the other
two. (Correction to an earlier, stronger version of this claim: the three are not pairwise
disjoint — #256 and #258 both edit DaqifiViewModel.cs. They auto-merge there, but that is
their overlap, not this PR's.) All three edit baselines/macos-arm64.sha256, on different
lines (2, 7, and #256's insertion at 16), which merges cleanly. I did not touch the gate's
expected= count — that is #256's to own.

The one conflict surface is tools/parity-audit/README.md. #256 and #258 both insert their
re-recording paragraph immediately after the baselines/<os>-<arch>.sha256 records …
paragraph — the top of the Baselines list — so those two collide with each other there.
This
PR's paragraph sits one entry lower, below the #228 entry, so it lands in a different hunk from
both; each paragraph self-dates, and the README says the placement was deliberate.

Shepherd-verified by actually merging. MERGEABLE is computed pairwise against main and
can never see a PR-to-PR conflict, so all three read clean while two of them collide:

merge result
#259 × #256, both orders clean — identical tree either way
#259 × #258, both orders clean
#256 × #258 CONFLICT, tools/parity-audit/README.md, one hunk. Resolution: keep both paragraphs (their order is a style choice)
#255 + #256 + #258 + #259, two orders clean once the above is resolved, and bit-identical tree in both orders

On that five-way tree: desktop head green, iOS Release iossimulator-arm64 green, 578 tests
pass, 0 fail
, the manifest auto-merges to 25 correct lines, --determinism is 25/25 across
5 runs
, and --check-captured against the merged manifest reports all 25 screens OK — so
the three baseline mutations genuinely coexist.

Build gate

Re-run in full after the round-1 copy change:

check result
desktop head succeeded, 191 warnings
iOS restore + Release iossimulator-arm64 succeeded
dotnet test 535 passed, 0 failed
packages.lock.json churn none

Warnings measured against my own merge-base, not a quoted constant: origin/main @
b924261 and this branch both report 191 on the desktop head — an all-project MSBuild
total
, 20 of which are third_party — and the first-party warning set is identical: 171
on each side, 0 added, 0 removed
(compared as a set, not just a count). The delta was
measured on the desktop head, which is the build that compiles the changed .axaml; the iOS
head built green but its total was not measured cleanly (incremental).

The Android head does not build on this Mac (workload absent) — I left that to CI rather than
claiming it, and CI's Android head job is green.

CI at 7a6043f (run 33995300439): six jobs green — Library + Desktop head + capture
harness, Desktop head on macOS, Android head, iOS head, Avalonia version lockstep, CI script
self-tests. The seventh, Claimed bumps are actually applied, is skipped, not passed — it
does not apply to this diff.

Qodo

Two rounds. Round 1 raised one Medium correctness finding (the impossible zero-session
instruction) — conceded and fixed, replied and resolved. Round 2 is clean on both surfaces at
this head SHA: Bugs (0) / Rule violations (0) / Skill insights (0), 0 unresolved qodo
threads, re-checked after a settle wait.

No bench validation: an empty-state rendering fix with no device attached is not
device-observable, so no board lock was taken.

tylerkron and others added 2 commits September 5, 2026 15:49
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ndering as a void

Closes #251. Re-records desktop-2-loggeddata.png only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tylerkron

Copy link
Copy Markdown
Contributor Author

/agentic_review

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Show Logged Data plot frame and guidance without a selected session

🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 10-20 Minutes

Grey Divider

AI Description

• Always render Logged Data's labelled plot frame when no session data exists.
• Show empty-state guidance while hiding impossible session-selection instructions on fresh
 installs.
• Add plot-model regression tests and re-record the macOS parity baseline.
Diagram

graph TD
  Model["Fresh Plot Model"] --> Plot["Visible PlotView"]
  Data{"Session data?"} -->|Yes| Series["Loaded Series"] --> Plot
  Data -->|No| Card["Empty-state Card"] -->|Overlays| Plot
  Sessions{"Sessions exist?"} -->|Yes| CTA["Show CTA"] --> Card
  Sessions -->|No| Hidden["Hide CTA"]
Loading
High-Level Assessment

The PR's subtraction-based approach is optimal because the existing PlotModel is already safely initialized with an empty, labelled frame. A separate placeholder plot, lazy model construction, or additional converters would duplicate existing behavior and introduce unnecessary state synchronization; keeping PlotView visible and narrowing the session-list condition directly fixes the incorrect gates.

Files changed (4) +115 / -14

Bug fix (1) +20 / -13
LoggedDataPanePrototype.axamlRender the plot and empty-state card without session data +20/-13

Render the plot and empty-state card without session data

• Removes the session-data visibility gate from PlotView and shows the empty-state card whenever nothing is plotted. Restricts the session-selection instruction to cases where selectable sessions actually exist.

Daqifi.Avalonia/Daqifi.Desktop/View/Prototype/LoggedDataPanePrototype.axaml

Tests (1) +83 / -0
EmptyLoggedPlotFrameTests.csProtect the empty logged-plot frame contract +83/-0

Protect the empty logged-plot frame contract

• Adds regression coverage proving a fresh main plot contains no stale series, includes all three titled axes in their expected positions, and configures gridlines appropriately. These tests protect the model assumptions required by the always-visible PlotView.

Daqifi.Avalonia.Tests/Loggers/EmptyLoggedPlotFrameTests.cs

Documentation (1) +11 / -0
README.mdDocument the Logged Data baseline re-recording +11/-0

Document the Logged Data baseline re-recording

• Records the environment and determinism evidence for the updated Logged Data capture. Notes that only the affected screenshot hash changed across the 24-screen baseline.

tools/parity-audit/README.md

Other (1) +1 / -1
macos-arm64.sha256Update the macOS Logged Data visual baseline +1/-1

Update the macOS Logged Data visual baseline

• Replaces the desktop Logged Data screenshot hash with the capture showing the labelled empty plot frame and guidance card.

tools/parity-audit/baselines/macos-arm64.sha256

@qodo-code-review

qodo-code-review Bot commented Sep 5, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Impossible zero-session instruction ✓ Resolved 🐞 Bug ≡ Correctness
Description
The card is now shown whenever HasSessionData is false, including when HasLoggingSessions is
false, but its primary text still says “Select a session to view data.” Fresh installations
therefore simultaneously instruct users to select a session and report that no sessions exist.
Code

Daqifi.Avalonia/Daqifi.Desktop/View/Prototype/LoggedDataPanePrototype.axaml[R505-506]

                        <StackPanel Grid.Row="1"
+                                    IsVisible="{Binding !DbLogger.HasSessionData}"
Relevance

●●● Strong

PR intent explicitly fixes the impossible CTA; similar correctness and XAML findings were accepted.

PR-#84
PR-#233

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The changed parent visibility depends only on !DbLogger.HasSessionData, while its always-visible
child instructs selection. Separately, the list renders NO SESSIONS when !HasLoggingSessions,
proving both contradictory messages appear together on a fresh installation.

Daqifi.Avalonia/Daqifi.Desktop/View/Prototype/LoggedDataPanePrototype.axaml[501-527]
Daqifi.Avalonia/Daqifi.Desktop/View/Prototype/LoggedDataPanePrototype.axaml[818-836]
Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DaqifiViewModel.cs[260-263]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The newly visible empty-plot card says “Select a session to view data” even when no logging sessions exist. Use conditional or neutral primary copy so the zero-session state does not request an impossible action.

## Issue Context
`HasLoggingSessions` currently hides only the secondary “Choose a session” line, while the synonymous primary instruction remains visible. The session-list empty state below explicitly reports `NO SESSIONS` and tells users how to create or import one.

## Fix Focus Areas
- Daqifi.Avalonia/Daqifi.Desktop/View/Prototype/LoggedDataPanePrototype.axaml[505-527]
- Daqifi.Avalonia/Daqifi.Desktop/View/Prototype/LoggedDataPanePrototype.axaml[818-836]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
Review mode: 🚀 Fast: The push makes a localized Avalonia text/visibility presentation adjustment plus its corresponding visual baseline hash, with no substantive logic or high-risk behavior changes.

Grey Divider

Tip of the day
💡 Did you know, you can keep summaries lean with Finding overflow, which tucks the rest behind 'View more'

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

…ssible action

Qodo round 1: with no sessions the card's primary line still read "Select a session to view data", directly above a list reporting NO SESSIONS. Now "No session selected" — descriptive, true in both states, and the same naming/instructing split LiveGraphPane's card uses. Re-records desktop-2-loggeddata.png.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tylerkron

Copy link
Copy Markdown
Contributor Author

/agentic_review

@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 7a6043f

@tylerkron

Copy link
Copy Markdown
Contributor Author

Shepherd verification — head 7a6043f8

Independent of the builder's runs. Everything below was re-derived or re-run here.

1. There is a documentation conflict between the open PRs that GitHub cannot show you

mergeable is computed pairwise against main, never between two open PRs, so #255, #256, #258 and #259 all read MERGEABLE while two of them collide. Merged them for real, in more than one order:

merge result
#259 × #256 (both orders) clean — identical tree either way
#259 × #258 (both orders) clean
#256 × #258 CONFLICT — tools/parity-audit/README.md, one hunk
#255 + #256 + #258 + #259, two orders clean once that is resolved, and bit-identical tree in both orders

The conflict, precisely. Both #256 and #258 insert their re-recording paragraph immediately after the baselines/<os>-<arch>.sha256 records the SHA-256 of all N Avalonia screens… paragraph, i.e. at the top of the Baselines list, before the desktop-9-summary-flyout.png entry. Two adds at one anchor.

Resolution: keep both paragraphs. They describe different recordings (#256 adds dialog-firmware-uploading.png; #258 re-records desktop-7-notifications-flyout.png), neither supersedes the other, and their relative order is a style choice — the tree is bit-identical either way once both are present. Nothing else in that file conflicts.

#259 is clear of both. Its paragraph sits one entry lower, below the #228 entry, which is far enough from the shared anchor that git's context windows do not overlap. The deliberate placement claimed in the PR body is real and it works — but note it was aimed at #258 only; it happens to clear #256 as well.

#256 also carries the 24 → 25 edits at lines ~102 and ~370 of that README, and #256 and #258 both edit DaqifiViewModel.cs. Neither is a conflict; both auto-merge.

2. The three baseline mutations really do coexist — proved by running the gate, not by reading file names

On the five-way merged tree (main + #255 + #256 + #258 + #259):

No #201 edge-column flake appeared on desktop-7/-8/-9.

3. The state the PR body says is not photographed — now photographed

The body correctly discloses that only the zero-session state is captured, and that "sessions exist, none selected" is reasoned from the code. I rendered it, via a temporary extra screen in AvaloniaCapture (reverted; nothing committed), on main and on this branch.

On main (b924261) — the card was already visible here (HasLoggingSessions is true), but the plot region is still a void, and the primary line asks for something the user has already been given:

Logged Data on main with three sessions present and none selected: the empty-state card reads Select a session to view data, and the plot region above the session list is entirely black with no axes

On this branch — labelled Analog (V) / Digital / Time (ms) axes with gridlines, the card reading No session selected, and the Choose a session from the list below second line correctly present because sessions exist:

Logged Data on this branch with three sessions present and none selected: labelled empty axes fill the plot region, with a centered card reading No session selected above Choose a session from the list below

This is the picture that makes the double-gate story concrete: the card's second condition is why a fresh install got nothing, and the PlotView's gate is why neither state ever had axes.

Mid-load is covered by something else and is safe. LoggingSessionListViewModel.DisplaySessionAsync sets IsLoggedDataBusy = true for the whole load, and LoggedDataPanePrototype.axaml puts a full-pane #CC000000 scrim + progress bar at ZIndex="20" over everything while that flag is set. So the card is not what the user sees while a session loads, before or after this change.

Subtraction did not make a hidden state reachable. The other three HasSessionData gates in that file — the session header strip (which also carries the plot toolbar), the minimap, and the legend panel — are untouched and still collapse with no session. Only the main PlotView was un-gated.

4. Every number in the body re-derived

claim re-derived
net −5 markup lines, ignoring comments ✅ +4 / −9 non-comment lines in the .axaml (total diff is +27/−14; comments are +18)
530 → 535 tests ✅ main 530, head 535, 0 failed — 5 cases in 3 methods
pre-fix capture matched the committed manifest ✅ my own pre-fix desktop-2-loggeddata.png hashes to c893092e…, which is exactly what main has committed — the void is what ships
the new hash ✅ my own post-fix capture hashes to b8d9c8d4…, exactly the value this PR records
blast radius 1 of 24 ✅ pre-fix vs post-fix captures differ on desktop-2-loggeddata.png and nothing else
desktop 191 warnings ✅ 191 all-project MSBuild total, on a full -t:Rebuild
first-party 171/171, 0 added ✅ 171 first-party each side against my own merge-base b924261, compared as a set: 0 added, 0 removed. The 20-warning gap is third_party OxyPlot

Two body claims were wrong and I corrected them in the body text only — no commit, so CI's green and Qodo's settled review still cover 7a6043f8:

5. One pre-existing nit, not a regression, not worth a commit here

A session that loads with zero samples (initial.IsEmpty in DatabaseLogger.DisplayLoggingSession) leaves HasSessionData false, so the card shows No session selected while a session is selected. main was equally wrong in that state (Select a session to view data), the card was already visible there, and this PR changes neither its visibility nor its reachability — so it is pre-existing. Filed separately rather than pushed into a settled review.

6. Status

  • Qodo: 2 rounds. Round 2's Code Review by Qodo comment says in its own words that it was "updated up to the latest commit 7a6043f8", and reports Bugs (0) / Rule violations (0) / Skill insights (0). GraphQL reviewThreads shows 1 qodo thread, resolved, 0 unresolved (author matched by startswith("qodo-code-review") — the GraphQL login has no [bot] suffix; validated against PR fix(logging): a half-finished Delete All left a table-less database, and the next logging toggle killed the app #196, which yields 3 qodo threads and 0 unresolved as expected).
  • CI: green on 7a6043f8 itself, not an inherited green — run 33995300439, all six real jobs success against head_sha 7a6043f8.
  • Not covered: the Android head was not built here (workload absent on this Mac) — CI's Android head job covers it. No bench/device validation; an empty-state render is not device-observable.

@tylerkron

Copy link
Copy Markdown
Contributor Author

Qodo-clean, CI green — ready for review at 7a6043f8d118f9aa933d8aba50aa34074486505d, which is the current head: Qodo's round-2 review names that commit and reports Bugs (0) / Rule violations (0) / Skill insights (0) with 0 unresolved qodo threads (re-checked on both surfaces 29 min after the round landed), and CI run 33995300439 ran on head_sha 7a6043f8 itself, six real jobs green. Body edits since then are text-only — no commit, so both still cover the head.

Uncovered: the Android head was not built here (workload absent on this Mac); no device/bench validation, since an empty-state render is not device-observable; and there is no automated assertion on the XAML itself, because these views carry no x:DataType. Before merging, note that #256 and #258 conflict with each other in tools/parity-audit/README.md (resolution: keep both paragraphs) — this PR is clear of both, and the five-way merge builds, tests 578/578 and passes the capture gate at 25/25. Detail in the verification comment; one pre-existing copy gap found on the way out is filed as #262. Not merging.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Logged Data's plot region is a featureless black void with no session selected

1 participant