Skip to content

fix(#433): a channel is the same colour on the mobile live plot as it is everywhere else — and the third palette goes away - #435

Merged
tylerkron merged 1 commit into
mainfrom
issue-433-mobile-plot-channel-color
Sep 22, 2026
Merged

tylerkron merged 1 commit into
mainfrom
issue-433-mobile-plot-channel-color

Conversation

@tylerkron

@tylerkron tylerkron commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

Not merging — for review. Closes #433.

What the user sees

On the mobile Stream tab the trace colour is the channel identifier: LivePlot draws the legend as "channel name + latest value, in the trace's colour". That colour was taken from a private 8-colour array indexed by the channel's position in the selected set, so:

  • The same channel was two colours in the same app. Stream AI0 and it is #4FC3F7 light blue on the plot; open the Channels tab and its chip is its real colour, e.g. #D32F2F Red700; open the logged session in Storage and it plots red again, because the viewer replays the colour that was stored. Live and replay of the same channel disagreed.
  • A channel changed colour between two runs with no colour action taken. Stream AI0–AI3, stop, deselect AI0, stream again — every remaining channel shifts one place along the array and comes back a different colour.
  • Past the 8th channel the index wrapped, so on a 16-channel device two traces shared a colour on a plot where colour is the only thing telling them apart — while 38 distinct colours were already assigned.

The fix

The live series now takes the channel's own ChannelColorBrush, the value assigned once at construction and shown by every other surface (desktop live-graph legend, desktop Channels pane, mobile Channels tab, and — via the copy DataSample persists from it — the mobile session viewer). The private palette is deleted.

This is the shape the rest of the app already uses: a plain property read, no converter, no service, no colour-resolution abstraction. ChannelSeries still takes a uint, LivePlot still caches pens by it, and nothing else moved.

The comment above that array claimed it held "the desktop channel colors". It did not — its eight values are Material 300 shades and none of them appears anywhere else in the repo. That comment is gone with the array.

Scope: this does not touch #430

#430 asks which of the app's palettes should be canonical — the 38-colour auto-assign set or the 8-colour desktop picker. That is a product decision and it is untouched here. This change does not pick a palette; it makes the live plot use whatever colour the channel already has, which is what every other surface does today. Whichever way #430 goes, this plot follows it for free. The two issues together were "three mutually disjoint channel palettes"; this removes the third and leaves #430's two for #430.

Simplification

Production diff is +28 / −10, but comments dominate: 19 of the 28 added lines are the XML doc on the replacement, and 2 of the 10 removed lines were the false comment. Counting only code, it is +6 / −8 — net −2 lines, and one fewer palette.

Tests are +193 / −40 (two new files, plus the shared device double lifted out of the watchdog tests instead of copied).

Verification

The new tests compile against unchanged origin/main and genuinely fail there — they reference no new member, only Series, ColorArgb and ChannelColorBrush, all of which exist on main. Run with only Daqifi.Avalonia/Views/MobileShellViewModel.cs reverted to origin/main (source touched, project rebuilt, so the assembly is not stale):

Failed:     3, Passed:     0, Skipped:     0, Total:     3

Every_trace_is_drawn_in_its_own_channels_colour
  Expected: 4292030255   (0xFFD32F2F — AI0's own colour in that run, Red700)
  Actual:   4283417591   (0xFF4FC3F7 — Palette[0])

Deselecting_an_earlier_channel_does_not_recolour_the_others
  Expected: 4294948685   (0xFFFFB74D — what AI1 was before AI0 was deselected)
  Actual:   4283417591   (0xFF4FC3F7 — AI1 shifted into Palette[0])

Sixteen_channels_get_sixteen_distinct_colours
  Expected: 16
  Actual:   8

Those are exactly the issue's Repro A and Repro B, and the wrap.

That first Expected value is run-dependent, and the test deliberately does not assert it as a constant. ChannelColorManager is a process-wide singleton whose _colorCount rotates over 38 brushes, so which entry AI0 is given depends on how many channels the process created before it. Reproduced independently as a whole-suite run against unchanged origin/main (rather than a filtered one), the same three tests fail 3/3 with the same Actual values, but the first Expected reads 4278356177 (0xFF0288D1, LightBlue700) instead of 4292030255. The other two numbers are stable because they are derived inside the test itself. ExpectedArgb reads ChannelColorBrush rather than hard-coding a colour, so the assertion is order-independent even though the number printed on failure is not — expect a different first Expected when you re-run this. Restoring the fix (source touched again, rebuilt) turns all three green — the mutation control ran in both directions.

The tests drive the shell's real StreamToggle command over the real device wrapper and a real Core device, with channels created by Core's own status handling. Nothing about the colour path is faked, and ExpectedArgb derives the expected value from ChannelColorBrush itself rather than calling the view-model's helper, so the assertion is not the shell agreeing with itself.

Build gate (local, under a build lock):

result
Desktop head, Debug succeeded, 135 warning(s), 0 errors
iOS head, Release iossimulator-arm64 succeeded, 156 warning(s), 0 errors
Android head, Debug (AndroidSdkDirectory + EmbedAssembliesIntoApk) succeeded, 6 warning(s), 0 errors
dotnet test, whole suite Passed: 1119, Failed: 0, Skipped: 0

Zero warnings name any changed file (positive control: ConnectionManager.cs, which nothing here touches, contributes 5 in the same log). Warning totals are this worktree's own measurement of this head, not a delta against a quoted baseline. packages.lock.json is untouched in all five lock files.

What I could NOT verify

  • Mobile runtime behaviour. There is no adb and no device or emulator in this environment, so the Stream tab was never rendered with a live stream on it. The Android build proves the head compiles; it proves nothing about what the screen looks like.
  • The capture harness is not evidence here, and I checked rather than assuming. It renders mobile-portrait-1-stream and mobile-landscape-1-stream, but it boots with no device posed for the mobile sweep, so IsConnected is false, ShowDeviceList is true and LivePlot is never in the visual tree. No captured screen can move, so no baseline was re-recorded and the harness was not run for this change.
  • No XAML binding surface changed: the removed Palette was private and never bound, and Plot.Series = _viewModel.Series is a compile-checked code-behind assignment, not a binding.

The evidence for the fix is therefore the unit tests over the real streaming path, in both directions, plus three green heads — not observation of the running app.

🤖 Generated with Claude Code

On the mobile Stream tab the trace colour IS the channel identifier — LivePlot
labels each trace with the channel name and its latest value in that trace's
colour. It was taken from a private 8-colour palette indexed by the channel's
position in the SELECTED set, so:

- the same channel was one colour while streaming and another in the mobile
  Channels tab and in the logged-session viewer, both of which show the
  channel's real ChannelColorBrush;
- deselecting an earlier channel shifted every later channel's colour, so a
  channel changed colour between two runs with no colour action taken;
- past the 8th channel the index wrapped and two traces shared a colour, on a
  16-channel device, while 38 distinct colours were already available.

Read the channel's own colour instead and delete the palette. Its comment
claiming it held "the desktop channel colors" was false — none of its eight
values appears anywhere else in the repo.

No new palette and no colour-resolution abstraction: every other surface
resolves this with a plain binding or a direct property read, and so does this
now. Which palette should be canonical is #430's question and is untouched here
— this change uses whatever colour the channel already has.

Tests drive the shell's real StreamToggle over the real device wrapper and pin
both repros. The shared ShellTestDevice double is lifted out of the watchdog
tests rather than copied.

Not verified at runtime on mobile: no adb and no device in this environment.

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

Copy link
Copy Markdown
Contributor Author

/agentic_review

@qodo-code-review

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

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider

Great, no issues found!

Qodo reviewed your code and found no material issues that require review

Grey Divider

Tip of the day
💡 Did you know, you can add REVIEW.md to your repo root and Qodo follows it on every PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Previous reviews

Review updated until commit 2bacbf3 ⚖️ Balanced

Results up to commit 2bacbf3 🚀 Fast


No changes from previous review

Grey Divider

Qodo Logo

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Use channel colors consistently in the mobile live plot

🐞 Bug fix 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Use each channel's assigned color for mobile live plot traces.
• Preserve trace colors across selection changes and beyond eight channels.
• Add regression coverage and share the mobile shell test device.
Diagram

graph TD
  Channels["Core Channels"] -->|assigned brush| Shell["Mobile Shell"] -->|creates colored series| Series["Channel Series"] -->|renders traces| Plot["Live Plot"]
  Channels -->|same brush| Views["Other Views"]
Loading
High-Level Assessment

The direct ChannelColorBrush read is the best approach because it follows the application's existing color ownership model without introducing another palette or resolution abstraction. A centralized color service or replacement plot palette would add indirection and overlap with the separate canonical-palette decision tracked by #430.

Files changed (4) +221 / -50

Bug fix (1) +28 / -10
MobileShellViewModel.csDerive live trace colors from channel brushes +28/-10

Derive live trace colors from channel brushes

• Removes the private eight-color, selection-indexed palette and creates each ChannelSeries from its channel's ChannelColorBrush. Non-solid brushes use the same gray fallback as stored session colors, keeping live and replay rendering consistent.

Daqifi.Avalonia/Views/MobileShellViewModel.cs

Tests (3) +193 / -40
MobileShellSeriesColorTests.csAdd mobile live-series color regression coverage +144/-0

Add mobile live-series color regression coverage

• Adds integration-style tests that stream through the real mobile shell command and channel setup. The tests verify channel-owned colors, stability after deselection, and distinct colors across all 16 analog channels.

Daqifi.Avalonia.Tests/Views/MobileShellSeriesColorTests.cs

MobileShellSilentStreamWatchdogTests.csReuse the shared mobile shell test device +2/-40

Reuse the shared mobile shell test device

• Replaces the watchdog suite's private device wrapper and reflective adoption helper with the shared ShellTestDevice fixture. Existing watchdog behavior remains unchanged.

Daqifi.Avalonia.Tests/Views/MobileShellSilentStreamWatchdogTests.cs

ShellTestDevice.csExtract a reusable mobile shell device fixture +47/-0

Extract a reusable mobile shell device fixture

• Introduces a shared test device backed by a real core streaming device, including synchronization and shell-adoption helpers. This removes duplicated reflection and device scaffolding from mobile shell tests.

Daqifi.Avalonia.Tests/Views/ShellTestDevice.cs

@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 2bacbf3

@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 2bacbf3

@tylerkron

Copy link
Copy Markdown
Contributor Author

Qodo-clean, CI green — ready for review. Head 2bacbf3 throughout; no commit was pushed after the PR opened, so every number in the body was derived at this SHA.

Two clean rounds, both settled, both surfaces checked.

trigger summary updated_at anchored to result grade
R1 23:41:58Z 23:43:21Z 2bacbf3 Bugs 0 / Rule violations 0 / Requirement gaps 0 🚀 Fast
R2 23:48:21Z 23:49:10Z 2bacbf3 Bugs 0 / Rule violations 0 / Requirement gaps 0 🚀 Fast

Each round's updated_at advanced past its own trigger and the re-rendered summary was read as content (a review naming this head with finding counts), not counted on the advance alone. Inline threads checked separately via GraphQL reviewThreads with a startswith("qodo-code-review") author test — 0 qodo threads, 0 unresolved, re-checked after a >3 min settle on both rounds. The author filter was validated against control PR #196, which yields 3 qodo-authored threads.

Disclosure on depth. Both rounds graded 🚀 Fast, and R2's rationale reads "No changes from previous review". This PR is a single commit, so both rounds' delta was the whole diff — there is no part of it that went unreviewed — but a deeper grade is unreachable here without manufacturing a commit, which is not something I will do. Qodo's own summary of the change describes all four files individually and its assessment agrees with the design choice ("follows the application's existing color ownership model without introducing another palette or resolution abstraction"), so the round read production code rather than being scoped to docs.

CI green on this exact head, all six jobs: Desktop head on macOS, iOS head, Android head, Library + Desktop head + capture harness, CI script self-tests, Avalonia version lockstep.

That capture-harness job is worth naming, because it independently closes a claim in the PR body: the body says this change cannot move a captured screen (the mobile sweep poses no device, so LivePlot is never in the visual tree) and that no baseline was therefore re-recorded. CI's baseline check passing on this head is that claim confirmed from the other end.

Still not verified, and not rounded up: mobile runtime behaviour. No adb, no device or emulator here. Three heads compile and the unit tests pin the colour mapping in both directions, but nobody has looked at the running Stream tab.

@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 2bacbf3

@tylerkron

Copy link
Copy Markdown
Contributor Author

Shepherd verification — Qodo-clean, CI green, ready for review. Head 2bacbf30c364d622806ca772d182d76f19907bfd (2bacbf3), unchanged throughout; this note and the body edit below create no commit, so CI's green and Qodo's review both still cover the current head.

The fourth file, which the change summary did not mention

Daqifi.Avalonia.Tests/Views/MobileShellSilentStreamWatchdogTests.cs (+2/−40) belongs to a different open issue (#358, the silent-stream watchdog), so I diffed it before anything else. It is mechanical, and it is behaviour-neutral. The 40 removed lines are exactly the private nested ShellTestDevice double and the private Adopt reflection helper, both lifted verbatim into the new shared ShellTestDevice.cs; the 2 added lines are the two call sites that follow them (new ShellTestDevice("Watchdog test device") and _device.AdoptInto(_shell)). No watchdog [Fact], assertion, timeout or arrangement was touched, nothing was weakened or skipped, and AdoptInto still reaches MobileShellViewModel.AdoptConnectedDevice by reflection exactly as the old helper did — the tests still exercise the shell's real adoption rather than a copy. No bearing on #358. Independent confirmation below: with only this PR's test files applied to unchanged origin/main, every watchdog test still passes.

Re-proof: the three new tests fail on unchanged origin/main

Run as the whole suite (not a --filter pass), with only the three test files taken from this PR and MobileShellViewModel.cs left at origin/main, sources touched so no stale assembly was used:

Failed!  - Failed: 3, Passed: 1116, Skipped: 0, Total: 1119

All three, and only those three. The Passed: 1116 equals main's own CI total, which is the watchdog-extraction check above. Actual values reproduce the body exactly (4283417591 = 0xFF4FC3F7 = Palette[0], twice; 8 distinct colours on 16 channels).

One correction, now fixed in the body in place. The first test's Expected is run-dependent: ChannelColorManager is a process-wide singleton rotating _colorCount over 38 brushes, so AI0's colour depends on how many channels the process built before it. My whole-suite run printed Expected: 4278356177 (0xFF0288D1, LightBlue700), not the body's 4292030255 (0xFFD32F2F, Red700). Nothing is wrong with the test — ExpectedArgb derives the value from ChannelColorBrush instead of hard-coding it, so the assertion is order-independent — but the body read as if Red700 were a fixed fact, and a reviewer re-running would have seen a different number and wondered. Body corrected; no commit.

Scoping held — #430 is untouched

Confirmed from the diff, not the summary: ChannelColorManager.cs and AnalogChannel.cs are not among the four changed files, and the change picks no palette — it reads whatever colour the channel already carries. #430's product decision stays open.

Line counts check out, and the honest figure is the one quoted. Raw production diff +28/−10; excluding comments and blanks, +6/−8 (the 6: the using, the four-line SeriesColorArgb, the one changed ChannelSeries construction; the 8: the 5-line Palette, var i = 0, the old construction, i++). Net −2 code lines and one palette fewer. Both figures are stated in the body, with the distinction named rather than the flattering one quoted alone.

My own Qodo round — clean, and at a deeper grade than the previous two

trigger summary updated_at anchored to result grade
R3 (this one) 00:01:24Z 00:03:01Z 2bacbf3 Bugs 0 / Rule violations 0 / Requirement gaps 0 ⚖️ Balanced

Timestamp recorded before posting. The advance was not taken as sufficient — the re-rendered summary was read as content and is a review naming this head with finding counts. Settled ≥3 min, then both surfaces re-checked: summary 0/0/0, and GraphQL reviewThreads with a startswith("qodo-code-review") author test gives 0 qodo threads, 0 unresolved (author leg validated against control PR #196 → 3 threads). R1 and R2 both graded 🚀 Fast and R2's own rationale read "No changes from previous review"; this round upgraded to ⚖️ Balanced on the same head and still found nothing, which is a stronger statement than either earlier round made.

One attribution to correct in the ready note above

The baseline capture check is not in the "Library + Desktop head + capture harness" job — that job only compiles AvaloniaCapture as a gate, and its only run step is HeadlessBench --scripted. The run.sh --determinism and --check-captured steps live in "Desktop head on macOS". The substance stands (CI did check the captures on this head, and they passed); only the job name was wrong.

Eleven-way merge — this subsumes the sweep

main + #418 9ad3811 + #419 b3c00d5 + #420 2ea658b + #422 a34c696 + #423 b55a6b6 + #425 081aaa1 + #426 f8966b0 + #428 453323e + #431 deb7bb7 + #432 9302f7a + #435 2bacbf3. Zero conflicts. Eleven-way tree 0a4e5b010eaf34526a2a8a5980ad60d92942ee74.

All six published intermediate trees reproduced byte-identically: 5-way 8599ae34, 6-way fd7c5e1b, 7-way 1369c47c, 8-way 2fb26b57, 9-way a0099579, 10-way 3584ccaa.

Suite on the merged tree: Passed: 1182, Failed: 0, Skipped: 0 — three numbers, whole suite. Reconciled from both ends:

Three heads, all green on the merged tree (Daqifi.Avalonia library is the denominator I'm quoting, since it is the one comparable across heads):

head result
Desktop, Debug succeeded, 0 errors — library 130 warnings (whole build 150, incl. 20 from third_party/oxyplot-avalonia)
iOS, Release iossimulator-arm64 succeeded, 0 errors — library 130 + iOS head 1 = 131
Android, Debug (AndroidSdkDirectory + EmbedAssembliesIntoApk) succeeded, 0 errors — 7 warnings (Android head only; library cached)

The 135 → 130 drop is fully accounted for, and it is not this PR. De-duplicated on (file, line, col, code), main's five surplus warnings are all CS8618 in ConnectionManager.cs at 237,13 / 1282,29 / 1284,29 / 1286,19 / 1288,19 (CI's macOS log prints each twice — 10 lines, 5 warnings). The only commit in the merge touching that file is #425 081aaa1, the DuplicateDeviceCheckResult → positional-record refactor, which is exactly the shape that removes them. 135 − 5 = 130. #435 itself contributes zero: no warning in any of the three merged-tree logs names MobileShellViewModel.cs (grep validated on the same logs against FirewallManager.cs, which yields 9 — a zero here is a real zero, not a failed search).

Capture gate on the merged tree: 39/39, and 39 is the number it should be. tools/parity-audit/baselines/macos-arm64.sha256 is 39 lines, the harness captured 39 PNGs, and 39 : OK lines came back — no screen moved and no baseline was re-recorded. Machine load was light (1.6) throughout, so this is not a green bought by a quiet machine. Consistent with the body's argument that this change cannot move a captured screen: the mobile sweep poses no device, so LivePlot never enters the visual tree. That argument, not the 39/39, is the evidence for this change — the gate is evidence for the other ten PRs in the tree.

packages.lock.json clean across every project after all restores and builds (git status --short -- '**/packages.lock.json' empty).

Still not verified, and not rounded up

Mobile runtime behaviour. adb does exist here (/opt/homebrew/share/android-commandlinetools/platform-tools/adb) — I ran adb devices rather than assuming, and it lists none. So the Stream tab has still never been rendered with a live stream on it. Three heads compile, the unit tests pin the colour mapping in both directions on the real streaming path, and Qodo has now passed it at Balanced. Nobody has looked at the running plot.

Not merged.

🤖 Generated with Claude Code

@tylerkron

Copy link
Copy Markdown
Contributor Author

Three-way merge: main + #435 + #436 + #437

Built on a throwaway branch on top of origin/main 97be4df. The branches were merged in this order: #435 2bacbf3, #436 b36c347, #437 d50ef98. All three merged with no conflicts. Result: commit 81ff5dc, tree bd5c5110b4d5058b414293b8a8879f660dcb0987.

Gate on the merged tree: desktop build, iOS restore, iOS Release -r iossimulator-arm64 --no-restore, and the full dotnet test all exit 0. I did not build the Android head locally.

Test count reconciles: 1199 passed / 0 failed / 0 skipped = main 1179 (I measured it) +3 (#435) +6 (#436) +11 (#437).

Ready notes against current heads:

PR Ready-note SHA Head now CI green on head CI merge base
#435 2bacbf3 2bacbf3 ✓ yes (run 35545404407) 4716ce1, before #418 #419 #420 #422 #423 #425 #426 #428 #431 #432
#436 b36c347 b36c347 ✓ yes (run 35547335403) 4716ce1, same 10 merges missed
#437 this PR's note above d50ef98 ✓ yes (run 35649810309) 97be4df = current main

So the greens for #435 and #436 come from before the last ten merges to main. This merged tree is the build that covers them against current main. It passes on desktop, iOS and tests, but not Android.

Collision audit (what changed across the three PRs):

🤖 Generated with Claude Code

@tylerkron

Copy link
Copy Markdown
Contributor Author

Sweep: five-way merge of main + #435 + #436 + #437 + #443 + #446 is clean

I built all five PRs merged onto origin/main 97be4df, using the heads 2bacbf3, b36c347, d50ef98, 9e29bb4 and 40bfef0. Nothing was pushed and nothing was merged.

@tylerkron

Copy link
Copy Markdown
Contributor Author

Sweep update: the six-way merge of main 97be4df + #435 + #436 + #437 + #443 + #446 + #449 is clean. The tree is b5f3d4d7, and the five-way 27e0b21c reproduced exactly. The full gate, HeadlessBench --no-incremental, 1208/1208 tests and parity 39/39 all pass, and this PR's head is unchanged. Details are on #449.

@tylerkron
tylerkron added this pull request to the merge queue Sep 22, 2026
Merged via the queue into main with commit a1b2713 Sep 22, 2026
7 checks passed
@tylerkron
tylerkron deleted the issue-433-mobile-plot-channel-color branch September 22, 2026 05:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant