boxenplot

package
v0.0.19 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 28, 2026 License: MIT Imports: 7 Imported by: 0

Documentation

Overview

Package boxenplot is the imzero2 widget for letter-value (Hofmann, Wickham & Kafadar 2017) plots. It composes:

  • boxer/public/analytics/stats/letterval — LV math + oracle
  • boxer/public/analytics/stats/tdigest — streaming quantile source
  • egui2 plotBoxes / plotScatter / plotText primitives — rendering

The widget is stateless: construct one Renderer, then call Render any number of times per frame. Configure-once / render-many is the canonical pattern (see fieldview / errorview).

Caller must wrap Render calls inside a c.Plot(id) block. Each Render emits one BoxPlot series (with N nested BoxElems) plus optional outlier scatter/text annotations.

Index

Constants

View Source
const VerboseReadoutLineCount = 3

VerboseReadoutLineCount is the fixed number of rows WriteStatusLineVerbose emits, so a host panel's status area keeps a constant height whether or not the cursor is over a distribution (mirrors ecdf.ReadoutLineCount). Hosts budget vertical space from this.

Variables

View Source
var PackageProps = packageprops.Props{
	WASMWASI:         packageprops.WASMCompiles,
	WASMJS:           packageprops.WASMCompiles,
	WASMFreestanding: packageprops.WASMCompiles,
}

PackageProps records this package's curated properties (ADR-0080). Seeded by `wasmsurvey props generate`; curate by hand, then `wasmsurvey props verify`.

Functions

func WriteStatusLine

func WriteStatusLine(ch Crosshair)

WriteStatusLine emits a single weak-styled LabelAtoms row that fully summarises the hovered letter-value box for the reader, suitable for placement immediately below c.Plot(...).Send(). The content names the distribution, anchors the cursor on the value axis, and describes the hovered ring by the quantiles its edges represent (the directly-meaningful concept) rather than by the Hofmann/Wickham/Kafadar "letter-value depth" (an academic index for the same thing).

  • Inside a box (ch.Depth ≥ 2): `<name> │ x=…, y=… │ n=…, median=… │ quantiles [lo%, hi%] = [v_lo, v_hi] │ ≈… obs/tail beyond`
  • Above the deepest box (ch.Depth == 0, ch.PlotY > ch.MaxDepthHigh): `<name> │ x=…, y=… │ n=…, median=… │ above hi-th percentile (deepest box [v_lo, v_hi]) │ ≈… obs in this tail`
  • Below the deepest box (ch.Depth == 0, ch.PlotY < ch.MaxDepthLow): `<name> │ x=…, y=… │ n=…, median=… │ below lo-th percentile (deepest box [v_lo, v_hi]) │ ≈… obs in this tail`

Quantile percentages render via `%g` so a depth-3 box reads `[12.5%, 87.5%]` and depth-2 reads `[25%, 75%]`. Coverage (the fraction of the distribution inside the box) is derivable as `hi% − lo%` and intentionally not duplicated on the line to keep it scannable. The tail count is the analytical estimate from the matched LV (inside-box) or the deepest LV (outside-box) — the same number OutlierModeCount draws as `+N`.

No-op when ch.Valid is false; callers that want a placeholder message ("hover a distribution to inspect cursor values") should emit it themselves on the !ch.Valid branch. For the explaining, multi-line counterpart use WriteStatusLineVerbose.

func WriteStatusLineVerbose added in v0.0.5

func WriteStatusLineVerbose(ch Crosshair)

WriteStatusLineVerbose emits the explaining, multi-line counterpart to WriteStatusLine: it describes the hovered letter-value box in plain language — what quantiles its edges represent, the central fraction it covers, and the tail beyond it — rather than packing the same facts into one dense line. It always emits VerboseReadoutLineCount rows: the description when ch.Valid, a one-line hover hint otherwise, padded with blank rows so hovering on / off never reflows the host. Text via the pure [formatVerbose].

Types

type Crosshair

type Crosshair struct {
	Valid bool

	// Raw hover position in plot-data coordinates.
	PlotX float64
	PlotY float64

	// Distribution context. TotalN is recovered from the levels slice
	// via the shallowest non-median LV (TailCount = floor(n · 2⁻ᵈ));
	// the recovery is exact for samples whose n is divisible by 2ᵈ and
	// otherwise off by < 2ᵈ — fine for a human-readable readout.
	Argument float64
	Name     string
	Median   float64
	TotalN   int64

	// Innermost LV box containing PlotY. Depth==0 signals "outside every
	// drawn box" — PlotY is in the tail beyond the deepest ring; the
	// remaining Depth* fields are zero/NaN in that case.
	Depth          uint8
	DepthLowerQ    float64
	DepthUpperQ    float64
	DepthLow       float64
	DepthHigh      float64
	DepthTailCount int64

	// Deepest LV in the distribution. Always populated when Valid so
	// the status line can describe where PlotY sits relative to the
	// outermost ring even when no box contains it.
	MaxDepth          uint8
	MaxDepthLow       float64
	MaxDepthHigh      float64
	MaxDepthTailCount int64
}

