Repository navigation
Releases: randyzwitch/dataviz_mojo
Release list
v0.15.0
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 aDisplayListholding 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 aCanvas,SvgCanvasorPdfCanvas. For every mark, the replay matchesrender()pixel for pixel andrender_svg()markup for markup. A figure with math labels, annotations and tooltips also matchesrender_pdf()byte for byte. PassOutputFormat.SVGorPDFwhen 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, callsize()and record again. To matchrender()on a raster canvas, replay betweenbegin_supersampled()andend_supersampled(). The new section in the output-formats guide shows how.pixi run bench-recordreports 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, usechart.erased_with[display_list=True](); recording anerased()chart raises and names that fix.- Drawing an
AnyChartinto 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
Canvastakes about 3x as long asrender(): 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): therestore()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 usepixi 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
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
maxchannel 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,Figureand the inset entry points take aList[AnyChart]: wrap each chart asAnyChart(chart)orchart.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 nowstepped_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_palettetotab10_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,.webpand the rest used to get SVG or PNG bytes under the wrong name. A path with no extension still usesTheme.output_format. -
Mark(n)is gone (#860). Marks come only from the named constants, such asMark.POINT. -
dataviz.plotexports onlyPlot(#831). Import other names fromdataviz, 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()withList[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).HatchStylehas 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()andfill_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()takesx_err, orx_err_lowerandx_err_upper, on points, lines and effect scatter. A point with both draws a cross. shared_x_scale(#846) on facets and grids, alongsideshared_y_scale.- Named palettes (#792): Tableau 10 and Paul Tol's bright, muted and light sets.
- Abbreviated tick labels (#789):
TickFormat.AUTOwrites 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
TensororBuffer. - 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 offPlot(#826, #832–#834), and the marks were added as types (#835) beforePlotwas 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/coreimports nothing above it, anddataviz.plotexports 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
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()andpairplot_pdf()are gone (#697). The three composites return an unrenderedFigureinstead, 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_tooltipsis replaced byTheme.tooltips(#700), a three-wayTooltips.AUTO/ON/OFFpolicy rather than a Bool.AUTOemits tooltips up toAUTO_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.FORCEbeside the existing circular one. - Tight cropping (#372) —
render_tight()andsave(..., tight=True)trim a figure to its ink. - Log-space binning and density (#720) —
hexbin,hist2dand 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,ECDFandPCOLORMESH, 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 formatcrashed on a shared grammar cache between workspaces; each workspace now has its own (#733).- Forty-five
mojo docwarnings, unreported becausemojo docexits 0 regardless (#756). Five of the new DataFrame overloads had anArgs: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 underdataviz/.
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.COUNTfails 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.jpgand.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-aarch64build. - #746 — an intermittent segfault in the parallel test suite, roughly one run in four.
v0.12.0
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_positionwas 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.DENDROGRAMwas covered by no regression gate at all (#634) —Mark.COUNTwas 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,pcolormeshandhexbin, 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.extendandBoundsTarget, 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
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()andMark.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()withoutencode_kde()aborted the process rather than raising (#439, #446)._kde_observationsreached forvalues[0]before checking the outer list was non-empty, turning a catchable "you forgot to callencode_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
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_rampanddataviz.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.ZEROandstacked_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 anyTheme.scale.StepStyleonmark_line(#336) andmark_area(#384) —PRE/MID/POST, verified byte-identical to matplotlib'spts_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) + stepagainstrange_min + step*(i+1)— which are equal in exact arithmetic and not always the sameFloat64. At 420×300 with 24 categories they came out186.99999999999997and187.0and 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
.pixisymlink 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
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
- Share one FontCache across each categorical render by @randyzwitch in #265
- Use Color.with_alpha in _lighten by @randyzwitch in #266
- Fill closed marks with FillRule.NONZERO by @randyzwitch in #267
- Record pixi run bench across the 0.13.0/0.14.0/0.15.0 canvas pins (#257) by @randyzwitch in #268
- svg_tooltips on six more categorical marks by @randyzwitch in #269
- CI: build the package and compile a consumer against it by @randyzwitch in #270
- Tests: property-style sweeps for the scale math and decimation by @randyzwitch in #271
- Share one FontCache across measuring and drawing in every render (#255) by @randyzwitch in #272
- Mark.CONTOUR: isolines over a regular grid by @randyzwitch in #273
- Tests: raster/SVG layout equivalence for every mark by @randyzwitch in #274
- Theme.legend_position: top, bottom and left, not only the right column by @randyzwitch in #275
- Cover Mark.CONTOUR in the equivalence sweep, and catch the next gap by @randyzwitch in #276
- Tests: structural SVG assertions instead of substring matching by @randyzwitch in #277
- Mark.CONTOURF: filled bands between contour levels by @randyzwitch in #278
- Tests: locate features by scanning, starting with the facet layouts by @randyzwitch in #279
- Horizontal continuous legends, finishing #211's TOP/BOTTOM by @randyzwitch in #280
- Bump canvas_mojo to v0.16.0 by @randyzwitch in #281
- Draw polar grid rings with the trait's draw_circle_aa by @randyzwitch in #282
- Drop _LazyFontCache now that FontCache is lazy upstream by @randyzwitch in #283
- Tests: locate the bar-sign features by scanning too by @randyzwitch in #284
- Benchmarks page: add the 0.16.0 column and correct the 0.15.0 SVG cause (#257) by @randyzwitch in #285
- Mark.TRICONTOUR: isolines over scattered samples by @randyzwitch in #286
- Docs: move cookbook_recipes out of the published source tree by @randyzwitch in #287
- Bump canvas_mojo to v0.17.0 by @randyzwitch in #288
- Calendar-aware ticks for a day-resolution temporal axis (#195) by @randyzwitch in #289
- Tests: locate the last hand-derived features by scanning too (#218) by @randyzwitch in #290
- Dev-loop compile time: measure the four #229 ideas, add test-changed by @randyzwitch in #291
- Bump canvas_mojo to v0.18.1 and adopt what moved upstream by @randyzwitch in #292
- Supersample through the canvas transform instead of scaled metrics by @randyzwitch in #293
- Move the named-color raster check to test_marks_basic.mojo by @randyzwitch in #295
- Bump canvas_mojo to v0.19.0 by @randyzwitch in #296
- Move Mark.BAR onto Float64 coordinates (1 of N) by @randyzwitch in #297
- Bump canvas_mojo to v0.20.0 by @randyzwitch in #299
- Move Mark.LOLLIPOP onto Float64 coordinates (2 of N) by @randyzwitch in #300
- Move Mark.BOX onto Float64 coordinates, and snap thin lines (3 of N) by @randyzwitch in #301
- Move Mark.WATERFALL onto Float64 coordinates (4 of N) by @randyzwitch in #302
- Bump canvas_mojo to v0.20.1, fixing the #298 flake by @randyzwitch in #307
- Move Mark.CANDLESTICK and Mark.BULLET onto Float64 coordinates (5 and 6 of N) by @randyzwitch in #303
- Move Mark.STACKED_BAR onto Float64 coordinates (8 of N) by @randyzwitch in #308
- Move Mark.POPULATION_PYRAMID onto Float64 coordinates (9 of N) by @randyzwitch in #309
- Stop rounding path coordinates in violin, ridgeline and streamgraph by @randyzwitch in #310
- Move Mark.SPAN_CHART and Mark.GANTT bars onto Float64 coordinates by @randyzwitch in #311
- Draw point markers at their data position, not the nearest whole pixel by @randyzwitch in #312
- Move the four annotation overlays onto Float64 coordinates by @randyzwitch in #313
- Fix the hairline seam between heatmap and calendar-heatmap cells by @randyzwitch in #315
- Stop rounding the radii that encode a value in corrplot and punchcard by @randyzwitch in #316
- Draw node-link diagrams at their laid-out positions by @randyzwitch in #317
- Close the seam where a sankey node meets its ribbon by @randyzwitch in #318
- Draw a calm wind barb where its data point is by @randyzwitch in #319
- Split the annotate_* overlays out of plot.mojo into annotations.mojo by @randyzwitch in #320
- Split plot.mojo along its remaining seams: 8,598 → 4,168 lines by @randyzwitch in #321
- Make delaunay() near-linear: 20,000 points from 9.8s to 34ms by @randyzwitch in #325
- Bump canvas_mojo to v0.21.0 by @randyzwitch in #328
- Add Mark.TRICONTOURF: filled contour bands over scattered samples by @randyzwitch in #327
- Bump canvas_mojo to v0.22.0 by @randyzwitch in #330
- Bump version to 0.9.0 by @randyzwitch in #358
Full Changelog: v0.8.0...v0.9.0
v0.8.0
What's Changed
- First pass re-writing docstrings by @randyzwitch in #202
- Re-write test comments and docstrings for tests/ by @randyzwitch in #203
- Wire annotate_band/vline/point/best_fit into facets and layers by @randyzwitch in #232
- Fix _format_fixed/_label_decimals overflow above 2^53 by @randyzwitch in #233
- Raise on empty data instead of rendering a blank canvas by @randyzwitch in #234
- Make Plot Copyable so a base plot can be cloned into variants by @randyzwitch in #235
- Add a benchmark script for large inputs by @randyzwitch in #240
- Document that the unconditional PythonObject import has no consumer cost by @randyzwitch in #239
- Add a version consistency check and a release script by @randyzwitch in #238
- Drop the mut requirement on render/save/render_facets/render_layers by @randyzwitch in #236
- Make the raster supersample factor a Theme knob by @randyzwitch in #237
- Reformat source with mojo format by @randyzwitch in #242
- CI: add a mojo format check by @randyzwitch in #243
- Add a real description to each chart function's docs hook by @randyzwitch in #245
- Raise on non-finite (NaN/inf) values in _min_max() by @randyzwitch in #244
- Rotate long x-axis category labels when they would overlap by @randyzwitch in #246
- Error bars on Mark.BAR and Mark.GROUPED_BAR by @randyzwitch in #247
- save() emits accessible SVG markup automatically when a title is set by @randyzwitch in #248
- Log scales in render_layers() and shared-scale render_facets() by @randyzwitch in #249
- Tick label formatting options: percent, thousands, SI, scientific, fixed by @randyzwitch in #250
- Explicit axis domain overrides (x/y limits) for continuous marks by @randyzwitch in #251
- Per-series name and legend for render_layers() by @randyzwitch in #252
- Theme.show_data_labels on Mark.LOLLIPOP/WATERFALL/BULLET/POPULATION_PYRAMID by @randyzwitch in #253
- Bump canvas_mojo to v0.14.0 by @randyzwitch in #263
- Bump canvas_mojo to v0.15.0 by @randyzwitch in #264
- Mark.BARBS: wind barbs over a continuous x/y vector field by @randyzwitch in #262
Full Changelog: v0.7.0...v0.8.0
v0.7.0
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
- Fix O(n^2) beeswarm layout; run tests from a glob by @randyzwitch in #188
- Merge the 86 test files into 12 family modules by @randyzwitch in #189
- Batch the docs example/recipe programs for CI verification by @randyzwitch in #191
- Remove dead code and unused imports; silence both build warnings by @randyzwitch in #192
- Move 23 per-mark style fields off Theme onto their mark by @randyzwitch in #193
- Share the horizontal/vertical drawing loops via _Orientation by @randyzwitch in #194
- Share the remaining three mirror pairs via _Orientation by @randyzwitch in #196
- Bump canvas_mojo to v0.13.0 by @randyzwitch in #197
- Import LinearScale/OrdinalScale explicitly instead of implicitly by @randyzwitch in #198
- Add SVG hover tooltips via canvas_mojo's annotated groups by @randyzwitch in #199
- Add an opt-in for tooltips on the point-per-datum marks by @randyzwitch in #200
- Bump version to 0.7.0 by @randyzwitch in #201
Full Changelog: v0.6.0...v0.7.0
v0.6.0
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