Skip to content

chore(loggers): extract PlotModelFactory from DatabaseLogger (#592)#608

Merged
tylerkron merged 2 commits into
mainfrom
chore/extract-plot-model-factory
Jun 18, 2026
Merged

chore(loggers): extract PlotModelFactory from DatabaseLogger (#592)#608
tylerkron merged 2 commits into
mainfrom
chore/extract-plot-model-factory

Conversation

@tylerkron

Copy link
Copy Markdown
Contributor

What & why

Next behavior-preserving extraction for #592 (breaking up the app's two largest
classes, one extraction per PR, ~800-line target). Follows the playbook from the
merged SessionSampleWriter (#604) and SessionDataRepository (#607): a sealed
collaborator, constructor-only dependencies, no reach into desktop singletons,
unit-testable in isolation.

This PR moves the pure OxyPlot construction out of DatabaseLogger into a new
PlotModelFactory. The logger stays the orchestration + live-model
mutation/binding surface (the LoggedDataPanePrototype.xaml binding contract —
PlotModel, MinimapPlotModel, DeviceLegendGroups, HasSessionData,
IsRefiningData, IsLegendPanelVisible, CurrentSession.*, and the
Zoom*/ResetZoom/SaveGraph/ToggleLegendPanel commands — is unchanged).

DatabaseLogger.cs: 1,376 → 1,119 lines.

Moved into PlotModelFactory (construction only — no EF, threading, Dispatcher, InvalidatePlot, or axis mutation)

  • CreateMainPlotModel — the main model + Analog/Digital/Time LinearAxis
    config, OxyPlotDarkTheme.ApplyTo, axis Add, IsLegendVisible=false.
  • CreateMinimapPlotModel — the minimap model, its two non-interactive axes,
    and the three RectangleAnnotations (selection rect + two dim overlays).
    Returns a MinimapPlotComponents record so the logger keeps the annotation
    handles it mutates.
  • CreateChannelSeries — a channel's LineSeries + LoggedSeriesLegendItem
    with the Analog/Digital YAxisKey mapping. The legend item still gets the live
    PlotModel + DatabaseLogger, so its visibility toggle invalidates the plot
    and syncs the minimap exactly as before.
  • CreateMinimapSeries — the per-channel minimap LineSeries construction
    only; the apply-to-live half of SetupMinimapSeries (Series.Clear/Add, the
    _minimapSeries map, axis.Zoom, annotation bounds, InvalidatePlot) stays in
    the logger.
  • Folds LoggedSeriesLegendItem into its own file (one-main-class-per-file
    per the style guide) and annotates its optional logger as nullable.

The shared axis-key strings (Analog/Digital/Time/MinimapTime/MinimapY)
become public consts on PlotModelFactory — the single source of truth the
factory builds from and the logger's viewport lookups key on.

Kept in DatabaseLogger (viewport/live-model coupled — untouched)

Every viewport/minimap-sync method (OnMainTimeAxisChanged, throttle/settle
ticks, UpdateMainPlotViewport, UpdateSeriesFromMemory,
FetchViewportDataFromDb, OnMinimap*, SetMinimapSeriesVisibility), the two
DispatcherTimers, the AxisChanged subscription, the
MinimapInteractionController wiring, the annotation fields, and every
InvalidatePlot/Zoom/axis-mutation call. The ADR-001 viewport controller is a
deliberately separate, future extraction — not this PR.

Invariants preserved (ADR-001 / CLAUDE.md "Plot Rendering")

  • InvalidatePlot(true) on every ItemsSource change — all stay in the logger.
  • No ResetAllAxes() with downsampled data; ResetZoom still does per-axis
    Reset() + explicit timeAxis.Zoom(...).
  • IsSyncingFromMinimap guard still wraps programmatic axis changes.
  • _downsampledCache still reused — no new per-frame allocations.
  • ClearPlot() still resets every axis on session switch.

The factory holds zero InvalidatePlot/Zoom/axis-mutation calls.

Tests

  • New PlotModelFactoryTests (13 tests, no DB / WPF runtime / threads — a real
    test seam where none existed): axis keys/positions, the Analog→Analog /
    Digital→Digital YAxisKey mapping, color parsing, minimap annotation/axis
    config, annotation z-order, and returned-handle identity.
  • Unit gate green: 434/434 (dotnet test --filter "TestCategory!=Ui&FullyQualifiedName!~WindowsFirewallWrapperTests").
  • Integration gate green on the attached device — both logged-data-plot FlaUI
    scenarios pass:
    LoggingSessionTests.StartLoggingSession_RendersLivePlot_RunsStopsAndDeletesSession
    and LoggingSessionTests.ViewLoggedSession_LoadsStoredSessionOntoLoggedDataPlot.

Also updates the CLAUDE.md plot-gotchas Key files list to name
PlotModelFactory.cs.

🤖 Generated with Claude Code

Next behavior-preserving extraction in the #592 effort to break up the app's
two largest classes (after SessionSampleWriter #604 and SessionDataRepository
#607). Moves the pure OxyPlot *construction* out of DatabaseLogger into a new
sealed PlotModelFactory, leaving the logger as the orchestration + live-model
mutation/binding surface.

Moved into PlotModelFactory (pure construction only — no EF, threading,
Dispatcher, InvalidatePlot, or axis mutation):
- The main plot model + Analog/Digital/Time LinearAxis config, dark-theme
  application, axis Add, and IsLegendVisible=false (was the constructor's
  axis-setup block).
- The minimap model, its two non-interactive axes, and the three
  RectangleAnnotations (selection rect + two dim overlays). The factory returns
  the annotation handles so the logger keeps its field references to mutate.
- CreateChannelSeries: a channel's LineSeries + LoggedSeriesLegendItem, with the
  Analog/Digital YAxisKey mapping. The legend item still receives the live
  PlotModel and the DatabaseLogger so its visibility toggle invalidates the plot
  and mirrors onto the minimap exactly as before.
- CreateMinimapSeries: the per-channel minimap LineSeries construction only — the
  apply-to-live half of SetupMinimapSeries (Series.Clear/Add, the _minimapSeries
  map, axis.Zoom, annotation bounds, InvalidatePlot) stays in the logger.

Also moves LoggedSeriesLegendItem to its own file (one-main-class-per-file per
the style guide) and annotates its optional logger as nullable.

Kept in DatabaseLogger (viewport/live-model coupled, untouched): every
viewport/minimap-sync method, the throttle/settle DispatcherTimers, the
timeAxis.AxisChanged subscription, the MinimapInteractionController wiring, the
annotation fields, and every InvalidatePlot/Zoom/axis-mutation call. The ADR-001
viewport controller remains a future, separate extraction.

The shared axis-key strings ("Analog"/"Digital"/"Time"/"MinimapTime"/
"MinimapY") become public consts on PlotModelFactory — the single source of
truth the factory builds from and the logger's viewport lookups key on.

Invariants (ADR-001 / CLAUDE.md "Plot Rendering") preserved: InvalidatePlot(true)
on every ItemsSource change, no ResetAllAxes() with downsampled data, the
IsSyncingFromMinimap guard, _downsampledCache reuse, and ClearPlot()'s per-axis
reset all stay in the logger. No viewport machinery moved.

DatabaseLogger.cs: 1,376 -> 1,119 lines. PlotModelFactory is unit-testable with
no WPF runtime, DB, or threads — a new PlotModelFactoryTests (13 tests) asserts
axis keys/positions, the channel YAxisKey mapping, color parsing, minimap
annotation/axis config, annotation z-order, and returned-handle identity.

Updates the CLAUDE.md plot-gotchas "Key files" list to name PlotModelFactory.cs.

Tests: unit gate green (434/434, incl. the 13 new). Both logged-data-plot FlaUI
scenarios pass on the attached device
(LoggingSessionTests.StartLoggingSession_RendersLivePlot_RunsStopsAndDeletesSession
and ViewLoggedSession_LoadsStoredSessionOntoLoggedDataPlot).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@tylerkron tylerkron requested a review from a team as a code owner June 18, 2026 17:45
@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Extract PlotModelFactory from DatabaseLogger for pure OxyPlot construction
✨ Enhancement 🧪 Tests 📝 Documentation 🕐 20-40 Minutes

Grey Divider

Description

• Extract pure OxyPlot model/series construction into a new PlotModelFactory.
• Keep DatabaseLogger focused on live viewport/minimap mutation and UI binding.
• Add unit tests covering axis keys, theming, minimap annotations, and series mapping.
Diagram

graph TD
  UI["LoggedData pane (XAML bindings)"] --> Logger["DatabaseLogger"] --> Factory["PlotModelFactory"]
  Factory --> Main[("Main PlotModel")]
  Factory --> Mini[("Minimap PlotModel")]
  Factory --> Legend["LoggedSeriesLegendItem"]
  Logger --> Controller["MinimapInteractionController"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Inject PlotModelFactory (interface) into DatabaseLogger
  • ➕ Enables swapping a test/dummy factory without constructing real OxyPlot objects in logger tests
  • ➕ Clarifies dependency graph and avoids new PlotModelFactory() inside DatabaseLogger
  • ➖ Slightly more plumbing (DI registration, interface definition) for a currently simple collaborator
  • ➖ May be premature if only DatabaseLogger uses the factory
2. Static factory methods on PlotModelFactory
  • ➕ Zero allocation/state; simplest call sites
  • ➕ Avoids keeping a factory field on DatabaseLogger
  • ➖ Harder to evolve if dependencies/configuration are later introduced
  • ➖ Less consistent with the sealed-collaborator extraction pattern described in the PR

Recommendation: Current extraction is sound and preserves the critical invariant: DatabaseLogger retains all live viewport/minimap mutation and invalidation. The main follow-up worth considering is constructor injection (or an interface) for PlotModelFactory if you want consistent DI patterns and easier substitution in higher-level tests; otherwise keeping a private readonly instance is acceptable given the factory is stateless.

Files changed (5) +678 / -291

Enhancement (1) +316 / -0
PlotModelFactory.csIntroduce PlotModelFactory for pure OxyPlot construction +316/-0

Introduce PlotModelFactory for pure OxyPlot construction

• Adds a sealed factory that builds the main PlotModel (axes + theme), minimap PlotModel (axes + dim/selection annotations returned as handles), channel series + legend items, and minimap series. Centralizes axis-key strings as public constants to preserve viewport/minimap lookup contracts.

Daqifi.Desktop/Loggers/PlotModelFactory.cs

Refactor (2) +123 / -290
DatabaseLogger.csDelegate plot/minimap construction to PlotModelFactory +33/-290

Delegate plot/minimap construction to PlotModelFactory

• Replaces inline OxyPlot axis/model/series construction with calls to PlotModelFactory, while keeping all viewport/minimap synchronization logic and plot invalidation in the logger. Updates all axis lookups to use shared axis-key constants.

Daqifi.Desktop/Loggers/DatabaseLogger.cs

LoggedSeriesLegendItem.csMove LoggedSeriesLegendItem to its own file and make logger nullable +90/-0

Move LoggedSeriesLegendItem to its own file and make logger nullable

• Extracts the legend-item type from DatabaseLogger into a standalone file and annotates the optional DatabaseLogger reference as nullable, preserving the minimap visibility-sync callback behavior.

Daqifi.Desktop/Loggers/LoggedSeriesLegendItem.cs

Tests (1) +238 / -0
PlotModelFactoryTests.csAdd unit tests for PlotModelFactory construction contracts +238/-0

Add unit tests for PlotModelFactory construction contracts

• Introduces focused unit tests validating main/minimap axis keys and configuration, dark theme application, channel-to-axis mapping, series metadata (tags/tracker), and minimap annotation ordering/handles.

Daqifi.Desktop.Test/Loggers/PlotModelFactoryTests.cs

Documentation (1) +1 / -1
CLAUDE.mdDocument PlotModelFactory as a key plot/minimap file +1/-1

Document PlotModelFactory as a key plot/minimap file

• Updates the plot/minimap gotchas section to include PlotModelFactory.cs and clarifies DatabaseLogger’s role as orchestration/live-model mutation.

CLAUDE.md

@qodo-code-review

qodo-code-review Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (2) 📎 Requirement gaps (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 48 rules

Grey Divider


Action required

1. PlotModelFactory not injected 📘 Rule violation ⌂ Architecture
Description
DatabaseLogger directly instantiates PlotModelFactory, introducing a hard dependency that
reduces testability and violates the dependency-injection requirement.
Code

Daqifi.Desktop/Loggers/DatabaseLogger.cs[134]

+        _plotModelFactory = new PlotModelFactory();
Evidence
Rule 244829 requires dependencies to be injected rather than instantiated inside classes. The added
line _plotModelFactory = new PlotModelFactory(); instantiates the dependency in the constructor.

Rule 244829: Inject dependencies instead of instantiating them inside classes
Daqifi.Desktop/Loggers/DatabaseLogger.cs[131-136]

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

## Issue description
`DatabaseLogger` constructs `PlotModelFactory` internally (`new PlotModelFactory()`), instead of receiving it via dependency injection.

## Issue Context
This PR introduces `PlotModelFactory` as a collaborator. Per the compliance rule, collaborators/services should be injected (constructor parameter or DI container) rather than instantiated inside the class.

## Fix Focus Areas
- Daqifi.Desktop/Loggers/DatabaseLogger.cs[131-140]

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


2. Two public types in file ✓ Resolved 📘 Rule violation ⌂ Architecture
Description
PlotModelFactory.cs defines both MinimapPlotComponents and PlotModelFactory as public
top-level types, violating the single-main-class-per-file requirement.
Code

Daqifi.Desktop/Loggers/PlotModelFactory.cs[R23-44]

+public sealed record MinimapPlotComponents(
+    PlotModel Model,
+    RectangleAnnotation SelectionRect,
+    RectangleAnnotation DimLeft,
+    RectangleAnnotation DimRight);
+
+/// <summary>
+/// Owns the pure OxyPlot <em>construction</em> extracted from <see cref="DatabaseLogger"/> (issue #592):
+/// building the main logged-data <see cref="PlotModel"/> and its Analog/Digital/Time axes, the overview
+/// minimap model with its axes and selection/dim annotations, a channel's <see cref="LineSeries"/> plus
+/// its <see cref="LoggedSeriesLegendItem"/>, and the minimap's per-channel line series.
+/// <para>
+/// Every method is a side-effect-free constructor of OxyPlot objects — no EF, no threading, no
+/// Dispatcher, no <c>InvalidatePlot</c>, no live-axis mutation — so the factory is unit-testable without
+/// a WPF runtime, a database, or background threads, and has no dependency on desktop singletons
+/// (<c>App.ServiceProvider</c>, <c>AppLogger.Instance</c>). <see cref="DatabaseLogger"/> remains the
+/// composition root and the live-model owner: it calls the factory to build the models, then keeps every
+/// viewport/minimap-sync mutation (axis subscription, <c>Zoom</c>, annotation bounds, <c>InvalidatePlot</c>)
+/// to itself.
+/// </para>
+/// </summary>
+public sealed class PlotModelFactory
Evidence
Rule 244819 limits each source file to a single main/public type. The file introduces a public
record (MinimapPlotComponents) and a public class (PlotModelFactory) in the same file.

Rule 244819: Limit each source file to a single main class
Daqifi.Desktop/Loggers/PlotModelFactory.cs[23-44]

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

## Issue description
`Daqifi.Desktop/Loggers/PlotModelFactory.cs` contains more than one public/top-level type (`MinimapPlotComponents` and `PlotModelFactory`).

## Issue Context
The compliance rule limits each source file to a single main class/type.

## Fix Focus Areas
- Daqifi.Desktop/Loggers/PlotModelFactory.cs[23-44]

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



Remediation recommended

3. AxisKey consts wrong casing ✓ Resolved 📘 Rule violation ✧ Quality
Description
Several newly added constants (both public axis-key constants like AnalogAxisKey/MinimapYAxisKey
and test fixture constants like Serial/Color) are named in PascalCase instead of
SCREAMING_SNAKE_CASE. This violates the constant naming convention required by the compliance
checklist (Rule 244814).
Code

Daqifi.Desktop/Loggers/PlotModelFactory.cs[R48-60]

+    public const string AnalogAxisKey = "Analog";
+
+    /// <summary>Key of the right-hand digital Y axis on the main plot.</summary>
+    public const string DigitalAxisKey = "Digital";
+
+    /// <summary>Key of the bottom time (ms) X axis on the main plot.</summary>
+    public const string TimeAxisKey = "Time";
+
+    /// <summary>Key of the minimap's time X axis.</summary>
+    public const string MinimapTimeAxisKey = "MinimapTime";
+
+    /// <summary>Key of the minimap's value Y axis.</summary>
+    public const string MinimapYAxisKey = "MinimapY";
Evidence
Rule 244814 requires constants to be named using SCREAMING_SNAKE_CASE, but the cited additions show
constants declared as public const string axis-key values (e.g., AnalogAxisKey,
DigitalAxisKey, MinimapYAxisKey) and private const string test fixture values (Serial,
Color) that are written in PascalCase rather than SCREAMING_SNAKE_CASE, demonstrating
non-compliance with the rule.

Rule 244814: Use SCREAMING_SNAKE_CASE for constant names
Daqifi.Desktop/Loggers/PlotModelFactory.cs[46-60]
Daqifi.Desktop.Test/Loggers/PlotModelFactoryTests.cs[22-23]

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

## Issue description
Constants were introduced using PascalCase identifiers (e.g., `AnalogAxisKey`, `DigitalAxisKey`, `MinimapYAxisKey`, and test constants `Serial`/`Color`) instead of the required SCREAMING_SNAKE_CASE.

## Issue Context
Rule 244814 requires constants to be named using SCREAMING_SNAKE_CASE. The affected constants include `public const string` values intended as shared axis-key constants and `private const string` values used as fixtures in unit tests.

## Fix Focus Areas
- Daqifi.Desktop/Loggers/PlotModelFactory.cs[46-60]
- Daqifi.Desktop.Test/Loggers/PlotModelFactoryTests.cs[22-24]

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


4. CLAUDE.md line exceeds 120 📘 Rule violation ✧ Quality
Description
The updated Plot/Minimap bullet in CLAUDE.md is a single very long line, exceeding the 120
character maximum line length requirement.
Code

CLAUDE.md[329]

+- **Plot/Minimap changes**: Read the "Plot Rendering (OxyPlot)" section below — there are non-obvious gotchas with `InvalidatePlot`, auto-range, and feedback loops. Key files: `DatabaseLogger.cs` (orchestration + live-model mutation/binding surface), `PlotModelFactory.cs` (pure OxyPlot construction — axes, theme, minimap model + annotations, channel/minimap series), `MinimapInteractionController.cs`, `MinMaxDownsampler.cs`
Evidence
Rule 244820 enforces a maximum line length of 120 characters for added/modified lines. The updated
Plot/Minimap bullet is a single long line that exceeds this limit.

Rule 244820: Enforce maximum line length of 120 characters
CLAUDE.md[329-329]

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

## Issue description
A newly modified markdown line exceeds the 120 character maximum line length.

## Issue Context
The Plot/Minimap key files bullet was expanded and is now a single long line.

## Fix Focus Areas
- CLAUDE.md[329-329]

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


5. Null Dispatcher dereference ✓ Resolved 🐞 Bug ☼ Reliability
Description
LoggedSeriesLegendItem.IsVisible dereferences Application.Current.Dispatcher without a null check,
so toggling visibility throws NullReferenceException in unit tests or any non-WPF host where
Application.Current is null. This breaks the PR’s stated “no WPF runtime” test seam for plot
construction if visibility is exercised.
Code

Daqifi.Desktop/Loggers/LoggedSeriesLegendItem.cs[R38-51]

+    public bool IsVisible
+    {
+        get => _isVisible;
+        set
+        {
+            if (SetProperty(ref _isVisible, value) && ActualSeries != null)
+            {
+                ActualSeries.IsVisible = _isVisible;
+                Application.Current.Dispatcher.Invoke(() =>
+                {
+                    _plotModel?.InvalidatePlot(true);
+                    _databaseLogger?.SetMinimapSeriesVisibility(_deviceSerialNo, _channelName, _isVisible);
+                });
+            }
Evidence
The setter directly dereferences Application.Current.Dispatcher, which is null in non-WPF hosts;
the new tests explicitly describe running without a WPF runtime while constructing these objects via
the factory.

Daqifi.Desktop/Loggers/LoggedSeriesLegendItem.cs[38-51]
Daqifi.Desktop.Test/Loggers/PlotModelFactoryTests.cs[11-17]

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

### Issue description
`LoggedSeriesLegendItem.IsVisible` unconditionally calls `Application.Current.Dispatcher.Invoke(...)`. In headless/unit-test execution, `Application.Current` can be null, causing a `NullReferenceException` when `IsVisible` is set.

### Issue Context
This PR introduces `PlotModelFactoryTests` explicitly aiming to construct plot/legend objects “with no WPF runtime”. The extracted `LoggedSeriesLegendItem` should therefore be resilient when `Application.Current` is unavailable.

### Fix
Update `LoggedSeriesLegendItem.IsVisible` to:
- Use `Application.Current?.Dispatcher` and fall back to executing the invalidation/sync inline when the dispatcher is unavailable.
- Optionally, avoid `Invoke` if already on the dispatcher thread (use `CheckAccess()`), to reduce re-entrancy risk.

Example shape:
```csharp
var dispatcher = Application.Current?.Dispatcher;
Action work = () => {
   _plotModel.InvalidatePlot(true);
   _databaseLogger?.SetMinimapSeriesVisibility(_deviceSerialNo, _channelName, _isVisible);
};

if (dispatcher == null || dispatcher.CheckAccess()) work();
else dispatcher.Invoke(work);
```

### Fix Focus Areas
- Daqifi.Desktop/Loggers/LoggedSeriesLegendItem.cs[38-53]

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


Grey Divider

Qodo Logo

// Pure OxyPlot construction (axes, theme, minimap model + annotations, series) lives in
// PlotModelFactory (issue #592). The logger keeps every live mutation: the axis subscription
// below and all viewport/minimap-sync machinery.
_plotModelFactory = new PlotModelFactory();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. plotmodelfactory not injected 📘 Rule violation ⌂ Architecture

DatabaseLogger directly instantiates PlotModelFactory, introducing a hard dependency that
reduces testability and violates the dependency-injection requirement.
Agent Prompt
## Issue description
`DatabaseLogger` constructs `PlotModelFactory` internally (`new PlotModelFactory()`), instead of receiving it via dependency injection.

## Issue Context
This PR introduces `PlotModelFactory` as a collaborator. Per the compliance rule, collaborators/services should be injected (constructor parameter or DI container) rather than instantiated inside the class.

## Fix Focus Areas
- Daqifi.Desktop/Loggers/DatabaseLogger.cs[131-140]

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to respectfully push back on this one. PlotModelFactory is a stateless, dependency-free pure factory — no EF, logger, or external services, so there's nothing to mock or substitute — and it's constructed exactly the way the two already-merged #592 collaborators are in this same constructor: _sampleWriter = new SessionSampleWriter(...) and _sessionDataRepository = new SessionDataRepository(...).

DatabaseLogger is the composition root here: it's instantiated directly by DaqifiViewModel (new DatabaseLogger(GetLoggingContextFactory())), not resolved from a DI container. The established pattern injects only the external dependency (the IDbContextFactory) and constructs the internal collaborators inline. Injecting a zero-dependency factory would add ceremony without any testability benefit (the factory's methods are pure and already unit-tested directly) and would diverge from the two merged extractions this PR is modeled on.

Happy to add an internal injection seam if/when these loggers move to the DI container, but as-is new PlotModelFactory() matches the current playbook. Leaving as-is.

Comment thread Daqifi.Desktop/Loggers/PlotModelFactory.cs Outdated
#592)

Applies the actionable Qodo findings on PR #608:

- Single-main-class-per-file: move the public `MinimapPlotComponents` record out
  of PlotModelFactory.cs into its own MinimapPlotComponents.cs (same rule that
  motivated splitting out LoggedSeriesLegendItem in this PR).
- Constant naming: rename the public axis-key consts to SCREAMING_SNAKE_CASE
  (ANALOG_AXIS_KEY, DIGITAL_AXIS_KEY, TIME_AXIS_KEY, MINIMAP_TIME_AXIS_KEY,
  MINIMAP_Y_AXIS_KEY) to match CLAUDE.md's stated convention and the existing
  Loggers/ consts (MINIMAP_BUCKET_COUNT, INITIAL_LOAD_POINTS, ...). Updates the
  references in DatabaseLogger and the tests.
- Null-dispatcher reliability: LoggedSeriesLegendItem.IsVisible no longer
  dereferences Application.Current.Dispatcher unconditionally. In the live app
  Application.Current is always set, so this is the same UI-thread dispatch as
  before; in a headless/unit-test host (where it is null) the work runs inline
  instead of throwing, keeping the extracted construction exercisable without a
  WPF runtime. Adds a test that toggles legend visibility headless.

Test-fixture constants (Serial/Color) keep their PascalCase names to stay
consistent with the sibling tests (SessionDataRepositoryTests,
SessionSampleWriterTests), which use the same convention.

Unit gate green: 435/435.

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

Copy link
Copy Markdown
Contributor Author

Replies to the Qodo summary findings

The two action-required (inline) findings are addressed in their threads: #2 (two public types) is fixed; #1 (PlotModelFactory not injected) I've replied to with a rationale for keeping it consistent with the two merged #592 collaborators.

For the summary-only findings:

5. Null Dispatcher dereference (🐞 Bug) — Fixed in fb4ff04. LoggedSeriesLegendItem.IsVisible now reads Application.Current?.Dispatcher and runs the invalidate/minimap-sync work inline when it's null (headless / unit-test host), instead of dereferencing a null dispatcher. In the live WPF app Application.Current is always set, so production behavior is unchanged (same UI-thread dispatch). Added a test that toggles legend visibility without a WPF runtime to cover the fallback path. (Note: this code moved verbatim from DatabaseLogger — the latent issue predates the PR — but since the PR introduces a WPF-runtime-free test seam over this construction, hardening it here is the right place.)

3. Constant casing (📘 Rule) — Partially addressed in fb4ff04. Renamed the public axis-key constants to SCREAMING_SNAKE_CASE — ANALOG_AXIS_KEY, DIGITAL_AXIS_KEY, TIME_AXIS_KEY, MINIMAP_TIME_AXIS_KEY, MINIMAP_Y_AXIS_KEY — to match CLAUDE.md's stated convention and the existing Loggers/ constants (MINIMAP_BUCKET_COUNT, MAIN_PLOT_BUCKET_COUNT, INITIAL_LOAD_POINTS, …) and the public DiskSpaceMonitor.PRE_SESSION_WARNING_BYTES. I deliberately left the test fixture constants Serial/Color in PascalCase: they're identical to the sibling tests SessionDataRepositoryTests and SessionSampleWriterTests (Serial, SerialB, BaseTick, SessionId), so renaming only mine would make the test file inconsistent with its neighbors.

4. CLAUDE.md line exceeds 120 (📘 Rule) — Disagreed. The entire "Common Tasks" section is written as long, unwrapped single-line markdown bullets — the pre-existing version of this exact bullet already exceeded 120, and the neighboring bullet (line 331) is longer still. Wrapping only this one bullet would make it inconsistent with the section's established style, and the base content can't fit under 120 without restructuring the bullet in a way no other bullet in the file uses. Left as a single line to match the surrounding prose.

@github-actions

Copy link
Copy Markdown

📊 Code Coverage Report

Summary

Summary
Generated on: 6/18/2026 - 6:15:47 PM
Coverage date: 6/18/2026 - 6:14:23 PM - 6/18/2026 - 6:15:41 PM
Parser: MultiReport (5x Cobertura)
Assemblies: 3
Classes: 133
Files: 161
Line coverage: 42% (3781 of 8993)
Covered lines: 3781
Uncovered lines: 5212
Coverable lines: 8993
Total lines: 29200
Branch coverage: 29.1% (874 of 3002)
Covered branches: 874
Total branches: 3002
Method coverage: Feature is only available for sponsors

Coverage

DAQiFi - 42%
Name Line Branch
DAQiFi 42% 29.1%
Daqifi.Desktop.App 3.7% 0%
Daqifi.Desktop.Channel.AbstractChannel 66.1% 44.7%
Daqifi.Desktop.Channel.AnalogChannel 58.7% 12.5%
Daqifi.Desktop.Channel.Channel 11.5% 0%
Daqifi.Desktop.Channel.ChannelColorManager 100% 100%
Daqifi.Desktop.Channel.DataSample 91.6%
Daqifi.Desktop.Channel.DigitalChannel 65.2% 12.5%
Daqifi.Desktop.Commands.CompositeCommand 0% 0%
Daqifi.Desktop.Commands.HostCommands 0%
Daqifi.Desktop.Commands.WeakEventHandlerManager 0% 0%
Daqifi.Desktop.Configuration.FirewallConfiguration 90.6% 66.6%
Daqifi.Desktop.Configuration.WindowsFirewallWrapper 64% 68.4%
Daqifi.Desktop.ConnectionManager 41.8% 39.2%
Daqifi.Desktop.Converters.BoolToActiveStatusConverter 0% 0%
Daqifi.Desktop.Converters.BoolToConnectionStatusConverter 0% 0%
Daqifi.Desktop.Converters.BoolToStatusColorConverter 0% 0%
Daqifi.Desktop.Converters.BrushColorMatchConverter 0% 0%
Daqifi.Desktop.Converters.ConnectionTypeToColorConverter 0% 0%
Daqifi.Desktop.Converters.ConnectionTypeToUsbConverter 0% 0%
Daqifi.Desktop.Converters.InvertedBoolToVisibilityConverter 0% 0%
Daqifi.Desktop.Converters.ListToStringConverter 0% 0%
Daqifi.Desktop.Converters.NotNullToVisibilityConverter 0% 0%
Daqifi.Desktop.Converters.OxyColorToBrushConverter 0% 0%
Daqifi.Desktop.Device.AbstractStreamingDevice 61.2% 52.1%
Daqifi.Desktop.Device.DeviceMessage 92.8%
Daqifi.Desktop.Device.Firmware.BootloaderSessionStreamingDeviceAdapter 0% 0%
Daqifi.Desktop.Device.Firmware.FirmwareUpdateCoordinator 65.6% 54.8%
Daqifi.Desktop.Device.Firmware.FirmwareUpdateServiceConfig 100%
Daqifi.Desktop.Device.Firmware.WifiPromptDelayProcessRunner 0% 0%
Daqifi.Desktop.Device.NativeMethods 100%
Daqifi.Desktop.Device.SerialDevice.SerialStreamingDevice 44.6% 33.3%
Daqifi.Desktop.Device.WiFiDevice.DaqifiStreamingDevice 44% 34.3%
Daqifi.Desktop.DialogService.DialogService 0% 0%
Daqifi.Desktop.DialogService.ServiceLocator 0% 0%
Daqifi.Desktop.DiskSpace.DiskSpaceCheckResult 100%
Daqifi.Desktop.DiskSpace.DiskSpaceEventArgs 100%
Daqifi.Desktop.DiskSpace.DiskSpaceMonitor 88.2% 86.6%
Daqifi.Desktop.DuplicateDeviceCheckResult 100%
Daqifi.Desktop.Exporter.LoggingSessionSampleSource 98.7% 77.2%
Daqifi.Desktop.Exporter.OptimizedLoggingSessionExporter 56.9% 46.1%
Daqifi.Desktop.Helpers.BooleanConverter`1 0% 0%
Daqifi.Desktop.Helpers.BooleanToInverseBoolConverter 0% 0%
Daqifi.Desktop.Helpers.BooleanToVisibilityConverter 0%
Daqifi.Desktop.Helpers.EnumDescriptionConverter 100% 100%
Daqifi.Desktop.Helpers.IntToVisibilityConverter 0% 0%
Daqifi.Desktop.Helpers.MinMaxDownsampler 100% 96.4%
Daqifi.Desktop.Helpers.MyMultiValueConverter 0%
Daqifi.Desktop.Helpers.NaturalSortHelper 100% 100%
Daqifi.Desktop.Helpers.OxyPlotDarkTheme 100%
Daqifi.Desktop.Helpers.VersionHelper 98.2% 66.2%
Daqifi.Desktop.Logger.DatabaseLogger 0% 0%
Daqifi.Desktop.Logger.DatabaseMigrator 0% 0%
Daqifi.Desktop.Logger.DeviceLegendGroup 100% 100%
Daqifi.Desktop.Logger.InitialSessionLoad 100%
Daqifi.Desktop.Logger.LoggedSeriesLegendItem 94.1% 44.4%
Daqifi.Desktop.Logger.LoggingContext 100%
Daqifi.Desktop.Logger.LoggingContextDesignTimeFactory 0%
Daqifi.Desktop.Logger.LoggingManager 0% 0%
Daqifi.Desktop.Logger.LoggingSession 36.5% 10.8%
Daqifi.Desktop.Logger.MinimapPlotComponents 100%
Daqifi.Desktop.Logger.PlotLogger 14% 38.7%
Daqifi.Desktop.Logger.PlotModelFactory 99.4% 62.5%
Daqifi.Desktop.Logger.SessionChannelInfo 100%
Daqifi.Desktop.Logger.SessionDataRepository 97.9% 91.3%
Daqifi.Desktop.Logger.SessionDeviceMetadata 80%
Daqifi.Desktop.Logger.SessionSampleWriter 96% 91.3%
Daqifi.Desktop.Logger.SummaryLogger 0% 0%
Daqifi.Desktop.Logger.TimestampGapDetector 95% 83.3%
Daqifi.Desktop.Loggers.ImportOptions 66.6%
Daqifi.Desktop.Loggers.ImportProgress 0% 0%
Daqifi.Desktop.Loggers.ImportTimestampQuality 100% 100%
Daqifi.Desktop.Loggers.SdCardImportResult 100%
Daqifi.Desktop.Loggers.SdCardSessionImporter 49.7% 51%
Daqifi.Desktop.MainWindow 0% 0%
Daqifi.Desktop.Migrations.AddSamplesSessionTimeIndex 97.8%
Daqifi.Desktop.Migrations.AddSessionDeviceMetadata 98.6%
Daqifi.Desktop.Migrations.AddSessionSampleCount 98.1%
Daqifi.Desktop.Migrations.InitialSQLiteMigration 97.4%
Daqifi.Desktop.Migrations.LoggingContextModelSnapshot 0%
Daqifi.Desktop.Models.AddProfileModel 0%
Daqifi.Desktop.Models.DaqifiSettings 83.3% 100%
Daqifi.Desktop.Models.DebugDataCollection 6.6% 0%
Daqifi.Desktop.Models.DebugDataModel 0% 0%
Daqifi.Desktop.Models.Notifications 75%
Daqifi.Desktop.Models.SdCardFile 16.6% 0%
Daqifi.Desktop.Services.NoOpMessageBoxService 0%
Daqifi.Desktop.Services.WindowsPrincipalAdminChecker 0%
Daqifi.Desktop.Services.WpfMessageBoxService 0%
Daqifi.Desktop.UpdateVersion.VersionNotification 85.7% 54.1%
Daqifi.Desktop.View.ConnectionDialog 0% 0%
Daqifi.Desktop.View.DebugWindow 0% 0%
Daqifi.Desktop.View.DeviceLogsView 0% 0%
Daqifi.Desktop.View.DuplicateDeviceDialog 0% 0%
Daqifi.Desktop.View.ErrorDialog 0% 0%
Daqifi.Desktop.View.ExportDialog 0% 0%
Daqifi.Desktop.View.FirmwareDialog 0% 0%
Daqifi.Desktop.View.Flyouts.FirmwareFlyout 0% 0%
Daqifi.Desktop.View.Flyouts.LiveGraphFlyout 0% 0%
Daqifi.Desktop.View.Flyouts.NotificationsFlyout 0% 0%
Daqifi.Desktop.View.Flyouts.SummaryFlyout 0% 0%
Daqifi.Desktop.View.MigrationStatusWindow 0% 0%
Daqifi.Desktop.View.MinimapInteractionController 0% 0%
Daqifi.Desktop.View.ProfilesPane 0% 0%
Daqifi.Desktop.View.Prototype.ChannelsPanePrototype 0% 0%
Daqifi.Desktop.View.Prototype.DevicesPanePrototype 0% 0%
Daqifi.Desktop.View.Prototype.LiveGraphPane 0% 0%
Daqifi.Desktop.View.Prototype.LoggedDataPanePrototype 0% 0%
Daqifi.Desktop.View.SuccessDialog 0% 0%
Daqifi.Desktop.ViewModels.ChannelsPaneViewModel 0% 0%
Daqifi.Desktop.ViewModels.ChannelTileViewModel 0% 0%
Daqifi.Desktop.ViewModels.ConfirmOverlayViewModel 100% 100%
Daqifi.Desktop.ViewModels.ConnectionDialogViewModel 38.9% 38.2%
Daqifi.Desktop.ViewModels.DaqifiViewModel 13.6% 7.5%
Daqifi.Desktop.ViewModels.DeviceLogsViewModel 50.4% 43.3%
Daqifi.Desktop.ViewModels.DevicesPaneViewModel 0% 0%
Daqifi.Desktop.ViewModels.DeviceTileViewModel 0% 0%
Daqifi.Desktop.ViewModels.DuplicateDeviceDialogViewModel 0%
Daqifi.Desktop.ViewModels.ErrorDialogViewModel 0%
Daqifi.Desktop.ViewModels.ExportDialogViewModel 59.4% 33.3%
Daqifi.Desktop.ViewModels.FirmwareDialogViewModel 0% 0%
Daqifi.Desktop.ViewModels.LoggingSessionListViewModel 95.4% 88.2%
Daqifi.Desktop.ViewModels.NewProfileChannelItem 0%
Daqifi.Desktop.ViewModels.NewProfileDeviceItem 0% 0%
Daqifi.Desktop.ViewModels.ProfilesPaneViewModel 0% 0%
Daqifi.Desktop.ViewModels.SettingsViewModel 0% 0%
Daqifi.Desktop.ViewModels.SuccessDialogViewModel 0%
Daqifi.Desktop.WindowViewModelMapping.IWindowViewModelMappingsContract 0%
Daqifi.Desktop.WindowViewModelMapping.WindowViewModelMappings 0%
Sentry.Generated.BuildPropertyInitializer 100%
Daqifi.Desktop.Common - 40.5%
Name Line Branch
Daqifi.Desktop.Common 40.5% 23.8%
Daqifi.Desktop.Common.AppDataPaths 84.2% 50%
Daqifi.Desktop.Common.Loggers.AppLogger 35.2% 21%
Daqifi.Desktop.Common.Loggers.NoOpLogger 0%
Daqifi.Desktop.IO - 100%
Name Line Branch
Daqifi.Desktop.IO 100% ****
Daqifi.Desktop.IO.Messages.MessageEventArgs`1 100%

Coverage report generated by ReportGeneratorView full report in build artifacts

@tylerkron tylerkron merged commit f7ccbcf into main Jun 18, 2026
6 checks passed
@tylerkron tylerkron deleted the chore/extract-plot-model-factory branch June 18, 2026 20:03
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.

2 participants