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()andGrid.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.yamlasked for adocspip extra, but v0.2.0 moved those to a PEP 735 dependency-group, whichextra_requirementscannot 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 nextbuild()starts clean;Chart._render()calls it first.Canvas.build()stays idempotent per instance..readthedocs.yamlinstalls the docs dependency-group via apost_installjob, 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 onbar,boxplot,violin,countplot,pointplot,strip,swarm, andhistogramcontrols 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 acceptcolor=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=Falseon.histogram()draws outline-only step histograms. In grouped stack/dodge modes, uses outline-only bars withedgecolorinstead of filled bars.
Internal
Dev dependencies (
pytest,sphinx, etc.) moved from[project.optional-dependencies]to PEP 735[dependency-groups]. Local development now usesuv syncinstead ofpip install -e ".[dev]"._resolve_order()helper centralized inmarks/_base.py— replaces per-marklist(dict.fromkeys(x))calls._apply_fill_style()helper inhistogram.pydeduplicates fill vs outline-only bar styling.Replaced
np.histogram()withnp.histogram_bin_edges()where only edges are needed.
Testing
422 tests passing
v0.1.2#
Fixes
Fixed docstring escape for
**extra_kwargsinKDE.render()so it renders correctly in Sphinx documentation.
Internal
Updated project logo.
v0.1.1#
Bug fixes
Rendering no longer mutates stored
Layerobjects —layer.paletteandlayer.encodingsare unchanged after.show()/.save(). Internally usesdataclasses.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**kwargsfor 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 topyproject.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 viaLineCollection. Supportscmap,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 withfmt).Area: new
y_errorparameter draws a lighter fill band around the top edge of the area.
Internal
Chart._render_ontogains asuppress_legendkwarg so facet rendering can draw one figure-level legend after panels.Chart._apply_labelssplit into_label_bar_containers,_label_scatter_offsets, and_label_line_pointshelpers, routed by layer mark types.Label application now runs before reference lines in
_apply_decorators, soaxes.linescontains only mark-created lines when labels are placed.Facet rendering switched from
tight_layout()tolayout="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 (viabar_label()) or scatter points (via column name). Supportsfmt,fontsize, andoffsetoptions.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. Supportsoutside right,outside left,outside top,outside bottom.
Internal
Consolidated
set_category_ticks()intomarks/_base.py— bar, pointplot, strip, and swarm all share the same helper.Vectorized
_aggregatein pointplot withnp.bincountinstead of per-category boolean masking._apply_labelsreceives the existingDataAdapterfrom 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). Setstacked=Trueto stack bars instead.Continuous color scales on scatter: pass a numeric column as
color=withcmap=,vmin=,vmax=to get a colormap + automatic colorbar. Setcolorbar=Falseto hide.Joint plot:
gufo.jointplot(df, "x", "y")creates a scatter with marginal histograms or KDE on the edges. Returns aGrid.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 wheregufo.chart(df)raisedTypeError: Unsupported data type: DataFramein 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()fromgufo/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 inbar.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()accepty_errorandx_errorparameters (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 thegufo.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:
RegressionandKDEare frozen dataclasses passed as parameters to existing marks, following the same pattern asGrid.Layer.__post_init__filters None values from encodings soenc.get("key", default)works correctly.Shared
render_categorical_scatter()helper in_base.pyeliminates duplication between strip and swarm renderers via pluggableoffset_fncallback.resolve_color_list()utility added to_base.pyfor categorical scatter color resolution.KDE mark uses
dataclasses.replace()for immutable config handling.gufo/stats/module added with__init__.py(scipy guard),regression.py, andkde.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. Supportscolorfor categorical grouping andcolumnsto select a subset. Returns aGrid.
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 supportViolin plot:
.violin("x", "y")— distribution shapes, horizontal mode, wide-form supportHeatmap:
.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 withpip 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
Gridis now a standalone class ingufo/layout/grid.py, no longer part ofChart.gufo.grid(2, 2)returns aGridinstance (not aChart). Thechart().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 aftercolscolumns (default 3).Wide-form scatter:
.scatter("x", ["col_a", "col_b"])renders one series per column with automatic colors and legend labelsWide-form bar:
.bar("x", ["col_a", "col_b"])renders grouped bars with automatic offset positioning, colors, and legend labels. Supportshorizontal=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 stylesDataAdapter.subset(mask)returns a filtered adapter for row-level subsetting (used by faceting)Gridsupports.title(),.theme(),.apply(),.show(),.save()Grid-level
.apply(func)receives(figure, axes_2d_array)for full matplotlib accessEmpty 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 usesGUFO_PALETTE.categoricalinstead of matplotlib’s tab10, so categorical colors are consistent with the active themeGrid 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 100Size 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
DataAdapterexposesraw_dataanddata_typeproperties — marks use these instead of private_data/_typeattributesBox plot and violin renderers now validate array lengths with
check_array_lengths()PEP 8 cleanup: long guard condition in
iter_color_groupsbroken across lines, extra blank lines removed invalidate.py, misaligned indentation fixed infacet.pyGridextracted fromChartinto dedicatedgufo/layout/grid.pyclassChart._render_onto(figure, axes, adapter=None)added soGridand faceting render panels without reaching into Chart internalsRedundant validation removed:
check_alpha,check_limit_order,check_positive_dimensions,check_scale,check_ticks_labels,check_xy_tuple— matplotlib provides equally clear errors for theseShared wide-form utilities extracted to
_base.py:is_wide_form(),render_wide_form()Shared distribution mark helper
group_by_x()in_base.pyDataAdapter._detect_typeusesisinstance(data, pd.DataFrame)instead of string comparisonapply_color()no longer returns a value — consistent withapply_label()Dark theme colors extracted to
_GUFO_DARK_PALETTEconstantAll imports moved to top of file per PEP 8 (no more lazy imports in methods)
layout/__init__.pysimplified to docstring only, breaking circular import chainPalettedataclass useslist[str]instead oftyping.List[str]Dead stubs removed:
data/transform.py,Chart._facet_optsDataAdaptercreated once per_render()call, not per layerrender_layer()accepts adapter directly instead of raw dataShared mark helpers extracted to
_base.py:apply_label(),apply_color(),iter_color_groups()Canvas.from_existing()classmethod replaces direct private attribute accessRedundant
_builtflag removed from Canvascheck_stroke_dashuses_DASH_STYLESas single source of truthwarn_nan_infusesnp.isfinite().all()fast path for clean dataSphinx 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_printGlobal theme via
gufo.set_theme()Per-chart theme via
.theme()Scoped theme via
gufo.theme_context()Custom theme creation via
Theme.merge()andgufo.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 gracefullygufo.grid(1, 1)no longer crashes (scalar Axes fromplt.subplotsnow handled)Color resolution with
data=Noneand a literal color string no longer raisesValueError
Internal improvements
Shared mark utilities (
resolve_color,default_colors) extracted tomarks/_base.py, eliminating code duplication across all four mark renderersChart._apply_decoratorsrefactored into table-driven setters and focused private methodsDataAdapter._resolve_columnconsolidated duplicate branchesColor 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