Changelog#

v0.2.1#

Fixes

  • Re-rendering a chart no longer double-draws. Canvas.build() cached the figure it created, so every render after the first drew on top of the previous render’s artists — calling .save() twice on one chart produced a wrong second image.

  • .clear() now clears rendered output, not just the registered layers. The caching above meant cleared marks were still drawn in the next render, so the “reusing charts” idiom documented in the README did not work.

  • .show() no longer leaks figures. Chart.show() and Grid.show() never closed their figure the way .save() does, leaving one open per call. Reusing a chart in a loop grew memory without bound.

  • Read the Docs installed none of the documentation dependencies. .readthedocs.yaml asked for a docs pip extra, but v0.2.0 moved those to a PEP 735 dependency-group, which extra_requirements cannot resolve.

Changed

  • .show() closes its figure after displaying it, unless matplotlib is in interactive mode (plt.ion(), %matplotlib widget), where the window or widget is still live.

Internal

  • New Canvas.reset() discards and closes the built figure so the next build() starts clean; Chart._render() calls it first. Canvas.build() stays idempotent per instance.

  • .readthedocs.yaml installs the docs dependency-group via a post_install job, keeping [dependency-groups] as the single source of truth.

Testing

  • 444 tests passing (up from 422). Each new regression test was verified to fail against the unfixed code.


v0.2.0#

New features

  • Category ordering: order= parameter on bar, boxplot, violin, countplot, pointplot, strip, swarm, and histogram controls category display order on the x-axis. Values not in the list are excluded from the plot.

  • Color group ordering: color_order= parameter on all marks that accept color= controls the rendering and legend order of categorical color groups.

  • Histogram grouping modes: multiple= parameter on .histogram()"layer" (overlay with transparency, the default), "stack" (cumulative bars sharing common bin edges), or "dodge" (side-by-side narrower bars per group).

  • Step histogram: fill=False on .histogram() draws outline-only step histograms. In grouped stack/dodge modes, uses outline-only bars with edgecolor instead of filled bars.

Internal

  • Dev dependencies (pytest, sphinx, etc.) moved from [project.optional-dependencies] to PEP 735 [dependency-groups]. Local development now uses uv sync instead of pip install -e ".[dev]".

  • _resolve_order() helper centralized in marks/_base.py — replaces per-mark list(dict.fromkeys(x)) calls.

  • _apply_fill_style() helper in histogram.py deduplicates fill vs outline-only bar styling.

  • Replaced np.histogram() with np.histogram_bin_edges() where only edges are needed.

Testing

  • 422 tests passing


v0.1.2#

Fixes

  • Fixed docstring escape for **extra_kwargs in KDE.render() so it renders correctly in Sphinx documentation.

Internal

  • Updated project logo.


v0.1.1#

Bug fixes

  • Rendering no longer mutates stored Layer objects — layer.palette and layer.encodings are unchanged after .show()/.save(). Internally uses dataclasses.replace() for immutable copies during the render pipeline.

  • Facet columns containing NaN now warn and exclude NaN rows instead of silently creating empty invisible panels.

  • kdeplot() now accepts **kwargs for matplotlib passthrough, consistent with all other marks.

  • Improved error messages when passing arrays, lists, or pandas Series to gufo.chart() — now explains to pass data directly to marks (e.g., gufo.chart().histogram(data)). Multi-dimensional arrays get a shape-specific message suggesting dict/DataFrame.

Internal

  • Facet _shared_color_scales() refactored from mutation-restore context manager to a pure function returning patched layer copies.

  • Added [test] and [dev] extras to pyproject.toml.

Testing

  • 403 tests passing


v0.1.0#

First tagged release on PyPI.

  • density= parameter on .histogram() for normalized histograms.

  • Documented matplotlib kwargs passthrough on all marks.

  • Release-hygiene: changelog cleanup, README polish, CI + trusted-publishing workflows, package rename from cerno to gufo, Read the Docs deploy.


v0.0.8#

Correctness fixes — faceted charts

  • Shared colorbar: faceted charts with continuous color (scatter/line) now draw a single figure-level colorbar using the global data range across all panels. Previously each panel produced its own colorbar with a per-subset scale, making colors incomparable across panels.

  • Shared legend: when .legend() is called on a faceted chart, a single figure-level legend is drawn (deduped by label) instead of one duplicate legend per panel.

