# Hexbin Plot
A hexbin plot bins scatter points `(x, y)` into a regular hexagonal grid and colors each cell by its point count — or by an aggregated third variable `z`. Hexagonal bins tile the plane without gaps and are equidistant from all six neighbors, giving a more visually uniform density estimate than rectangular bins. A colorbar labeled **"Count"** (or the chosen aggregation) is added to the right margin automatically.
**Import path:** `kuva::plot::hexbin::HexbinPlot`, `kuva::plot::hexbin::ZReduce`, `kuva::plot::ColorMap`
---
## Basic usage
Pass `(x, y)` scatter points to `.with_data()`. The plot divides the pixel canvas into hexagonal bins, counts the points in each, and applies the Viridis colormap.
```rust,no_run
use kuva::plot::hexbin::HexbinPlot;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;
let x: Vec<f64> = /* your data */ vec![];
let y: Vec<f64> = /* your data */ vec![];
let plot = HexbinPlot::new().with_data(x, y);
let plots = vec![Plot::Hexbin(plot)];
let layout = Layout::auto_from_plots(&plots)
.with_title("Hexbin Density")
.with_x_label("X")
.with_y_label("Y");
let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
std::fs::write("hexbin.svg", svg).unwrap();
```
<img src="../assets/hexbin/basic.svg" alt="Basic hexbin density plot — three clusters, Viridis colormap" width="560">
Three Gaussian clusters binned at the default resolution (20 bins across). The Viridis colorbar on the right maps bin counts from dark purple (sparse) to yellow (dense).
---
## Bin resolution
`.with_n_bins(n)` sets the number of hex columns across the x-axis. More bins reveal finer density structure at the cost of noisier estimates in sparse regions.
```rust,no_run
# use kuva::plot::hexbin::HexbinPlot;
# use kuva::render::plots::Plot;
// Coarse — large hexes, smooth shape
let plot = HexbinPlot::new().with_data(x.clone(), y.clone()).with_n_bins(10);
// Fine — small hexes, more detail
let plot = HexbinPlot::new().with_data(x, y).with_n_bins(40);
```
<img src="../assets/hexbin/bins_coarse.svg" alt="Hexbin — 10 bins, coarse resolution" width="560">
10 bins — the cluster shapes are obvious but internal density structure is lost.
<img src="../assets/hexbin/bins_fine.svg" alt="Hexbin — 40 bins, fine resolution" width="560">
40 bins — peaks within each cluster become visible; individual bins in the periphery are noisier.
---
## Log color scale
When a few bins dominate the count (dense core, sparse halo), the linear color scale saturates the colormap at the peak and hides structure elsewhere. `.with_log_color(true)` applies log₁₀(count + 1) before color mapping so both dense and sparse regions remain readable.
```rust,no_run
# use kuva::plot::hexbin::HexbinPlot;
# use kuva::render::plots::Plot;
let plot = HexbinPlot::new()
.with_data(x, y)
.with_log_color(true);
```
<img src="../assets/hexbin/log_color.svg" alt="Hexbin — log color scale" width="560">
The colorbar tick marks show actual count values (1, 10, 100, …) while the color scale compresses high counts to reveal the low-density fringe.
---
## Third variable — Z aggregation
`.with_z(z, reduce)` replaces count-based coloring with an aggregated third variable. Each bin collects the `z` values of the points it contains and applies the chosen `ZReduce` function. The colorbar label updates automatically.
```rust,no_run
use kuva::plot::hexbin::{HexbinPlot, ZReduce};
# use kuva::render::plots::Plot;
// Color bins by the mean of a per-point measurement
let plot = HexbinPlot::new()
.with_data(x, y)
.with_z(z, ZReduce::Mean);
```
<img src="../assets/hexbin/z_mean.svg" alt="Hexbin — Z aggregation by mean" width="560">
Bins colored by the mean of z = x + y. The gradient follows the diagonal, reflecting the additive structure of the z variable.
| `Count` *(default)* | Count | Number of points in the bin |
| `Mean` | Mean | Arithmetic mean of z |
| `Sum` | Sum | Sum of z values |
| `Median` | Median | Median of z values |
| `Min` | Min | Minimum z value |
| `Max` | Max | Maximum z value |
When `z` values are absent, all `ZReduce` variants fall back to point count.
---
## Normalized density
`.with_normalize(true)` divides each bin's count by the total number of input points, expressing density as a fraction in [0, 1]. The colorbar label changes to **"Density"**.
```rust,no_run
# use kuva::plot::hexbin::HexbinPlot;
# use kuva::render::plots::Plot;
let plot = HexbinPlot::new()
.with_data(x, y)
.with_normalize(true)
.with_colorbar_label("Density"); // optional: keep the default or override
```
<img src="../assets/hexbin/normalize.svg" alt="Hexbin — normalized fractional density" width="560">
Normalized density makes plots with different sample sizes comparable on the same scale.
---
## Min count filter
`.with_min_count(n)` suppresses bins that contain fewer than `n` points. This trims noise from the periphery of a distribution, leaving only regions with meaningful density.
```rust,no_run
# use kuva::plot::hexbin::HexbinPlot;
# use kuva::render::plots::Plot;
// Only render bins with at least 8 points
let plot = HexbinPlot::new()
.with_data(x, y)
.with_n_bins(10)
.with_min_count(8);
```
<img src="../assets/hexbin/min_count_1.svg" alt="Hexbin — min_count=1, all bins shown" width="560">
min_count = 1 — all bins are shown; peripheral singletons are visible.
<img src="../assets/hexbin/min_count_8.svg" alt="Hexbin — min_count=8, only dense bins" width="560">
min_count = 8 — only the dense cluster cores remain.
---
## Flat-top orientation
By default hexes are **pointy-top** (a vertex at the top). `.with_flat_top(true)` rotates to **flat-top** orientation (a flat edge at the top), which can align better with certain data layouts or visual preferences.
```rust,no_run
# use kuva::plot::hexbin::HexbinPlot;
# use kuva::render::plots::Plot;
let plot = HexbinPlot::new()
.with_data(x, y)
.with_flat_top(true);
```
<img src="../assets/hexbin/flat_top.svg" alt="Hexbin — flat-top orientation" width="560">
---
## Hex outline (stroke)
`.with_stroke(color)` draws a CSS-colored border around each hexagon. This distinguishes individual bins in dense regions and is useful for publication figures.
```rust,no_run
# use kuva::plot::hexbin::HexbinPlot;
# use kuva::render::plots::Plot;
let plot = HexbinPlot::new()
.with_data(x, y)
.with_stroke("#333333")
.with_stroke_width(0.8);
```
<img src="../assets/hexbin/stroke.svg" alt="Hexbin — with hex outline" width="560">
A dark outline clearly separates adjacent bins at the cost of slightly more visual noise.
---
## Color range clamping
`.with_color_range(lo, hi)` pins the colormap to a fixed value interval, ignoring bins outside the range. Values below `lo` receive the lowest colormap color; values above `hi` receive the highest. Use this to compare multiple plots on the same scale or to highlight a specific density range.
```rust,no_run
# use kuva::plot::hexbin::HexbinPlot;
# use kuva::render::plots::Plot;
let plot = HexbinPlot::new()
.with_data(x, y)
.with_color_range(2.0, 8.0);
```
<img src="../assets/hexbin/color_range.svg" alt="Hexbin — color range clamped to 2–8 counts" width="560">
---
## Axis range clipping
`.with_x_range(lo, hi)` and `.with_y_range(lo, hi)` restrict binning to a sub-region of the data and fix the corresponding axis limits. Points outside the specified window are silently discarded.
```rust,no_run
# use kuva::plot::hexbin::HexbinPlot;
# use kuva::render::plots::Plot;
let plot = HexbinPlot::new()
.with_data(x, y)
.with_x_range(-0.5, 3.0)
.with_y_range(-2.0, 4.0);
```
<img src="../assets/hexbin/x_range.svg" alt="Hexbin — x-axis range clipped to -0.5 .. 3.0" width="560">
---
## Colormaps
`.with_color_map(map)` selects the colormap. The same `ColorMap` variants used by `Heatmap` and `Histogram2D` apply.
```rust,no_run
use kuva::plot::{hexbin::HexbinPlot, ColorMap};
# use kuva::render::plots::Plot;
let plot = HexbinPlot::new()
.with_data(x, y)
.with_color_map(ColorMap::Inferno);
```
<img src="../assets/hexbin/inferno.svg" alt="Hexbin — Inferno colormap" width="560">
| `ColorMap::Viridis` | Blue → green → yellow. Perceptually uniform, colorblind-safe. **(default)** |
| `ColorMap::Inferno` | Black → orange → yellow. High contrast. |
| `ColorMap::Grayscale` | White → black. Print-friendly. |
| `ColorMap::Turbo` | Blue → green → red. High contrast over a wide range. |
| `ColorMap::Custom(f)` | User-supplied `Arc<dyn Fn(f64) -> String>`. |
---
## Hiding the colorbar
`.with_colorbar(false)` suppresses the colorbar and reclaims the right margin.
```rust,no_run
# use kuva::plot::hexbin::HexbinPlot;
# use kuva::render::plots::Plot;
let plot = HexbinPlot::new()
.with_data(x, y)
.with_colorbar(false);
```
---
## API reference
| `HexbinPlot::new()` | Create with defaults (20 bins, Viridis, Count, pointy-top, colorbar on) |
| `.with_data(x, y)` | Load scatter data; accepts any `Into<f64>` iterable |
| `.with_z(z, reduce)` | Attach a third variable and choose the `ZReduce` aggregation |
| `.with_n_bins(n)` | Number of hex columns across the x-axis (default `20`) |
| `.with_bin_size(s)` | Explicit hex circumradius in pixels — overrides `n_bins` |
| `.with_color_map(m)` | Colormap (default `Viridis`) |
| `.with_log_color(b)` | Log₁₀ color scale — compresses high-count peaks |
| `.with_min_count(n)` | Suppress bins with fewer than `n` points (default `1`) |
| `.with_normalize(b)` | Divide counts by total points; colorbar label → "Density" |
| `.with_colorbar(b)` | Show / hide the colorbar (default `true`) |
| `.with_colorbar_label(s)` | Override the auto-derived colorbar label |
| `.with_stroke(color)` | Hex outline color (CSS string) |
| `.with_stroke_width(w)` | Hex outline width in pixels (default `0.5`) |
| `.with_flat_top(b)` | Flat-top orientation (default `false` = pointy-top) |
| `.with_x_range(lo, hi)` | Clip data and fix x-axis extent |
| `.with_y_range(lo, hi)` | Clip data and fix y-axis extent |
| `.with_color_range(lo, hi)` | Clamp the colorbar scale to a fixed interval |