Skip to content

Releases: randyzwitch/dataviz_mojo

v0.15.0

Choose a tag to compare

@randyzwitch randyzwitch released this 01 Oct 19:40

Three pull requests since v0.14.0. The new feature is record(), which draws a chart once into a canvas_mojo DisplayList that can be replayed as often as needed.

Built on canvas_mojo 0.42.0 (commit 8fcb0c8, the v0.42.0 tag) and on the same Mojo 1.2.0.dev2026092105 nightly as v0.14.0. A consumer workspace keeps the same channel list and cutoff as before:

channels = [
    { channel = "https://conda.modular.com/max-nightly", exclude-newer = "2026-09-21T12:00:00Z" },
    "https://conda.modular.com/max",
    "conda-forge",
]

New

  • record(plot, output=OutputFormat.PNG) (#862, #864) returns a DisplayList holding the whole figure: background, axes, mark, annotations, labels, styled runs and math rules, in the order a direct render draws them. The recording owns its data, so it outlives the chart, and it replays into a Canvas, SvgCanvas or PdfCanvas. For every mark, the replay matches render() pixel for pixel and render_svg() markup for markup. A figure with math labels, annotations and tooltips also matches render_pdf() byte for byte. Pass OutputFormat.SVG or PDF when the recording is for vector output, since a few marks draw differently there. A recording is one layout at the chart's size; for another size, call size() and record again. To match render() on a raster canvas, replay between begin_supersampled() and end_supersampled(). The new section in the output-formats guide shows how.
  • pixi run bench-record reports recording time, retained bytes and replay cost beside direct rendering.

Changed

  • AnyChart.bound[...] takes a fifth parameter, display_list (#864), so a direct call needs one more argument. erased_with[...]() has a default for it and needs no edit. erased() does not bind the display-list slot. To record a chart after erasing it, use chart.erased_with[display_list=True](); recording an erased() chart raises and names that fix.
  • Drawing an AnyChart into a target type with no erased slot is now a compile error. It used to fall through to the bounds slot.

Fixed

  • Calendar heatmap tooltips (#864). Each cell's SVG tooltip group was never closed, so the color legend ended up inside the last cell's tooltip. Each group now closes after its cell. Raster output is unchanged.

Known issue

  • Replaying a dense recording into a supersampled Canvas takes about 3x as long as render(): 54 ms against 20 ms for a 10,000-point scatter. The pixels are identical. The cause is in canvas_mojo (randyzwitch/canvas_mojo#506): the restore() at the end of every replay makes the supersampled region switch from drawing in bands to drawing the whole enlarged canvas. SVG replay and unsupersampled raster replay are as fast as direct rendering.

For contributors

  • pixi run check-consumer (#863, #865) builds the package the way a user installs it and runs a chart program against it. CI's package job uses the same script. Don't use pixi build: it ignores the nightly channel's cutoff and fails whenever a newer nightly exists.
  • Tests run in batch drivers, which shortens CI (#861).

v0.14.0

Choose a tag to compare

@randyzwitch randyzwitch released this 28 Sep 23:53

Sixty-six commits since v0.13.0. A chart's mark is now its type, so calling the wrong encoder is a compile error. Dated data gets real time axes, render_layers() combines more kinds of bars, filled charts can carry patterns, and the package moves to Mojo 1.2.

Built on canvas_mojo 0.41.0 at commit 0bea340, one commit past the v0.41.0 tag (it adds font discovery inside Conda environments), and on the Mojo 1.2.0.dev2026092105 nightly.

Not purely additive. These need a caller edit:

  • Mojo 1.2 nightly, pinned exactly (#851). 1.2 is not on the stable max channel yet. A consumer workspace needs the nightly channel first in its list, with the same cutoff, so dataviz's dependencies build with the pinned compiler:

    channels = [
        { channel = "https://conda.modular.com/max-nightly", exclude-newer = "2026-09-21T12:00:00Z" },
        "https://conda.modular.com/max",
        "conda-forge",
    ]
  • Composition takes AnyChart (#836). render_layers, render_facets, render_grid, Figure and the inset entry points take a List[AnyChart]: wrap each chart as AnyChart(chart) or chart.erased(). For the common case, pass the charts one by one instead: render_layers(bars, line), save_facets(a, b, cols=2, path=...).

  • histogram(..., stepfilled=True) is now stepped_histogram(...) (#836).

  • The default categorical palette is Okabe–Ito (#792), which stays distinguishable under the common forms of color blindness. For the old colors, set Theme.categorical_palette to tab10_legacy().

  • A repeated category raises (#788). bar(), encode_categorical() and the DataFrame overload name the first and second positions of the duplicate, where they used to draw two bars with one label.

  • save() refuses an extension it cannot write (#775). .jpg, .gif, .webp and the rest used to get SVG or PNG bytes under the wrong name. A path with no extension still uses Theme.output_format.

  • Mark(n) is gone (#860). Marks come only from the named constants, such as Mark.POINT.

  • dataviz.plot exports only Plot (#831). Import other names from dataviz, or from the module that defines them.

The mark is a type

Plot().mark_gantt() returns a Chart[Gantt], and every one-call function returns its own Chart type (#836). Each mark type implements only the encoders it accepts, so Plot().mark_gantt().encode_categorical(...) fails to compile, naming the encoder and the mark. It used to compile and raise at run time. encode_quiver() on a barbs chart and encode_ecdf() on a KDE or rug chart are compile errors too (#852).

Settings can still come before the mark: Plot().size(400, 300).mark_point() and Plot().mark_point().size(400, 300) build the same chart.

A program compiles only the marks it uses. Passing charts one by one to render_layers() or render_facets() compiles only the render pipelines those calls draw through (#839).

Dates on the x-axis

Three marks place data at real timestamps instead of treating dates as categories. Gaps such as weekends and missing days take up real space, and ticks show dates in the first timestamp's time zone.

  • bar(dates, values) (#799): each bar is centered on its timestamp and spans the shortest observed interval, so bars never overlap.
  • candlestick() with List[Morrow] (#797): body width follows the median spacing, capped so short gaps don't overlap.
  • gantt(categories, start, end) with dates (#798): tooltips show both dates, and data labels show the duration.

Each has a DataFrame overload that reads typed date and datetime columns.

Layering

  • Several bar layers with the same categories sit side by side within each category (#796).
  • A grouped or stacked bar chart can own the frame, with point, line or area layers at each category's center (#800), for example stacked bars with a total line.
  • Two or more arc layers draw as concentric rings, each with a legend entry (#795).

Patterned and pinned fills

  • Theme(fill_pattern_by_category=True) draws a pattern over each filled shape of the eleven marks that fill one shape per category or series, including arc, stacked and grouped bar, funnel, treemap and sunburst (#849). HatchStyle has eight styles, assigned in the same order as the palette colors. Legends match, and raster, SVG and PDF all draw the same geometry.
  • fill_colors() and fill_patterns() pin a category or series name to a color or pattern (#850). Names the map leaves out keep their palette position, and names the chart lacks are ignored, so one map can serve every panel of a figure.

Also new

  • sort_categories() (#847) on the eight categorical marks: by value or label, either direction, or in an order you give. Error bars move with their categories.
  • X error bars (#845): encode() takes x_err, or x_err_lower and x_err_upper, on points, lines and effect scatter. A point with both draws a cross.
  • shared_x_scale (#846) on facets and grids, alongside shared_y_scale.
  • Named palettes (#792): Tableau 10 and Paul Tol's bright, muted and light sets.
  • Abbreviated tick labels (#789): TickFormat.AUTO writes values of a million and up, and nonzero values below 0.001, with SI prefixes.
  • MAX tensors as input (#805): any plotting call that takes a NumPy array also takes a MAX Python Tensor or Buffer.
  • More DataFrame overloads: gauge (#808), pcolormesh from long-form cells (#811), dendrogram (#809), voxels (#812), bullet (#810), radar from long-form rows (#807), arc diagram (#803), grouped scatter facets (#801), jointplot and pairplot (#804), corrplot (#791), parallel coordinates (#794), eventplot from long-form rows (#793), explicit tricontour levels (#787), and five more flat-column functions (#777).
  • PNG and PDF export with no system fonts (#806). The package depends on a packaged DejaVu Sans face, which canvas_mojo finds inside the Conda environment, so exports work in minimal containers.
  • Linux ARM (#802), tested and packaged in CI.
  • CJK text in PNG and PDF, with fonts that ship as TrueType Collections, is now covered by a test (#786).

Infrastructure

  • The route to mark types: the render pipeline moved out of plot.mojo (#816, #819), each mark bound its own renderer (#817, #821, #822), shared settings were grouped and every renderer and encoder re-signed off Plot (#826, #832–#834), and the marks were added as types (#835) before Plot was switched over (#836).
  • Facts about a mark live on its type (#837, #858). Its features and drawing properties, such as whether it can be layered or supersampled, are required constants, so a new mark that leaves one out fails to compile. Code that checked lists of marks by hand reads those constants now. Leftover run-time mark checks (#856), a lookup table (#857) and the name table (#860) are gone.
  • New gates: dataviz/core imports nothing above it, and dataviz.plot exports only what it defines (#830, #831); dependency pins must agree across the manifest's sections (#785); the output digest records the toolchain it was made with (#790); the documented install line must name the current tag (#771); the home page's chart count is checked (#772).
  • Project hygiene: issue and PR templates and a security policy (#770), dataframe_mojo pinned to a tag (#769), the README points to releases and the tracker (#774), and the interactive-exploration scope is written down and deferred (#814).

v0.13.0

Choose a tag to compare

@randyzwitch randyzwitch released this 21 Sep 13:39

Seventy-three commits since v0.12.0. Charts now read from a dataframe_mojo DataFrame, missing observations have a defined meaning everywhere, ten 3D marks arrive, and labels can carry mathematical notation.

Built on canvas_mojo v0.39.1, and on Mojo 1.1.

Not purely additive. Two changes need a caller edit:

  • clustermap_svg(), clustermap_pdf(), jointplot_svg(), jointplot_pdf(), pairplot_svg() and pairplot_pdf() are gone (#697). The three composites return an unrendered Figure instead, which you render with whichever backend you want — one function where there were three, and the figure can be inspected or placed before it is drawn.
  • Theme.svg_tooltips is replaced by Theme.tooltips (#700), a three-way Tooltips.AUTO/ON/OFF policy rather than a Bool. AUTO emits tooltips up to AUTO_TOOLTIP_LIMIT (1000) data points and stops, so a large chart no longer silently doubles its own file size.

Charts from a data frame

The largest change in this release (#364, #743). Every one-call chart function has an overload that names columns instead of taking lists:

var plot = bar(df, x="region", y="revenue")

Sixty-two chart functions take a DataFrame now, across every family — basic, categorical, distributions, binned, grid, hierarchy, radial, relationships, multivariate and 3D. Column names become the default axis titles, so a labeled chart is one call.

Long-form frames (#743) — one row per observation, with a column naming the group, is the shape data actually arrives in. box(df, category="site", value="reading") buckets the rows itself; multi-series marks read a series column the same way.

Faceting by a column (#364) — facet_by(df, "region") splits a frame into one part per distinct value, in first-appearance order, ready for render_facets(). pooled_extent() computes a shared domain across the parts so the panels can be compared.

Missing data

A defined answer everywhere a value can be absent (#367), rather than a crash or a silent zero. Theme.missing selects the policy: Missing.DRAW (the default) draws around the hole, Missing.RAISE refuses the chart.

  • A line breaks at a gap rather than interpolating across it, and a point is not plotted.
  • A matrix cell with no value is left blank rather than being colored as if it were zero.
  • Every statistic — mean, median, quantiles, KDE bandwidth, correlation, the regression fits — is computed from the observations that are there, with the count reported honestly.
  • A missing category is labeled by Theme.missing_category_label.

Reading a frame, absence is decided by the Arrow validity bit and never by the payload, so a slot holding a leftover number is still absent. Inside the package it travels as NaN, which every mark and statistic checks for.

3D marks

Ten new marks (#345), taking the total from 63 to 73:

  • scatter3d() / plot3d() — points and lines in space, with a camera, a labeled frame and depth sorting.
  • surface3d() / wire3d() / trisurf3d() — a height field as a shaded surface, a wireframe, or a surface over a triangulation.
  • bar3d() / voxels() — bars on a plane, and filled cells on a lattice.
  • stem3d() / quiver3d() / fill_between3d() — stems to a baseline, a vector field, and a filled ribbon between two curves.

Mathematical labels

"$\\sigma^2$" in any title, axis caption, legend entry, tick label or annotation (#371). Superscripts, subscripts, fractions, Greek letters and the common operators, laid out as positioned runs and drawn through the same text path every plain label takes — so raster, SVG and PDF place each run at the same anchor by construction rather than by three implementations agreeing.

A label with no $ never enters the parser, and its output is byte-for-byte what it was before.

Cross-mark capability

A published support matrix (#213) — nine capabilities (tooltips, data labels, horizontal, the three annotation kinds, log x, log y, encoded color/size) across all 73 marks, generated from the marks themselves and checked by a test, so the table cannot drift from the code.

Then the gaps it exposed were closed. SVG tooltips reached the grid, radial, hierarchy and relationship families and a dozen more marks (#677–#683); data labels reached nine more (#684, #685); horizontal=True reached WATERFALL, BULLET and POINTPLOT (#686). A mark that ignores one of these now raises instead of accepting the setting and doing nothing (#717).

Also new

  • annotate_smooth() (#147) — a LOESS or polynomial trend line through a scatter.
  • A force-directed graph layout (#157) — GraphLayout.FORCE beside the existing circular one.
  • Tight cropping (#372) — render_tight() and save(..., tight=True) trim a figure to its ink.
  • Log-space binning and density (#720) — hexbin, hist2d and KDE estimate in log space on a log axis, instead of binning linearly and drawing the result on a log scale.
  • Log axes on three more marks (#719) — RUG, ECDF and PCOLORMESH, with the reason recorded for each mark that still cannot take one.
  • Tick counts fitted to the axis (#727) — a short axis gets fewer ticks rather than the same count crowded together.
  • save(PdfCanvas) and one central extension guard (#696).
  • Asymmetric error bars on Mark.LINE (#702).
  • Migration guides from matplotlib, pyplot and seaborn (#613), and five end-to-end workflow recipes that check their own claims (#374).

Fixes

  • A caller's explicit domain was discarded when encode_histogram() pinned the bin range (#721).
  • annotate_vline() was misplaced on a horizontal frame, which reported the wrong axis as its x scale (#688).
  • contour() drew through a hole rather than around it (#743).
  • mojo format crashed on a shared grammar cache between workspaces; each workspace now has its own (#733).
  • Forty-five mojo doc warnings, unreported because mojo doc exits 0 regardless (#756). Five of the new DataFrame overloads had an Args: block truncated by a stray blank line, so eight arguments each were missing from the API reference. A gate now fails the build on any warning naming a file under dataviz/.

Infrastructure

  • Renderer dispatch chosen at construction (#607) rather than per draw call.
  • Text replayed through one DrawTarget-generic function (#578), so the three backends share one implementation.
  • Three new gates: a docstring that names another plotting library fails the format check (#650), a relative docs link that resolves to nothing fails the docs build (#652), and a mark added without raising Mark.COUNT fails a test (#648).

Known open

  • #754 — a CJK label fails PNG and PDF export: the fallback face is a TrueType Collection that canvas cannot open (canvas_mojo#481).
  • #755 — save() writes SVG markup to any extension it does not recognize, including .jpg and .html.
  • #767 — export needs a font on the machine, so it fails in a minimal container. Whether to bundle a fallback face is open.
  • #764 — the default categorical palette is not color-blind safe.
  • #762 / #763 — installing still means a git source under a preview flag, and there is no linux-aarch64 build.
  • #746 — an intermittent segfault in the parallel test suite, roughly one run in four.

v0.12.0

Choose a tag to compare

@randyzwitch randyzwitch released this 15 Sep 16:00
a7271c8

One hundred and twenty-six commits since v0.11.0. Eight new marks, five new composite figures, a second output format, and two new axis transforms.

Built on canvas_mojo v0.37.1.

Not purely additive: countplot() was removed (#611). A count per category is the caller's one-liner, and the chart it drew is bar() over that. Everything else is additive.

New marks

Fifty-five to sixty-three.

Mark.HISTOGRAM (#435, #513) — histogram() had been drawing through the bar mark; it now has one of its own, one rectangle per bin at numeric x positions, with horizontal=True and a pinned histogram-plus-KDE overlay (#437, #515).

Mark.HIST2D / Mark.HEXBIN (#340, #509) — hist2d() and hexbin(), points binned by density onto a rectangular or hexagonal lattice. The hexagonal one tiles without the axis-aligned artifacts a rectangular grid shows on diagonal structure.

Mark.QUIVER (#342, #512) and Mark.STREAMPLOT (#343, #519) — a vector field as arrows, and the same field as streamlines traced through it.

Mark.POINTPLOT (#350, #504) — the estimate as a joined point with a whisker.

Mark.BOXENPLOT (#356, #505) — the letter-value plot, which keeps resolving the tails where a box plot stops at one whisker.

Mark.DENDROGRAM (#355, #619) — the merge tree agglomerative clustering produces, drawn as brackets.

New composite figures

clustermap() (#355, #619) — a matrix with its rows and columns reordered by hierarchical clustering, a dendrogram on each clustered side. linkage() is public: four methods (single, complete, average, Ward) over three metrics (Euclidean, cityblock, correlation).

jointplot() (#354, #593) — a scatter with each variable's distribution beside it.

pairplot() (#353, #566) — every variable against every other, distributions down the diagonal.

render_grid() (#347, #567) — unequal cells on a shared layout core, with align_axes to line up plot rects down a column and across a row (#569, #591).

render_inset() (#347, #610) — a second plot inside a base plot's plot rect.

Publication export

PDF output (#372, #617) — render_pdf() and save(plot, "chart.pdf"), through the same DrawTarget path every other backend uses. Paths stay paths and text stays text, embedded as a font subset, so a label is selectable rather than a picture of itself.

A physical size contract — one layout unit is one PDF point, 1/72 inch, so size(640, 420) is a 640-by-420-point page. size_inches() and size_mm() say it the other way round, and save(..., dpi=300) holds the physical size while moving the pixel count.

Every composite figure renders to vector and to PDF too (#620, #621, #629).

Axis and color control

symlog axes (#368, #630) — linear within a threshold of zero, logarithmic beyond it. Data that crosses zero cannot go on a log axis at all, and a linear axis collapses the small values against the large; this keeps zero, keeps the sign, and still resolves decades.

Axis controls (#368, #614) — explicit tick positions and labels, reversed axes, equal data-unit aspect.

Color normalization (#370, #616) — shared limits across charts, a pinned diverging center, out-of-range colors, and symmetric_color_domain() for a balanced ramp.

Shared semantic mappings (#365, #625) — shared_categories(), shared_color_map() and shared_shape_map() resolve one answer for a whole figure, so a category missing from one panel no longer shifts the colors of the rest. Plot.encode(shape_map=...) is new alongside the color_map that already existed.

Contour coordinates (#423, #624) — contour(z, x=..., y=...) puts a grid on the caller's own axes, and both contour marks can now be layered, including contour over contourf.

Fixes

  • Theme.legend_position was ignored by the continuous color bar (#618, #622) — ten marks drew it in the right-hand column whatever the setting said. A setting accepted and then ignored is worse than one not offered.
  • Mark.DENDROGRAM was covered by no regression gate at all (#634) — Mark.COUNT was left one short when the mark was added, so five sweeps stopped before reaching it, and the two guards that should have caught that failed in complementary ways: one was satisfied by a second bug, the other named the previous newest mark by hand and rotted. Found while preparing this tag.
  • A facet figure's partial last row was a white hole rather than the figure background (#568, #588).
  • Seams in tripcolor, pcolormesh and hexbin, each now drawn as one fill rather than a shape per cell (#575, #576, #577, #584, #585, #587).

Infrastructure

  • An output digest (#570, #595, #600) — one committed fingerprint per figure across raster and SVG, over every mark and five compositions, so a rendering change has to be deliberate and the diff says which figures moved. It has now answered three canvas releases' "no rendered-output change" claims without anyone writing a throwaway program.
  • CI roughly halved (#605, #606) by folding thirty small test modules into eight families; every module pays the same dependency compilation, so the module count was the cost.
  • canvas_mojo v0.34.0 through v0.37.1, including Path.extend and BoundsTarget, both filed from this repository's needs.

Known open

  • #631 — a PDF is not byte-identical across machines, because it embeds the font program and two packagings of one family differ. Whether an export should promise byte-reproducibility or only the same glyphs is open.
  • #372 — tight bounds (cropping a figure to its ink), now unblocked by BoundsTarget.
  • #535 — an intermittent SIGSEGV, and a timeout variant, in the parallel test run.

v0.11.0

Choose a tag to compare

@randyzwitch randyzwitch released this 08 Sep 23:52

Twenty-eight commits since v0.10.0. Three new marks, a histogram API that grew from one function into eight, four theme presets, and a repository-wide prose cleanup.

Built on canvas_mojo v0.27.0.

Purely additive: nothing was removed from dataviz/__init__.mojo, so existing code keeps working.

New marks

Mark.ECDF (#338, #441) — ecdf(), the empirical cumulative distribution. The step function through the sorted sample, which answers "what fraction of the data is below x" without the bin-width choice a histogram forces on you.

Mark.EVENTPLOT (#339, #445) — eventplot(), a raster of event positions per row. One tick per event, one row per series: the standard way to show spike trains, log events, or anything where when matters and magnitude does not.

Mark.IMSHOW / Mark.PCOLORMESH (#341, #430) — imshow() and pcolormesh(), a 2D array displayed as an image. imshow puts one cell per array element on equal axes; pcolormesh maps the same grid onto explicit x/y edges, so the cells can be unevenly spaced.

Histogram, properly

histogram() existed. It now has an API underneath it (#366, #443, #433, #453) — bin_edges, uniform_bin_edges, shared_bin_edges, histogram_bins, HistogramBins, HistStat, BinRule — covering numeric bin edges, bins shared across several series so their bars line up, per-observation weights, and normalization to density or probability.

Automatic bin selection (#436, #458) came next: BinRule implements the rules numpy's histogram_bin_edges offers — Sturges, Rice, Scott, Freedman-Diaconis, and the auto combination — so the bin count follows the data rather than a guessed constant.

That work also turned up a Mojo language defect, filed upstream as modular/modular#7095: Float64 ** is a low-accuracy approximation — 4.0 ** 0.5 is not 2.0, and 200 ** 0.5 is about 1e6 ULP off libm — which made the Rice rule return 21 bins where numpy returns 20 until it was rewritten to use sqrt/cbrt. Our own sqrt matches libm exactly, so the workaround is solid.

New public API

  • Four theme presets (#429) — dark, minimal, high_contrast, print_safe, importable straight from the package.
  • Plot.annotate_arrow() (#335, #431) — point at a data point from empty space. The label is measured with the real font metrics, so the arrow stops at the text box rather than running under it.
  • Mark.name() (#415, #421) — so a raise can say which mark the caller actually used instead of a bare number.
  • Step interpolation on stacked_area() and Mark.STREAMGRAPH (#413), joining the line and area marks that got it in v0.10.0.
  • render_layers() admits eleven more continuous-frame marks (#376, #428).

Fixes

  • kdeplot() without encode_kde() aborted the process rather than raising (#439, #446). _kde_observations reached for values[0] before checking the outer list was non-empty, turning a catchable "you forgot to call encode_kde()" into an out-of-bounds assert.
  • A facet cell's x-axis title collided with the next row's title (#417, #432).
  • Six British spellings the #386 sweep missed (#438, #444) — the sweep's word list had been built from words already fixed, so it could only confirm what someone had already thought of. Replaced with a suffix-pattern audit.
  • Three docstrings recommended a render_layers() call that raises (#410).

Dependency

canvas_mojo v0.25.0 (#447) then v0.27.0 (#460), crossing three upstream releases. Both v0.26.0 and v0.27.0 expanded the DrawTarget trait — ten primitives to sixteen — which breaks only implementations outside canvas; dataviz consumes the trait generically and implements none.

v0.27.0's one rendered-output change (exact-area coverage for even-odd clip masks on fractionally positioned rectangles) cannot reach here: dataviz has no canvas clip call sites, and every fill rule it passes is NONZERO. Verified with all 144 rendered example outputs byte-identical across the bump.

Also

The published Benchmarks page is gone (#416, #448) and machine-dependent timings are out of docstrings (#449) — they rot, and they are machine-specific; benchmarks/METHODOLOGY.md holds them now, dated and with the machine stated. Docstrings and implementation comments shortened (#450) and a repository-wide prose cleanup landed (#452). First-use docs improved and their examples validated (#459). Bar-combo layers now draw through the shared functions instead of duplicating the geometry (#422, #454).

Full diff: v0.10.0...v0.11.0

v0.10.0

Choose a tag to compare

@randyzwitch randyzwitch released this 07 Sep 21:36
4654907

Nineteen commits since v0.9.0. Four new marks, six new pieces of public API, and six defect fixes — four of which rendered wrongly rather than raising, which is the kind that reaches a reader.

Built on canvas_mojo v0.24.0.

New marks

Mark.KDE / Mark.RUG (#351) — kdeplot() and rugplot(), seaborn's pair. The kernel-density estimate was already computed for violins but could only be drawn mirrored against a categorical axis; it is now a chart in its own right, on shared value/density axes. mark_kde(rug=True) composes the two, because a KDE is smooth everywhere — including where there are no observations at all — and the rug is the honesty check on it.

Mark.TRIPLOT / Mark.TRIPCOLOR (#344) — the Delaunay triangulation, finally drawable. triplot strokes the mesh (every edge once, deduplicated, as subpaths of one Path, so a 20k-point mesh costs one stroke call); tripcolor fills each face flat through the color scale, so Theme(color_ramp=viridis()) works for free.

New public API

  • Theme.color_ramp and dataviz.colormaps (#332) — viridis, magma, inferno, plasma, cividis. Continuous color was three stops, which is enough for a diverging scale and cannot express a perceptually uniform sequential map. Accuracy is measured, not asserted: sampling at all 256 positions against matplotlib 3.11.1 gives a maximum error of 2 levels per channel out of 255 for all five, with exact endpoints.
  • StackBaseline.ZERO and stacked_area() (#337) — the same series a streamgraph stacks, on a flat zero baseline, so the bottom series rests on the axis and the top edge is the running total.
  • LineStyle (#331) — dashed, dotted, dash-dot, in multiples of the stroke's own scale so a dash looks the same at any Theme.scale.
  • StepStyle on mark_line (#336) and mark_area (#384) — PRE/MID/POST, verified byte-identical to matplotlib's pts_to_prestep/midstep/poststep.
  • Theme.show_minor_ticks / show_minor_gridlines (#334) — five subdivisions per major step on a linear axis, the 2..9 multiples on a log one. Both opt-in, so no existing chart moves.

Fixes

  • Heatmap cells could leave a one-pixel background column through the middle of the grid (#379). Two adjacent cells computed their shared boundary by different expressions — (range_min + step*i) + step against range_min + step*(i+1) — which are equal in exact arithmetic and not always the same Float64. At 420×300 with 24 categories they came out 186.99999999999997 and 187.0 and snapped a pixel apart. Found in 3 of 170 swept geometries; 0 after.
  • Calendar month labels ran together into JanFebMar… below about 530px (#361). They are the only thing identifying which column is which month, so the row now thins to every 2nd or 3rd month rather than overlapping.
  • A standalone rugplot drew an empty 0–1 y-axis encoding nothing (#378), and then accepted annotate_line() against that placeholder domain (#389).
  • mark_line(style=...) was silently ignored in a bar-combo layered chart (#383) — a dashed reference line rendered solid, which reads as another data series.
  • _attr_values() could not read an element's first attribute (#387) and returned an empty list rather than failing — a test helper that could not fail, only stop checking.
  • The .pixi symlink was tracked, handing every fresh clone a dangling absolute path (#393).

Also

Theme.raster_supersample now defaults to auto, choosing the factor per mark rather than always 3 (#363); a sunburst reads as a hierarchy instead of a pie (#359); ridgeline curves are outlined so overlapping rows stay distinguishable (#360).

Full diff: v0.9.0...v0.10.0

v0.9.0

Choose a tag to compare

@randyzwitch randyzwitch released this 07 Sep 14:31
761195f

Four new marks, no removals, and the largest performance change the package has had.

Highlights

New marks — Mark.CONTOUR, Mark.CONTOURF, Mark.TRICONTOUR and Mark.TRICONTOURF, with contour(), contourf(), tricontour() and tricontourf() one-call functions. Isolines and filled bands over both a regular grid and scattered samples.

New options — Theme.legend_position places the legend top, bottom or left as well as the default right column; continuous legends can be horizontal; svg_tooltips covers six more categorical marks.

Performance. delaunay() went from O(n²) to near-linear, so tricontour at 20,000 points renders in 34 ms instead of 9.8 s. Six canvas_mojo bumps (v0.16.0 → v0.22.0) took the fixed per-chart cost from 22.5 ms to 5.4 ms, most of it v0.22.0's persistent font cache. Measured against matplotlib on the same data and machine, this release renders 1.7× to 3.8× faster at default settings while drawing 9× the pixels, since raster_supersample defaults to 3.

Rendering. Marks now draw at Float64 coordinates instead of rounding to whole pixels — bars, box, lollipop, candlestick, bullet, waterfall, stacked bar, population pyramid, span/gantt, the annotation overlays, point markers, node-link diagrams, violin/ridgeline/streamgraph paths, and the value-encoding radii in corrplot and punchcard.

Most of that is sub-pixel and invisible at 1×. Three were visible defects, now fixed:

  • a 1px background seam through heatmap and calendar-heatmap cells
  • a pale vertical line where a sankey node met its ribbon
  • corrplot circle sizes quantised to roughly six distinct radii at fifteen variables, which collapsed the encoding the chart exists for

Compatibility

Purely additive. Nothing was removed or renamed: no public export and no Theme parameter disappeared between v0.8.0 and this release, so existing code keeps working. Charts will render slightly differently where the Float64 migration touched them.

Requires canvas_mojo v0.22.0.

What's Changed

Full Changelog: v0.8.0...v0.9.0

v0.8.0

Choose a tag to compare

@randyzwitch randyzwitch released this 04 Sep 19:11

What's Changed

Full Changelog: v0.7.0...v0.8.0

v0.7.0

Choose a tag to compare

@randyzwitch randyzwitch released this 03 Sep 19:19
c6c9553

Breaking change: per-mark geometry moved off Theme

Theme had grown to 67 fields, a third of them geometry only one mark ever read. Those 17 now belong to the mark that reads them (#193). Per-mark colors stayed on Theme, so themes can still restyle them.

# before
gauge(v, theme=Theme(gauge_start_angle=pi, gauge_sweep_angle=pi))
pie(cats, vals, theme=Theme(donut_inner_radius_fraction=0.55))

# after
gauge(v, start_angle=pi, sweep_angle=pi)
pie(cats, vals, inner_radius_fraction=0.55)

# unchanged -- a theme still restyles these
Theme(treemap_label_color=WHITE, waterfall_total_color=SLATEGRAY)

Moved: donut_inner_radius_fraction, bullet_measure_width_fraction, waterfall_delta_width_fraction, chord_ring_fraction, radialbar_ring_gap_fraction, radar_grid_rings, violin_width_fraction, corrplot_bubble_fraction, gauge_band_inner_fraction, gauge_needle_fraction, gauge_start_angle, gauge_sweep_angle, ridgeline_overlap, polar_bar_padding, polar_grid_rings, polar_grid_spokes, sankey_node_width.

Highlights

SVG hover tooltips (#199, #200). Each datum gets a <title> a browser shows on hover, via canvas_mojo v0.13.0's annotated groups. On by default for the categorical marks, where the whole docs corpus grows only 1.02x. Opt-in via tooltips=True for scatter/effect-scatter/beeswarm, where a title costs about as much as the point element itself.

Beeswarm no longer quadratic (#188). _beeswarm_offsets was a selection sort. At 32,000 points: 3,707 ms → 131 ms, byte-identical output.

CI is much faster. The test suite went from 6,985 to 1,339 CPU-seconds by merging 86 test files into 13 (#189), and the docs build from 9,787 to 688 by batching the 117 example and recipe programs into 8 (#191). Neither changes what is verified: the same 600 tests, and all 120 rendered images byte-identical.

Less duplicated rendering code (#194, #196). The horizontal and vertical variants of each categorical mark shared 379 near-identical lines; _Orientation brought that to 165 by isolating the handful of places band and value become x/y pixels.

What's Changed

Full Changelog: v0.6.0...v0.7.0

v0.6.0

Choose a tag to compare

@randyzwitch randyzwitch released this 02 Sep 22:36
7a610ab

What's Changed

  • Add Plot.encode()'s labels channel for Mark.POINT/EFFECT_SCATTER data labels by @randyzwitch in #165
  • Phase 1 of #158: Float64Sequence/StringSequence + Plot.encode()'s array-like x/y overload by @randyzwitch in #166
  • Add numpy/pandas adapter: Plot.encode()/encode_categorical() accept PythonObject by @randyzwitch in #168
  • Bump canvas_mojo to v0.10.1 by @randyzwitch in #169
  • Add StringSequence container axis to encode_categorical()/encode_grouped_bar() by @randyzwitch in #170
  • Add DType overloads to 25 quickplot functions + sweep examples to List[Int] (#167) by @randyzwitch in #171
  • Add DType-generic overloads for nested List[List[Float64]] data by @randyzwitch in #172
  • Add DType-generic overload for encode_radar()/radar()'s max_values by @randyzwitch in #176
  • Add Plot.mark_bar(horizontal=True)/bar(horizontal=True): horizontal bar charts by @randyzwitch in #177
  • Rename the Mojo import name from dataviz_mojo to dataviz by @randyzwitch in #178
  • Add horizontal orientation to Mark.LOLLIPOP by @randyzwitch in #179
  • Add horizontal orientation to Mark.GROUPED_BAR and Mark.STACKED_BAR by @randyzwitch in #180
  • Add horizontal orientation to Mark.BOX, Mark.VIOLIN, and Mark.BEESWARM by @randyzwitch in #181
  • Bump canvas_mojo to v0.11.0 (internal package renamed to canvas) by @randyzwitch in #182
  • Simplify: dedupe bar-color and grouped/stacked-bar legend logic by @randyzwitch in #183
  • Add 24 Cookbook recipes exercising previously-undemonstrated Theme flags by @randyzwitch in #184
  • Add point-shape markers for black-and-white multi-series charts by @randyzwitch in #185
  • Bump canvas_mojo to v0.12.0 by @randyzwitch in #186
  • Bump version to 0.6.0 by @randyzwitch in #187

Full Changelog: v0.5.0...v0.6.0