Mark feature parity

  • Line: color= now accepts a numeric column for a gradient line drawn via LineCollection. Supports cmap, vmin, vmax, colorbar — mirrors .scatter()’s continuous-color API.

  • .label() on line and pointplot: per-point labels on line charts (optionally from a column) and on pointplot means (formatted with fmt).

  • Area: new y_error parameter draws a lighter fill band around the top edge of the area.

Internal

  • Chart._render_onto gains a suppress_legend kwarg so facet rendering can draw one figure-level legend after panels.

  • Chart._apply_labels split into _label_bar_containers, _label_scatter_offsets, and _label_line_points helpers, routed by layer mark types.

  • Label application now runs before reference lines in _apply_decorators, so axes.lines contains only mark-created lines when labels are placed.

  • Facet rendering switched from tight_layout() to layout="constrained" for correct spacing with figure-level colorbars and legends.

Testing

  • 384 tests passing


v0.0.7#

New chart types

  • Point plot: .pointplot("x", "y") — connected category means with 95% CI error bars. Supports categorical color grouping (dodged) and horizontal mode.

New features

  • Data labels: .label() adds value labels to bar charts (via bar_label()) or scatter points (via column name). Supports fmt, fontsize, and offset options.

  • LOWESS smoothing: fit=gufo.lowess() on .scatter() adds a non-parametric smooth curve. Requires statsmodels (pip install gufo[stats]).

  • Facet axis sharing: .facet("col", sharex=False, sharey=False) allows independent axis ranges per panel. Default is shared (preserving existing behavior).

  • Legend outside positioning: .legend(position="outside right") places the legend outside the plot area. Supports outside right, outside left, outside top, outside bottom.

Internal

  • Consolidated set_category_ticks() into marks/_base.py — bar, pointplot, strip, and swarm all share the same helper.

  • Vectorized _aggregate in pointplot with np.bincount instead of per-category boolean masking.

  • _apply_labels receives the existing DataAdapter from the render pipeline instead of constructing a new one.

Testing

  • 363 tests passing


v0.0.6#

New features

  • Stacked/dodged bar grouping: .bar("x", "y", color="category") now groups bars by category (dodged by default). Set stacked=True to stack bars instead.

  • Continuous color scales on scatter: pass a numeric column as color= with cmap=, vmin=, vmax= to get a colormap + automatic colorbar. Set colorbar=False to hide.

  • Joint plot: gufo.jointplot(df, "x", "y") creates a scatter with marginal histograms or KDE on the edges. Returns a Grid.

  • Grid width/height ratios: gufo.grid(2, 2, width_ratios=[3, 1], height_ratios=[1, 3]) for non-uniform panel sizing.

  • Horizontal histogram: .histogram("x", horizontal=True).

Bug fixes

  • DataAdapter now detects pandas and polars DataFrames even when the module-level import fails, using type(data).__module__ as a fallback. Fixes a bug where gufo.chart(df) raised TypeError: Unsupported data type: DataFrame in certain environment configurations (e.g., Jupyter notebooks with separate venvs).

  • Scatter continuous color detection no longer misfires on single-character color strings like "r".

  • Removed unused is_datetime() from gufo/data/inference.py.

Documentation

  • Complete docstrings added to all public Chart methods (title, subtitle, xlabel, ylabel, caption, annotate, xlim, ylim, xscale, yscale, xticks, yticks, legend, theme, size) and Grid methods (title, theme).

  • Visual gallery with 19 rendered chart examples at docs/gallery.md.

  • Getting-started tutorial expanded with a 6-step walkthrough.

  • Sphinx doc pages added for countplot, ecdf, and rug.

  • Pair plot moved from “Chart types” to “Layout” section.

  • Joint plot added to README.

Internal

  • Extracted shared _draw_dodged() and _aggregate_by_x() helpers in bar.py, replacing duplicated logic between grouped-color and wide-form bar rendering.

  • Replaced O(nug) boolean masking in bar color groups with vectorized np.bincount.

Testing

  • 339 tests passing


v0.0.5#

New chart types

  • Countplot: .countplot("x") — bar chart of value counts, with optional categorical color grouping for side-by-side bars.

  • ECDF: .ecdf("x") — empirical cumulative distribution function, with optional categorical color grouping.

  • Rug plot: .rug("x") — tick marks along an axis, with configurable height and alpha. Useful as a layer on histograms or KDE plots.

New features

  • Categorical color on box/violin: .boxplot("x", "y", color="category") and .violin("x", "y", color="category") now group by a third variable.

  • Error bars: .scatter(), .line(), and .bar() accept y_error and x_error parameters (column name or array).

  • Reference lines and bands: .hline(), .vline(), .hband(), .vband() for adding reference markers with optional labels and styling.

  • Color palette API: .palette("colorblind") or .palette(["#e63946", "#457b9d"]) to set named or custom palettes per chart. Built-in palettes: gufo, pastel, bold, colorblind.