Crosshair captures the cursor position over a boxenplot's Plot block and every derived statistic needed to interpret a hovered letter- value box: the matched distribution's series name and argument, the recovered sample size and median, the innermost LV depth whose box contains the hover Y (plus that depth's quantile range, value bounds, and analytical per-tail count), and the deepest LV's bounds and tail count so a cursor outside every drawn box is still placed relative to the outermost ring.

Valid is false when no hover information is currently available — the cursor is outside the plot, the cached hover refers to a different plot id, or no distribution claimed the hover via At() this frame.

Crosshair is intentionally analogous to ecdf.Crosshair so callers already familiar with the ECDF pattern (At → Render → PaintCrosshair → c.Plot(...).Send → WriteStatusLine) can lift the same scaffold across the two widgets without re-learning the contract.

type OutlierModeE

type OutlierModeE uint8

OutlierModeE controls how observations beyond the deepest rendered LV level are visualised.

const (
	// OutlierModeAuto picks Points or Count based on the analytical
	// budget (per-tail expected count) against OutlierAutoThreshold.
	// Below threshold → Points; at/above → Count.
	OutlierModeAuto OutlierModeE = iota
	// OutlierModeNone draws nothing beyond the deepest box.
	OutlierModeNone
	// OutlierModePoints draws each value in the extremes slice as a
	// scatter point at (argument, value). Caller must supply the
	// extremes (e.g. K-smallest and K-largest tracked alongside the
	// digest).
	OutlierModePoints
	// OutlierModeCount draws a "+N" annotation at the lower and upper
	// edges of the deepest box, where N is the per-tail expected count.
	OutlierModeCount
)

type Renderer

type Renderer struct {
	// contains filtered or unexported fields
}

Renderer is the configured boxenplot. Values are immutable after construction; fluent setters return modified copies.

func New

func New(idPrefix string) (inst Renderer)

New constructs a Renderer with IDS-aligned defaults. Palette and fill alpha are resolved from the styletokens Tier-1 / Tier-2 env surface (IDS_PALETTE_SEQUENTIAL / IDS_ACCESSIBILITY), so a typical caller never touches palette plumbing:

r := boxenplot.New("p99-mem")    // honours user's IDS env
r.Render(arg, levels, nil, -1)

Static defaults:

  • palette: styletokens.SequentialDefault()
  • paletteTStart/End: [0.20, 0.85] (default) or [0.10, 0.95] under AccessibilityHighContrast for boosted discriminability
  • fillAlpha: 0xC0 (default) or 0xFF under AccessibilityHighContrast
  • stroke: NeutralBorderDefault at 1.0 px
  • box width: 0.6 (argument-axis units), shrink 0.85 per depth
  • outlier mode: Auto with threshold 20

idPrefix scopes any widget-id-bearing primitive emitted by Render — pass a stable short string (e.g. "lat-cluster", "p99-mem").

func (Renderer) AnnotationColor

func (inst Renderer) AnnotationColor(col color.Color) (out Renderer)

AnnotationColor sets the colour used for the "+N" outlier-count labels and for any outlier scatter points. Default NeutralTextSecondary.

func (Renderer) At

func (inst Renderer) At(plotID c.AbsoluteWidgetId, argument float64, name string, levels []letterval.LVLevel) (out Crosshair)

At returns the Crosshair for the (argument, name, levels) tuple describing one distribution rendered into the given plot. The caller passes the same plotID it will hand to c.Plot — as an AbsoluteWidgetId (not ids.PrepareStr), so the id is stable across frames and matches the r15 hover register's stored value.

Crosshair.Valid is true when:

  • the r15 hover register names plotID as the hovered plot,
  • HoverX is finite,
  • and |HoverX - argument| ≤ snapWindow (default 0.5).

The typical caller loops over its distributions and keeps the last Valid Crosshair the loop produced — the snap window is half the argument-axis spacing, so at most one distribution claims any given hover. levels may be empty (the depth-1-only median-marker case); the returned Crosshair still reports the median and Argument while Depth stays at 0.

func (Renderer) BoxWidth

func (inst Renderer) BoxWidth(base, shrink float64) (out Renderer)

BoxWidth sets the depth-2 (innermost LV) box width in argument-axis units, plus a per-depth shrink multiplier. Each successive box is width × shrink^(depth-2) wide. Defaults: base 0.6, shrink 0.85. shrink = 1.0 gives Hofmann's constant-width convention; shrink < 1 produces the seaborn-style taper. Values outside (0, 1] are clamped.

func (Renderer) FillAlpha

func (inst Renderer) FillAlpha(a uint8) (out Renderer)

FillAlpha sets the alpha channel applied to every per-depth fill, overriding the palette's opaque output. Default 0xC0.

func (Renderer) OutlierAutoThreshold

func (inst Renderer) OutlierAutoThreshold(n int64) (out Renderer)

OutlierAutoThreshold sets the per-tail observation count at which Auto mode switches from Points (small counts) to Count (large). Default 20.

func (Renderer) OutlierMode

func (inst Renderer) OutlierMode(m OutlierModeE) (out Renderer)

OutlierMode selects the outlier-rendering strategy. See OutlierModeE for the enumerated semantics.

func (Renderer) PaintCrosshair

func (inst Renderer) PaintCrosshair(ch Crosshair)

PaintCrosshair emits a vertical PlotVLine at ch.Argument (snapped to the matched distribution's centre, not the raw hover X) using the renderer's annotation colour at half alpha. No-op when ch.Valid is false. Must be invoked inside the same c.Plot block as Render — the egui_plot drain renders vlines after box series so the crosshair sits visually on top of every box.

The vline anchors to the argument rather than HoverX because the boxenplot's argument axis is categorical (one column per distribution); a vline at the raw cursor X would slide between columns and read as a "no-man's-land" cursor instead of a "selected distribution" affordance.

func (Renderer) Palette

func (inst Renderer) Palette(p styletokens.SequentialE) (out Renderer)

Palette selects the IDS sequential data-encoding palette used for per-depth fills. Default SequentialBatlow.

func (Renderer) PaletteRange

func (inst Renderer) PaletteRange(start, end float32) (out Renderer)

PaletteRange clamps the t ∈ [0, 1] range sampled from the palette. Defaults (0.20, 0.85) avoid the extreme dark/light ends which lose shape against typical dark IDS backgrounds.

Note: when the resolved palette ramps in the opposite direction to batlow (white→black; currently only SequentialGrayC), fillForDepth silently swaps the supplied (start, end) so "deep=light, shallow=dark" reads consistently across presets. Callers needing the supplied range verbatim must avoid SequentialGrayC or override fillForDepth directly — there is no per-call opt-out today.

func (Renderer) Render

func (inst Renderer) Render(argument float64, levels []letterval.LVLevel, extremes []float64, perTailCountOverride int64)

Render emits the boxenplot primitives for one distribution at the given x position. Caller must already be inside a Plot block.

  • levels: from letterval.RecommendedLevels(oracle) or letterval.Levels(oracle, maxDepth). May be empty (no-op) or contain only depth 1 (single median marker is drawn).
  • extremes: raw extreme values for OutlierModePoints. Ignored by other modes; can be nil there. Provide both lower and upper extremes in one slice (the renderer treats the slice as opaque points, no sign convention).
  • perTailCountOverride: optional explicit per-tail outlier count for Count / Auto modes. Pass -1 to use the analytical estimate (deepest LV's TailCount). Use the override when the caller tracks the true count separately (a top-K + counter).

func (Renderer) SeriesName

func (inst Renderer) SeriesName(name string) (out Renderer)

SeriesName sets the legend label for the BoxPlot series. Empty disables the legend entry for this series. Default "boxen".

func (Renderer) SnapWindow

func (inst Renderer) SnapWindow(w float64) (out Renderer)

SnapWindow sets the half-width (in argument-axis units) within which At() claims a hover for this distribution. The default 0.5 matches unit-spaced arguments (1, 2, 3, …) and selects the nearest distribution by construction: hovering at x=1.4 lands inside argument 1's window but outside argument 2's. Callers with denser or sparser argument layouts should override accordingly. Non-positive values are clamped to a small positive epsilon so At() never matches everywhere.

func (Renderer) Stroke

func (inst Renderer) Stroke(col color.Color, widthPx float32) (out Renderer)

Stroke sets the box outline colour and width (px). Default NeutralBorderDefault at 1.0 px.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL