feature/QuiverPlot#70
Open
gabo515 wants to merge 13 commits into
Open
Conversation
QuiverPlot renders each arrow with a tail at (x, y) and vector (u, v).
Zero-config defaults: auto-scaled length so the longest arrow spans 85%
of the shorter tail span, proportional arrow heads (clamped 4–14 px) so
every arrow reads as an arrow regardless of magnitude.
- `from_function((xlo, xhi, nx), (ylo, yhi, ny), |x, y| (u, v))` for the
common gridded-field case; builder API still available for irregular data.
- `QuiverPivot { Tail, Middle, Tip }` for where (x, y) sits on each arrow.
- Optional magnitude-driven colormap with automatic colorbar, via
`with_magnitude_colormap(cmap, label)` or the individual setters.
- `with_tight_bounds()` opts into tail-only axis bounds; arrows are clipped
to the plot rectangle so they don't spill into axis labels.
- Interactive-mode tooltips: each arrow wraps in a `<g class="tt" data-*>`
with a native `<title>` showing x, y, u, v, |v|, θ.
Factored `render_utils::arrow_head_path` shared by QuiverPlot,
NetworkPlot directed edges, and TextAnnotation arrows.
CLI: `kuva quiver` — x/y/u/v column selectors, --arrow-scale / --auto-scale,
--pivot, --tight-bounds, --colormap, --colorbar-label, --shaft-width,
--head-length / --head-width, --legend. Docs at docs/src/plots/quiver.md
and docs/src/cli/quiver.md; example at examples/quiver.rs; smoke tests
covering basic + colormap; integration tests covering SVG output, colorbar
widening, and --arrow-scale / --auto-scale mutual exclusion.
- fix O(n²) in bounds() and add_quiver: effective_scale() iterates all arrows, and was being called per-arrow inside endpoints(). Hoist the scale once and add endpoints_with_scale() that takes the precomputed value. 1000-arrow fields now compute bounds in O(n) instead of O(n²). - factor colorbar_linear(cmap, min, max, label) helper in plots.rs; absorb the six duplicate `Arc<Fn>` closures across Heatmap, DotPlot, DicePlot, Contour, Clustermap, and Quiver arms (and route the 3-D colorbar_from_z through it too). - CLI --pivot now uses clap::ValueEnum (CliPivot -> QuiverPivot) matching the convention in bump/hexbin/treemap/sunburst, replacing the manual value_parser + unreachable!(). - split with_head(len, half_width) into with_head_length(l) / with_head_width(w) so the CLI can pass only the flag the user actually set without fabricating a 10-px default for the other dimension. - use with_arrows(iter) in the CLI instead of a zip chain of with_arrow calls. - strip narrating WHAT-comments from add_quiver / resolve_head; keep the one WHY comment about clipping with tight bounds.
Close the gaps from the PR template + recurring review patterns: - tests/quiver_basic.rs (13 tests): dedicated integration suite covering from_function count, endpoint-inclusive sampling, pivot Tail/Middle/Tip shifts, auto-scale with huge vectors, explicit with_scale, colormap + colorbar, tight_bounds clip-path emission, proportional heads on small arrows, legend entry emission, empty input, per-arrow vs plot color fallback priority. Color assertions are encoding-resilient (match hex, named, rgb()). - Fix degenerate auto-scale bug surfaced by the new tests: when all arrows are collinear on one axis, span was min(x, y) = 0 and scale fell back to 1.0. Now uses the non-zero axis when one is degenerate. - Promote effective_scale / endpoints_with_scale / resolve_head / magnitude_extent to `pub` so the integration suite can exercise them. - Non-default-config smoke tests (pivot middle, pivot tip, explicit head dims, legend) — was a common gap James had to fix post-merge. - scripts/terminal_plots.sh entry for quiver. - docs/src/cli/index.md subcommand table entry. - docs/src/gallery.md gallery card. - scripts/gen_docs.sh EXAMPLES list. - Regenerated man/kuva.1 so kuva-quiver(1) is referenced.
The previous formula sized the longest arrow to `0.85 × shorter_span`, which for an 8×8 grid produced arrows ~7× too long — the saddle-field smoke test rendered as compressed vertical strips because the huge scale factor crushed the v-component and dumped all tips near y=0. Replaced with matplotlib's convention: target the longest arrow at ~one grid cell, approximated as `span / √n` for n arrows. The default fraction is now 0.9 of the nearest-neighbor distance (was 0.85 of the full span). Verified visually: the regenerated doc SVGs and all six smoke-test outputs now render as proper vector fields (rotational field shows clear circulation; saddle field shows the expected diverging pattern).
Background gridlines compete visually with arrow shafts and add no information in quiver plots — each arrow already shows both position and direction. Pass --no-grid in smoke tests and .with_show_grid(false) in the integration test render helper.
After changing the auto-scale formula to match matplotlib (longest arrow ≈ one grid cell), four docs/help strings still described the old "85% of shorter span" formula: - docs/src/plots/quiver.md — example dropped the now-redundant .with_auto_scale(0.85) call; scaling section rewritten to describe the √n grid-cell heuristic; CLI flag table default updated to 0.9. - docs/src/cli/quiver.md — default updated to 0.9 with the grid-cell description. - src/bin/kuva/quiver.rs clap doc — same, feeds into --help and the man page. Tests: fixed the stale "fill ~85% of x span" comment in test_quiver_auto_scale_picks_sensible_length. Moved create_dir_all into the render() helper so parallel test runs can't race on the first fs::write. Downgraded QuiverPlot::resolve_head from pub back to pub(crate) — it takes a pixel argument that only makes sense inside the render pipeline, and no test now needs it publicly.
1. with_head_min_px / with_head_max_px builders — head_min_px and head_max_px were pub but had no setters, forcing users to mutate fields directly. 2. Stronger color-priority test — adds a case where both a colormap and a per-arrow color override are set; the per-arrow override must win. The previous test only covered per-arrow vs. plot-level. 3. CHANGELOG calls out the arrow_head_path and colorbar_linear refactors in their own Changed section, since they touch six other plot types besides quiver. 4. Downgrade effective_scale / endpoints_with_scale / magnitude_extent back to pub(crate) — they were promoted purely for integration tests, which was a weak reason to widen the public surface. Moved the 4 affected tests into a #[cfg(test)] mod tests block inside src/plot/quiver.rs. Added two small negative-path tests while I was there (fallback scale of 1.0 when arrows are empty or all zero). 5. Decouple clip-to-plot from tight_bounds. New clip_to_plot_area: Option<bool> field with should_clip() resolver; builders with_clip_to_plot_area() (force on) and with_no_clip() (force off). CLI gets --clip / --no-clip (mutually exclusive). Default behaviour preserved: tight_bounds still implies clipping when the user hasn't pinned the flag explicitly. 6. Strengthen empty-plot test — now asserts Plot::Quiver::bounds() returns None and that no interactive groups are emitted. Previously only asserted <svg presence. 7. Added "quiver grid on + tight bounds" smoke test so the default grid rendering is exercised with quiver at least once. Also: drop "matplotlib" references from new quiver docs / comments / CHANGELOG — describe the behaviour directly without attributing it.
# Conflicts: # CHANGELOG.md # scripts/smoke_tests.sh # src/render/render.rs
10 new tests across 4 files addressing coverage thin spots: tests/quiver_basic.rs (5 new): - interactive-mode emits one <g class="tt" data-x/y/u/v/mag> per arrow, relaxed to [24, 25] to tolerate the zero-magnitude arrow at the grid center being skipped. - with_clip_to_plot_area() alone (no tight_bounds) still emits the clip-path. - with_no_clip() + with_tight_bounds() suppresses the clip-path. - with_color_range pins the colorbar axis extent (user-set max appears on the colorbar tick axis, not the data extent). - with_head_length / with_head_width overrides change the rendered output vs. the proportional default. src/plot/quiver.rs (2 new internal tests): - auto_scale_matches_grid_cell_formula pins the exact fraction × span / (√n × max_mag) value so a regression in the formula is caught, not just "scale < 0.1". - auto_scale_honors_custom_fraction — passing with_auto_scale(0.45) halves the scale vs. the 0.9 default. tests/heatmap.rs (1 new): - test_heatmap_colorbar_regression pins Viridis gradient endpoints (#440154 / #fde725) so silent drift in the refactored colorbar_linear helper would be caught. tests/network_basic.rs (1 new): - test_network_directed_arrowhead_format_regression verifies the refactored arrow_head_path helper still emits the M/L/L/Z triangle shape for every directed edge. tests/cli_basic.rs (4 new): - --pivot middle produces different SVG than the tail default. - --tight-bounds emits a clip-path. - --no-clip + --tight-bounds suppresses the clip-path. - --head-length / --head-width are accepted and change the output.
Blocking: - All QuiverPlot data-entry builders now accept impl Into<f64>, matching the convention freshly merged from PR Psy-Fer#68 (BarPlot / ForestPlot / WaterfallPlot / ...). Updated: with_arrow, with_arrows, with_colored_arrow, with_scale, with_auto_scale, with_shaft_width, with_head, with_head_length, with_head_width, with_head_ratio, with_head_min_px, with_head_max_px, with_color_range. Callers can now pass i32/u32/f32 etc. without `as f64`. Doc fix: - with_auto_scale doc-comment updated — was still describing the pre-√n "fraction of the shorter tail-span" formula with the wrong default (0.85 → 0.9). The field doc on auto_scale_fraction was already correct; this was the only remaining stale reference. Robustness: - with_arrow / with_arrows / with_colored_arrow now silently drop non-finite (NaN / ±inf) inputs instead of letting them propagate through bounds() / effective_scale() / render. Documented in the builder doc-comments. Tests: - test_quiver_with_arrow_accepts_integer_types — covers the i32 / u32 / f32 paths through impl Into<f64>. - test_quiver_drops_non_finite_arrows — verifies NaN / ±inf rows are skipped at push time. - test_quiver_with_arrows_drops_non_finite — same for the iterator variant. - test_quiver_with_legend_emits_entry strengthened — now also asserts the legend Line glyph adds at least one <line> element vs. the no-legend baseline. - test_quiver_cli_colorbar_label and test_quiver_cli_legend — close the last two CLI flag-coverage gaps flagged in review.
1. NaN guards on numeric config setters — with_scale, with_auto_scale, with_shaft_width, with_head, with_head_length, with_head_width, with_head_ratio, with_head_min_px, with_head_max_px, with_color_range now debug_assert that their input is_finite (and shaft_width is non-negative). Release builds still accept NaN/inf silently and produce empty plots — the docs on with_scale spell out that degeneration explicitly. 2. Tighter legend-glyph test — uses a non-default plot color (crimson) so the legend's <line> glyph can be unambiguously identified by stroke. Resilient to encoding (matches "crimson", "#dc143c", or "rgb(220,20,60)") per the kuva PR-review-pattern guidance. 3. Doc note on with_arrows — clarifies that all four tuple positions must use the same numeric type within the iterator (limitation of the generic shape; matches BarPlot::with_bars and friends).
Contributor
Author
|
I will deal with the merge conflicts once the new release happens :) |
Owner
|
haha yea good idea :) not long now |
# Conflicts: # CHANGELOG.md # docs/src/SUMMARY.md # docs/src/gallery.md # scripts/gen_docs.sh # src/bin/kuva/main.rs # src/plot/mod.rs # src/prelude.rs # src/render/plots.rs # src/render/render.rs # src/render/render_utils.rs # tests/heatmap.rs
Upstream ran cargo fmt across the project on 2026-05-04 (commit 2f7b743 "cargo fmt fixes"). The quiver-PR code I wrote between then and now wasn't fmt-clean, so this aligns it with upstream's style ahead of the next round of review. 10 files reformatted (~430 lines): examples/quiver.rs, src/bin/kuva/quiver.rs, src/plot/quiver.rs, src/render/annotations.rs, src/render/plots.rs, src/render/render.rs, tests/cli_basic.rs, tests/heatmap.rs, tests/network_basic.rs, tests/quiver_basic.rs. No behavioral changes. Tests still pass (70 + 15 + 22 + 18 across the quiver / heatmap / network / cli_basic suites); clippy clean.
Contributor
Author
|
I think it is ready |
Owner
|
Yea looks good mate. Just been sorting out some raster/backend upgrades before going back into more plot additions. Current sprint is around that and some cli fixes/additions. Then back to regular plot expansion/features. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Adds
QuiverPlot— a 2-D vector-field renderer. Every arrow has a tail at(x, y)and a vector(u, v)in data coordinates; the renderer maps these through the axis transform, shrinks the shaft to butt against a triangular head, and wires the whole thing into the standard kuva layout / legend / colorbar pipeline.Highlights:
QuiverPlot::from_function((xlo, xhi, nx), (ylo, yhi, ny), |x, y| (u, v))samples a closure on a regular grid; the longest arrow is auto-scaled to roughly one grid cell (0.9 × span / √n), so a naïve call produces a sensible plot regardless of the units of(u, v).0.28 × shaft, clamped to[4, 14]pixels), so tiny arrows still have a visible head and long arrows don't grow gigantic ones. Explicit pixel overrides viawith_head(len, half_w)/with_head_length/with_head_width.QuiverPivot::Tail(default, arrow starts at(x, y)),Middle(centered on(x, y)),Tip(arrow points into(x, y)).with_magnitude_colormap(cmap, label). Priority for per-arrow color: per-arrow override > colormap > plot-level color.with_tight_bounds()uses tails-only bounds;with_clip_to_plot_area()/with_no_clip()control clipping independently. Tight bounds implies clipping when the clip flag isn't pinned.<g class="tt" data-x data-y data-u data-v data-mag>with a native<title>showingx, y, u, v, |v|, θ— so the embedded JS picks them up in--interactivemode.Refactors (touch existing plots)
render_utils::arrow_head_path— shared arrow-head triangle helper. Replaces three inline copies:NetworkPlotdirected edges,TextAnnotationarrows, and (new)QuiverPlot.colorbar_linear(cmap, min, max, label)inrender/plots.rs— consolidates six near-identicalArc<Fn>closures that built linearly-normalized colorbars acrossHeatmap,DotPlot,DicePlot,Contour,Clustermap, andQuiver. The 3-Dcolorbar_from_zpath also routes through it now.Type of change
Checklist
Library (new plot type)
src/plot/quiver.rs—QuiverPlot+QuiverArrow+QuiverPivot; builder methods (with_arrow,with_arrows,with_colored_arrow,with_color,with_scale,with_auto_scale,with_shaft_width,with_head,with_head_length,with_head_width,with_head_ratio,with_head_min_px,with_head_max_px,with_color_map,with_color_range,with_color_legend_label,with_magnitude_colormap,with_legend,with_tight_bounds,with_clip_to_plot_area,with_no_clip,with_pivot);from_functionconstructor;#[cfg(test)] mod testsblock for internal-accessor testssrc/plot/mod.rs—pub mod quiver+ re-exportsQuiverPlot,QuiverArrow,QuiverPivotsrc/render/plots.rs—Plot::Quivervariant +bounds()(tip-inclusive vs tight) +colorbar_info()(via newcolorbar_linear) +set_color()+estimated_primitives()+From<QuiverPlot>src/render/render.rs—add_quiver+render_quiver; wired intorender_multipledispatch and the palette auto-cycle group; not pixel-space so noskip_axessrc/render/layout.rs—has_colorbarcheck added (magnitude colormap triggers colorbar margin) +has_legendcheck (legend label triggers legend margin); no category axis extensions neededsrc/prelude.rs—QuiverPlot,QuiverArrow,QuiverPivotre-exportedTests
tests/quiver_basic.rs— 10 integration tests through the public API: basic render + shape counts,from_functionexact-count + endpoint-inclusive sampling, colormap triggers colorbar, tight-bounds emits clip-path, proportional heads on small arrows, legend entry emission, empty-plot edge case (bounds None, no interactive groups), per-arrow color override vs plot-level, per-arrow override beats colormap. Encoding-resilient color assertions (hex / named /rgb(...)).src/plot/quiver.rs#[cfg(test)] mod tests— 6 unit tests for internal accessors: pivot Tail/Middle shift, pivot Tip placement, auto-scale shrinks huge vectors,with_scalepins exact value, empty-plot fallback scale 1.0, all-zero-magnitude fallback scale 1.0.tests/cli_basic.rs— 3 CLI tests: SVG output, colorbar widens canvas,--arrow-scale/--auto-scalemutual-exclusion error path.cargo test --features cli,full— 1362 tests pass, 0 fail (includes upstreamimpl Into<f64>unification tests merged in).CLI
src/bin/kuva/quiver.rs—QuiverArgswith clap doc-comments +CliPivotValueEnummapped toQuiverPivot+run(). Useswith_arrows(iter)(not zip-chain ofwith_arrowcalls).src/bin/kuva/main.rs—mod quiver,Commands::Quivervariant, match arm.scripts/smoke_tests.sh— 7 quiver invocations covering basic, colormap + colorbar, pivot middle, pivot tip, explicit head dimensions, legend, and grid-on + tight bounds.scripts/terminal_plots.sh— quiver entry added (visible in the terminal-rendering matrix).tests/cli_basic.rs— SVG output + content-verification tests (above).docs/src/cli/index.md— subcommand table entry.man/kuva.1— regenerated;kuva-quiver(1)referenced.Documentation
examples/quiver.rs— generatesdocs/src/assets/quiver/{basic,colormap,source}.svg.examples/data/quiver.tsv— saddle-field sample data used by smoke tests.scripts/gen_docs.sh—quiveradded to theEXAMPLESarray.docs/src/plots/quiver.md— full page: basic usage, scaling, pivot, coloring, styling, CLI flags reference table, embedded basic SVG.docs/src/cli/quiver.md— CLI reference page with flag table and examples.docs/src/SUMMARY.md— plot + CLI links added.docs/src/gallery.md— gallery card added.README.md— plot types table updated N/A — README has no plot-types table; only quick-start / docs / contributors sections.Visual inspection
tight_boundsclipping, colormap output, and legend rendering.test_outputs/for layout regressions — The two refactors (arrow_head_path,colorbar_linear) do change behavior paths for 6 existing plot types; the test suite covers them but visual inspection was donebash scripts/smoke_tests.sh— 183/183 outputs pass.Housekeeping
CHANGELOG.md—[Unreleased]→Added(QuiverPlot) andChanged(arrow_head_path + colorbar_linear refactors).README.md