Testing

  • 319 tests passing


v0.0.4#

New chart types

  • KDE (kernel density estimation): .kdeplot("x") — standalone density plot with optional fill, categorical color grouping, configurable bandwidth. Requires scipy. (Originally named .kde(); renamed in 0.1.0 to avoid collision with the gufo.kde() overlay factory.)

  • Strip plot: .strip("x", "y") — individual data points with random jitter along a categorical axis. Supports horizontal mode and wide-form data.

  • Swarm plot: .swarm("x", "y") — beeswarm layout that avoids overlapping points along a categorical axis. Requires scipy.

New features

  • Regression overlay: pass fit=gufo.regression() to .scatter() for linear or polynomial fit lines. Supports custom degree, color, linestyle, linewidth, and label. Uses numpy only (no scipy required).

  • KDE histogram overlay: pass kde=gufo.kde() to .histogram() to overlay a density curve scaled to the histogram’s y-axis.

  • scipy optional dependency: pip install gufo[scipy] for KDE and swarm plot support.

Internal improvements

  • Config object pattern: Regression and KDE are frozen dataclasses passed as parameters to existing marks, following the same pattern as Grid.

  • Layer.__post_init__ filters None values from encodings so enc.get("key", default) works correctly.

  • Shared render_categorical_scatter() helper in _base.py eliminates duplication between strip and swarm renderers via pluggable offset_fn callback.

  • resolve_color_list() utility added to _base.py for categorical scatter color resolution.

  • KDE mark uses dataclasses.replace() for immutable config handling.

  • gufo/stats/ module added with __init__.py (scipy guard), regression.py, and kde.py.

Testing

  • 282 tests passing


v0.0.3#

New features

  • Pair plot: gufo.pairplot(df) generates an NxN grid of scatter plots (off-diagonal) and histograms (diagonal) for all numeric columns. Supports color for categorical grouping and columns to select a subset. Returns a Grid.

Bug fixes

  • Histogram now handles categorical color encoding by grouping (previously passed raw category names to matplotlib as color values, causing a crash)

Internal improvements

  • DataAdapter.column_names() method added for listing available columns

Testing

  • 245 tests passing


v0.0.2#

New chart types

  • Box plot: .boxplot("x", "y") — grouped boxes, horizontal mode, wide-form support

  • Violin plot: .violin("x", "y") — distribution shapes, horizontal mode, wide-form support

  • Heatmap: .heatmap() — matrix form (DataFrame is the matrix) and long-form (x, y, color columns pivoted internally). Custom colormaps and cell annotations.

  • Area chart: .area("x", "y") — filled area, stacked area from wide-form data, categorical color grouping

New features

  • Polars support: pass a Polars DataFrame to gufo.chart() and use it exactly like pandas. Install with pip install gufo[polars].

  • Two-variable faceting: .facet("col_var", row="row_var") creates a row × column grid of subplots. Row-only faceting with .facet(row="var").

  • pandas is now an optional dependency — install with pip install gufo[pandas]. Core gufo works with dicts, numpy arrays, and lists.

Breaking changes

  • Grid is now a standalone class in gufo/layout/grid.py, no longer part of Chart. gufo.grid(2, 2) returns a Grid instance (not a Chart). The chart().grid(...) pattern no longer works.

Previously shipped features (v0.0.1 cycle)

  • Faceting: gufo.chart(df).scatter("x", "y").facet("category") splits data by a categorical column into subplots. Chart-level .title() becomes a super-title; each panel is titled with its category value. Panels wrap after cols columns (default 3).

  • Wide-form scatter: .scatter("x", ["col_a", "col_b"]) renders one series per column with automatic colors and legend labels

  • Wide-form bar: .bar("x", ["col_a", "col_b"]) renders grouped bars with automatic offset positioning, colors, and legend labels. Supports horizontal=True.

  • Input validation module (gufo/core/validate.py) with plain-English error messages for: array length mismatches, non-numeric histogram data, NaN/Inf warnings, and invalid stroke dash styles

  • DataAdapter.subset(mask) returns a filtered adapter for row-level subsetting (used by faceting)

  • Grid supports .title(), .theme(), .apply(), .show(), .save()

  • Grid-level .apply(func) receives (figure, axes_2d_array) for full matplotlib access

  • Empty grid cells are automatically hidden

  • Dark theme (gufo_dark) now includes a high-contrast color cycle

