Skip to content

dev->main merge for release v0.2.0#76

Merged
Psy-Fer merged 125 commits into
mainfrom
dev
May 7, 2026
Merged

dev->main merge for release v0.2.0#76
Psy-Fer merged 125 commits into
mainfrom
dev

Conversation

@Psy-Fer
Copy link
Copy Markdown
Owner

@Psy-Fer Psy-Fer commented May 7, 2026

Release merge for v0.2.0

maflot and others added 30 commits March 30, 2026 22:37
Adds a new plot type for visualising multivariate categorical and
continuous data on a grid, where each cell shows up to 6 dots arranged
in a canonical die-face layout. Ports the rendering logic from the
ggdiceplot R package (v1.2.0), including column-major grid positions,
tight-packing pip radius calculation, and pip_scale offset shrinkage.

Three input modes:
- Categorical (with_records): per-dot CSS colour, absent dots omitted
- Continuous (with_points): colourmap tile background, hollow absent dots
- Per-dot continuous (with_dot_data): per-dot colourmap + radius scaling

Legend support: spatial-position legend (mini die faces), categorical
colour legend, and size legend sections, stacked vertically.
- docs/src/plots/diceplot.md: full documentation page with usage examples
  for categorical, per-dot continuous, and ZEBRA domino modes
- docs/src/assets/diceplot/: three SVG examples
- docs/src/SUMMARY.md: added Dice Plot entry after Dot Plot
- docs/src/gallery.md: gallery card with mirna_compound preview
- CHANGELOG.md: entry under [Unreleased]
11 tests covering all three rendering modes (categorical, continuous tile,
per-dot continuous), legend variants (position, colour, size, colorbar,
stacked), edge cases (empty data, absent dots omitted), and all ndots
variants (1-6).
Adds **DicePlot**, a new plot type based on the dice-face grid design
from the R package
[ggdiceplot](https://github.com/matthias-da/ggdiceplot) v1.2.0. It is
intended for compact visualization of multivariate categorical and
continuous data, with each grid cell showing up to 6 dots in a canonical
die-face layout.

The implementation supports three input modes:
- **Categorical** (`with_records`): per-dot CSS colour, absent dots
omitted
- **Continuous** (`with_points`): colourmap tile background, hollow
absent dots
- **Per-dot continuous** (`with_dot_data`): per-dot colourmap +
proportional radius (ZEBRA/domino style)

I’m happy to adapt the implementation if there is a preferred approach
for the legend or rendering integration.

**Note on `render.rs` size:** the diff is relatively large (+505 lines)
because DicePlot requires a custom stacked legend (position + colour +
size sections) that does not fit the existing generic legend path. I
kept this in `render.rs` rather than introducing a separate file only
for this logic, but I’m happy to restructure it if that would fit the
codebase better.

**Note on example data:** the examples and tests use fairly verbose
inline data. These datasets are only used for tests and doc asset
generation.

## Type of change

- [x] New plot type
- [ ] New feature / API addition
- [ ] Bug fix
- [ ] Documentation / assets only
- [ ] Refactor / housekeeping

---

## Checklist

### Library (new plot type)
- [x] `src/plot/<name>.rs` — struct + builder methods
- [x] `src/plot/mod.rs` — `pub mod` + re-export
- [x] `src/render/plots.rs` — `Plot` enum variant + `bounds()` /
`colorbar_info()` / `set_color()`
- [x] `src/render/render.rs` — `render_<name>()`, added to
`render_multiple()` match, `skip_axes` if pixel-space
- [x] `src/render/layout.rs` — `auto_from_plots()` extended if
categories needed

### Tests
- [x] New test file in `tests/` with ≥ basic render + SVG content +
legend tests
- [x] `cargo test --features cli,full` — all existing tests still pass

### CLI (if applicable)
- [ ] `src/bin/kuva/<name>.rs` — Args struct (with `/// doc comment`) +
`run()` — N/A, input too structured for CLI
- [ ] `src/bin/kuva/main.rs` — module, Commands variant, match arm — N/A
- [ ] `scripts/smoke_tests.sh` — at least one invocation — N/A
- [ ] `tests/cli_basic.rs` — SVG output test + content verification test
— N/A
- [ ] `docs/src/cli/index.md` — subcommand entry — N/A
- [ ] `man/kuva.1` — regenerated (`./target/debug/kuva man >
man/kuva.1`) — N/A

### Documentation
- [x] `examples/<name>.rs` — Rust example for doc asset generation
- [x] `scripts/gen_docs.sh` — invocations added; `bash
scripts/gen_docs.sh` runs clean
- [x] `docs/src/plots/<name>.md` — documentation page with embedded SVGs
- [x] `docs/src/SUMMARY.md` — link added
- [x] `docs/src/gallery.md` — gallery card added
- [ ] `README.md` — plot types table updated

### Visual inspection
- [x] Opened `test_outputs/` — new plot SVGs look correct
- [x] Scanned neighbouring plots in `test_outputs/` for layout
regressions
- [x] `bash scripts/smoke_tests.sh` — all existing smoke test outputs
still look correct
- [x] No text clipped, no legend overlap, no spurious axes on
pixel-space plots

### Housekeeping
- [x] `CHANGELOG.md` — entry added under `## [Unreleased]`
- [ ] `README.md` — item marked done in TODO section if applicable
- Rotated text (e.g. Z-axis ticks) was hardcoded to column 0; now uses
  actual x position via to_cx().
- Fill paths with a stroke were skipping the fill; render fill first,
  then stroke.
- Relax ylabel test assertion to check first 5 columns instead of
  column 0 only.
Introduces Scatter3DPlot and Surface3DPlot — the first 3D plot types in
kuva. Built on a shared orthographic projection module and open-box
wireframe renderer.

New modules:
- projection.rs: 3D→2D orthographic projection with configurable
  azimuth/elevation, front-corner detection, depth sorting
- plot3d.rs: shared Box3DConfig and DataRanges3D types
- scatter3d.rs: depth-sorted point rendering with z-colormap,
  per-point colors/sizes, depth shading, marker shapes
- surface3d.rs: quad mesh with painter's algorithm, z-colormap,
  wireframe overlay, NaN-safe grid handling

Also includes:
- CLI subcommands (scatter3d, surface3d) with --resolution upsampling
- Consolidate parse_colormap into shared data.rs (was duplicated 6x)
- ColorMap::map_rgb() fast path avoiding String allocation
- Manual Debug impl for ColorMap
- Examples, docs, tests, smoke tests, changelog, man page
## Description

Adds **Scatter3DPlot** and **Surface3DPlot** — the first 3D plot types
in kuva. Introduces a reusable orthographic projection module
(`projection.rs`), shared 3D infrastructure (open-box wireframe, grid,
rotated tick/axis labels), and two complete plot types with CLI
subcommands, docs, examples, and tests.

  Also includes:
- Consolidation of `parse_colormap` into shared `data.rs` (was
duplicated 6×)
- `ColorMap::map_rgb()` fast path to avoid String allocation per
face/point
  - Manual `Debug` impl for `ColorMap`
- Fix for terminal backend: rotated text position + fill paths with
stroke (pre-existing bugs surfaced by 3D rendering)

  ## Type of change

  - [x] New plot type
  - [x] New feature / API addition
  - [x] Bug fix
  - [ ] Documentation / assets only
  - [x] Refactor / housekeeping

  ---

  ## Checklist

  ### Library (new plot type)
- [x] `src/plot/<name>.rs` — struct + builder methods (`scatter3d.rs`,
`surface3d.rs`, `plot3d.rs`)
  - [x] `src/plot/mod.rs` — `pub mod` + re-export
- [x] `src/render/plots.rs` — `Plot` enum variant + `bounds()` /
`colorbar_info()` / `set_color()`
- [x] `src/render/render.rs` — `render_<name>()`, added to
`render_multiple()` match, `skip_axes` if pixel-space
- [x] `src/render/layout.rs` — `auto_from_plots()` extended if
categories needed

  ### Tests
- [x] New test file in `tests/` with ≥ basic render + SVG content +
legend tests (`scatter3d_basic.rs`: 10 tests, `surface3d_basic.rs`: 10
tests)
  - [x] `cargo test --features cli,full` — all existing tests still pass

  ### CLI (if applicable)
- [x] `src/bin/kuva/<name>.rs` — Args struct (with `/// doc comment`) +
`run()`
  - [x] `src/bin/kuva/main.rs` — module, Commands variant, match arm
  - [x] `scripts/smoke_tests.sh` — at least one invocation
- [x] `tests/cli_basic.rs` — SVG output test + content verification test
(5 CLI tests)
  - [x] `docs/src/cli/index.md` — subcommand entry
  - [x] `man/kuva.1` — regenerated

  ### Documentation
- [x] `examples/<name>.rs` — Rust example for doc asset generation
(`scatter3d.rs`, `surface3d.rs`)
- [x] `scripts/gen_docs.sh` — invocations added; `bash
scripts/gen_docs.sh` runs clean
- [x] `docs/src/plots/<name>.md` — documentation page with embedded SVGs
  - [x] `docs/src/SUMMARY.md` — link added
  - [x] `docs/src/gallery.md` — gallery card added
  - [x] `README.md` — plot types table updated

  ### Visual inspection
  - [x] Opened `test_outputs/` — new plot SVGs look correct
- [x] Scanned neighbouring plots in `test_outputs/` for layout
regressions
- [x] `bash scripts/smoke_tests.sh` — all existing smoke test outputs
still look correct
- [x] No text clipped, no legend overlap, no spurious axes on
pixel-space plots

  ### Housekeeping
  - [x] `CHANGELOG.md` — entry added under `## [Unreleased]`
  - [x] `README.md` — item marked done in TODO section if applicable

Terminal rendering examples:
### Scatter3DPlot

<img width="633" height="539" alt="image"
src="https://github.com/user-attachments/assets/27718821-9348-4818-b014-93eae41349fe"
/>

### Surface3DPlot
<img width="705" height="550" alt="image"
src="https://github.com/user-attachments/assets/7dd36671-13d8-49de-8472-a26420a67553"
/>
Psy-Fer and others added 28 commits April 28, 2026 16:10
The embedded TTF is now stored as a gzip stream and inflated on first
use, cached for the process lifetime via OnceLock. Saves ~400 KB in the
published crate (757 KB raw -> 358 KB compressed) at the cost of a
one-time ~5-10 ms inflate when any backend first loads the font.

Uses flate2 rather than calling miniz_oxide directly so that:
  - the on-disk asset is round-trippable with standard tools
    (gunzip DejaVuSans.ttf.gz works), and
  - we don't own a hand-rolled gzip header parser.

flate2 adds no new transitive crates under the png/pdf features (it's
already pulled in by tiny-skia/usvg); under default features it adds
flate2 + crc32fast, ~30 KB compiled.

The raw .ttf is kept in the repo for regenerating the .gz asset but is
excluded from the published crate.
## Description

The embedded TTF is now stored as a gzip stream and inflated on first
use, cached for the process lifetime via OnceLock. Saves ~400 KB in the
published crate (757 KB raw -> 358 KB compressed) at the cost of a
one-time ~5-10 ms inflate when any backend first loads the font.

Uses flate2 rather than calling miniz_oxide directly so that:
- the on-disk asset is round-trippable with standard tools (gunzip
DejaVuSans.ttf.gz works), and
  - we don't own a hand-rolled gzip header parser.

flate2 adds no new transitive crates under the png/pdf features (it's
already pulled in by tiny-skia/usvg); under default features it adds
flate2 + crc32fast, ~30 KB compiled.

The raw .ttf is kept in the repo for regenerating the .gz asset but is
excluded from the published crate.

@Psy-Fer: I won't be offended in the least if you decide to reject this
PR, hold it until after your next release, or decide to re-work it
yourself. I made several decisions that could easily be flipped:

1. Kept the original TTF in the repo in addition to the gzipped one. No
harm in doing this, but also not a lot of value since `gunzip
DejaVuSans.ttf.gz` regenerates it.
2. Used gzip-framing on the file to make it easy to uncompress on the
command line should anyone want to
3. But miniz_oxide supports DEFLATE compression, but not with gzip
framing ... so I pulled in flate2 (with it's default miniz_oxide
backend). It in turns, I believe, only pulls in miniz_oxide and a CRC
calculation library that have no further transitive dependencies (also,
these are, I think, all pulled in when PNG or PDF features are on)
4. Did _not_ feature gate it. Sine the font byte including isn't feature
gated, this felt a bit weird to feature gate and support both
compressed/uncompressed fonts embedding but not "no font embedding". The
upside is it's simpler to maintain, and the binary is always smaller;
the downside is now 3 extra crates always on the compile pathway.

On the plus side, the gzipped (using libdeflate-gzip -12) font goes from
757,076 bytes down to 357,932. Even after adding back 30-50kb of
flate/crc/miniz object code, we're will shaving around 360kb off the
total size.

## Type of change

- [ ] New plot type
- [ ] New feature / API addition
- [ ] Bug fix
- [ ] Documentation / assets only
- [x] Refactor / housekeeping

---

## Checklist

### Housekeeping
- [ ] `CHANGELOG.md` — entry added under `## [Unreleased]`
- [ ] `README.md` — item marked done in TODO section if applicable
@Psy-Fer Psy-Fer self-assigned this May 7, 2026
@Psy-Fer Psy-Fer merged commit 300305e into main May 7, 2026
6 checks passed
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.

5 participants