Release v2.9.0: chart UX fixes, grid toggle, MLG timestamp fix#68
Release v2.9.0: chart UX fixes, grid toggle, MLG timestamp fix#68SomethingNew71 merged 8 commits intomainfrom
Conversation
Pinch and cmd+wheel zoom were silently no-ops while cursor_tracking was on (the default). The cursor-tracking branch in the chart closure forces bounds to a window of view_window_seconds centered on the cursor every frame, overwriting any zoom egui_plot just applied. Translate zoom_delta (and smooth_scroll_delta.y when scroll_to_zoom is enabled) into changes of view_window_seconds itself, so the window shrinks/grows around the cursor as the user expects.
Two MLG-related fixes bundled together: 1. Parser: MLG raw u16 timestamps are 10 µs/tick per the EFI Analytics MLG Binary Log Format spec, not 1 ms/tick. The previous formula compounded a 10× error in both the raw remainder and the wrap count for a net 100× over-estimate of wall-clock time (a ~30 min log showed as ~48 hours). Fixed via named constants MLG_TICK_SECONDS = 1e-5 and MLG_WRAP_TICKS = 65536, plus a regression test covering rusefi/speeduino sample logs. 2. UI: chart downsampling now slices raw data to the visible viewport before LTTB so detail scales with zoom. Previously LTTB compressed the full log to MAX_CHART_POINTS regardless of zoom, leaving fine detail invisible. Viewport bounds are remembered per plot area between frames; Y normalization uses the channel's global min/max so heights stay stable across pans.
LTTB partitions a slice into N equal buckets *by index* and picks the "best" point per bucket. During cursor-tracked playback the slice itself shifts a few samples each frame, so each bucket gets a slightly different sample set and the chosen peak flickers — visibly so when zoomed far out, where peaks are flattened to single buckets. Replace LTTB in compute_viewport_points with min/max-per-bucket, where bucket boundaries are anchored to absolute time (k × bucket_size from t = 0) instead of the viewport. As the cursor advances, samples slide through a fixed grid and each bucket's contents are invariant to the viewport offset, so peaks stay put. Two points per bucket (min, max) preserve the envelope; the bucket count is halved so total output stays under MAX_CHART_POINTS.
Add a Show Grid checkbox in the View menu and a Show Grid / Opacity slider in Settings → Display. Both knobs persist in UserSettings so the choice survives across sessions. Wire egui_plot's show_grid([x, y]) and grid_color() into both Plot::new chains (single and multi-area). The opacity slider overrides only the alpha of the base grid color, leaving the distance-based fade and theme RGB intact. Adds en/ru translations for menu.show_grid, settings.show_grid, settings.show_grid_desc, settings.grid_opacity.
Pinch / cmd+wheel / scroll-to-zoom in cursor-tracking mode used to write straight into view_window_seconds — the same field bound to the Window slider in Settings. Scrolling the chart visibly dragged the slider, and the zoomed value got persisted as the new default. Split the field in two: - `view_window_seconds` stays the user-set persistent default (Slider in Settings / Sidebar). The slider's write-path also resets the live render width. - `current_view_window` is the live render width used by the chart in cursor-tracking mode. Zoom interactions update only this field, leaving the slider and persisted value alone. Also gate the zoom helper on `ui.rect_contains_pointer(ui.max_rect())` so wheel/pinch events that land over the side panel (Settings, etc.) no longer reach the chart at all.
The IPC server set the listener to non-blocking and slept 100 ms between WouldBlock errors. There is no shutdown signal feeding the loop, so the non-blocking pattern only added up to ~100 ms of latency before each accepted connection plus continuous busy-poll wakeups. Switching to blocking accept removes the latency and the wakeups, and restores deterministic ordering for the in-tree IPC integration tests that previously flaked under cargo test parallelism (the listener could be mid-sleep when a connection arrived, pushing command delivery past the test's 2 s polling window).
Flatten the nested grid_opacity persistence guards in the settings panel to satisfy clippy::collapsible_if (edition 2021, no let_chains). Cover the new LoadedFile::get_channel_column lazy column-major accessor with unit tests for hot, out-of-bounds, idempotent, and empty-log paths.
There was a problem hiding this comment.
Code Review
This pull request updates UltraLog to version 2.9.0, adding a customizable chart grid and viewport-aware downsampling for better visual detail. It also fixes Speeduino MLG timestamp scaling and introduces lazy data transposition for performance. Feedback suggests moving viewport state to the tab level to prevent flickering, implementing cache invalidation for computed channels, and transposing data per-channel to further reduce memory overhead.
| /// the visible viewport before LTTB-downsampling, so chart detail scales | ||
| /// with zoom level instead of being fixed at MAX_CHART_POINTS over the | ||
| /// full log range. Keyed by plot_area_id (0 in single-plot mode). | ||
| pub(crate) chart_last_x_bounds: HashMap<usize, (f64, f64)>, |
There was a problem hiding this comment.
Storing viewport bounds globally in the UltraLogApp state keyed by plot_area_id causes a 1-frame flicker when switching between tabs with different time ranges. The new tab will initially attempt to downsample its data using the previous tab's viewport bounds before the chart interaction logic can update them. Moving this state into the Tab struct would ensure each log maintains its own independent viewport memory and eliminate this artifact.
| }; | ||
|
|
||
| // Cache the result | ||
| self.minmax_cache.insert(cache_key, (min_val, max_val)); |
There was a problem hiding this comment.
The minmax_cache is now used for computed channels via get_channel_data_ref, but there is no mechanism to invalidate these entries when a computed channel's formula is edited or when channels are removed (which shifts indices). This can lead to incorrect vertical scaling on the chart. Consider clearing the cache for a specific file whenever its computed channel list is modified.
| /// access so the chart hot path can borrow `&[f64]` for a channel instead | ||
| /// of re-collecting an owned `Vec<f64>` from the row-major store on every | ||
| /// frame. | ||
| channel_columns: OnceLock<Vec<Vec<f64>>>, |
There was a problem hiding this comment.
The current OnceLock<Vec<Vec<f64>>> type forces a full transposition of all log channels into column-major format as soon as any single channel is accessed. For logs with hundreds of channels, this causes a significant lag spike and doubles the memory usage for data that may never be viewed. Changing this to a Vec<OnceLock<Vec<f64>>> would allow for per-channel lazy initialization, significantly reducing the performance and memory impact.
channel_columns: Vec<OnceLock<Vec<f64>>>,| log, | ||
| channels_with_data, | ||
| channel_columns: OnceLock::new(), | ||
| } |
| pub fn get_channel_column(&self, channel_index: usize) -> Option<&[f64]> { | ||
| let cols = self.channel_columns.get_or_init(|| { | ||
| (0..self.log.channels.len()) | ||
| .map(|i| self.log.get_channel_data(i)) | ||
| .collect() | ||
| }); | ||
| cols.get(channel_index).map(Vec::as_slice) | ||
| } |
Summary
Release v2.9.0 promotes the dev branch to main. Includes the chart UX overhaul from #67, an IPC fix, a clippy/test follow-up, and the version bump.
Changes
Bug fixes
view_window_seconds(slider-bound, persisted) andcurrent_view_window(transient). Stops scroll inside the Settings panel from rewriting the saved tracking window.New feature
drag_stopped/lost_focusto avoid thrashing settings.json.Review follow-up
collapsible_ifviolation in the grid-opacity persistence guard (edition 2021, no let_chains) and added 4 unit tests for the newLoadedFile::get_channel_columnlazy column accessor.Test plan
cargo fmt --all -- --checkpassescargo clippy -- -D warningspassescargo test— 893 tests pass (4 new in tests/core/state_tests.rs, 3 new MLG regression tests)