Bug fixes

  • Violin wide-form now correctly applies user color encoding (previously silently ignored)

  • Figure memory leak: .save() now closes the figure after writing (both single charts and grids)

  • default_colors() now uses GUFO_PALETTE.categorical instead of matplotlib’s tab10, so categorical colors are consistent with the active theme

  • Grid figures now created inside theme context (previously ignored theme)

  • _normalize_size() always returns numpy arrays (previously mixed list/array return types)

  • _normalize_size() equal-value fallback now uses midpoint of min/max size instead of hardcoded 100

  • Size encoding in scatter now raises on resolution failure instead of silently swallowing the error

  • Size encoding in scatter categorical path resolved once instead of N times per category

Internal improvements

  • DataAdapter exposes raw_data and data_type properties — marks use these instead of private _data/_type attributes

  • Box plot and violin renderers now validate array lengths with check_array_lengths()

  • PEP 8 cleanup: long guard condition in iter_color_groups broken across lines, extra blank lines removed in validate.py, misaligned indentation fixed in facet.py

  • Grid extracted from Chart into dedicated gufo/layout/grid.py class

  • Chart._render_onto(figure, axes, adapter=None) added so Grid and faceting render panels without reaching into Chart internals

  • Redundant validation removed: check_alpha, check_limit_order, check_positive_dimensions, check_scale, check_ticks_labels, check_xy_tuple — matplotlib provides equally clear errors for these

  • Shared wide-form utilities extracted to _base.py: is_wide_form(), render_wide_form()

  • Shared distribution mark helper group_by_x() in _base.py

  • DataAdapter._detect_type uses isinstance(data, pd.DataFrame) instead of string comparison

  • apply_color() no longer returns a value — consistent with apply_label()

  • Dark theme colors extracted to _GUFO_DARK_PALETTE constant

  • All imports moved to top of file per PEP 8 (no more lazy imports in methods)

  • layout/__init__.py simplified to docstring only, breaking circular import chain

  • Palette dataclass uses list[str] instead of typing.List[str]

  • Dead stubs removed: data/transform.py, Chart._facet_opts

  • DataAdapter created once per _render() call, not per layer

  • render_layer() accepts adapter directly instead of raw data

  • Shared mark helpers extracted to _base.py: apply_label(), apply_color(), iter_color_groups()

  • Canvas.from_existing() classmethod replaces direct private attribute access

  • Redundant _built flag removed from Canvas

  • check_stroke_dash uses _DASH_STYLES as single source of truth

  • warn_nan_inf uses np.isfinite().all() fast path for clean data

  • Sphinx theme switched from Furo to PyData Sphinx Theme

Testing

  • 229 tests covering data layer, core API, all mark types (including wide-form and distribution), theming, grid layout, single- and two-variable faceting, Polars integration, and input validation


v0.0.1#

Initial release.

Chart types

  • Scatter: color encoding (categorical and numeric), size encoding, alpha

  • Line: single series, wide-form multi-series, long-form color grouping, stroke dash styles

  • Bar: vertical and horizontal, color encoding

  • Histogram: configurable bins, works with raw arrays

Theming

  • Three built-in themes: gufo_modern, gufo_dark, gufo_print

  • Global theme via gufo.set_theme()

  • Per-chart theme via .theme()

  • Scoped theme via gufo.theme_context()

  • Custom theme creation via Theme.merge() and gufo.register_theme()

Layout

  • gufo.grid(rows, cols) multi-panel layout

Data formats

  • pandas DataFrame (long-form and wide-form)

  • Python dict

  • numpy arrays and Python lists

Escape hatch

  • .apply(func) for direct matplotlib access

Bug fixes

  • is_categorical() no longer crashes on empty arrays

  • _normalize_size() handles empty arrays gracefully

  • gufo.grid(1, 1) no longer crashes (scalar Axes from plt.subplots now handled)

  • Color resolution with data=None and a literal color string no longer raises ValueError

Internal improvements

  • Shared mark utilities (resolve_color, default_colors) extracted to marks/_base.py, eliminating code duplication across all four mark renderers

  • Chart._apply_decorators refactored into table-driven setters and focused private methods

  • DataAdapter._resolve_column consolidated duplicate branches

  • Color palette defined in a single source of truth (style/color.py)

Testing

  • Added pytest test suite with 138 tests covering data layer, core chart API, all mark types, theming, and grid layout