Skip to content

Releases: jrvannucci/plotpress

0.43.0

Choose a tag to compare

@jrvannucci jrvannucci released this 23 Sep 22:39

Changed

  • Grouped style.py, colors.py, ticker.py, and dates.py into
    plotpress/style/
    -- the "how a value becomes appearance" cluster
    (Style, colormaps, tick locating/formatting, its datetime extension).
    style.py becomes style/__init__.py (it names the package); colors.py,
    ticker.py, dates.py become plain siblings with no re-export. The
    genuinely public API this exposes (plotpress.Normalize, plotpress. register_cmap, etc.) is unaffected -- it's re-exported through
    plotpress/__init__.py's existing lazy-attribute layer, which only needed
    its internal path strings updated. Code importing plotpress.colors/
    .ticker/.dates directly needs the style.-prefixed path.
  • Moved qt.py into plotpress/gui/ and png.py into
    plotpress/backends/
    -- png.py is only ever used by the render
    backends (SVG/Vega/Vega-Lite embedding PNG data URIs), never by axes.py
    or anything outside backends/.
  • Grouped the remaining top-level modules into backends/ and core/.
    svg/, raster.py, vega.py, vega_lite.py, and _interactive.py
    (+ _js/) moved into plotpress/backends/ -- every format a Figure
    renders itself to, matching AGENTS.md's own render-pipeline diagram.
    artists.py, primitives.py, and transform.py moved into
    plotpress/core/ -- the shared scene/geometry layer every backend reads
    from. Neither new package re-exports (unlike figure//svg/'s own
    __init__.pys, which preserve their pre-split single-file import path on
    purpose): these were already independent top-level modules, so there is no
    single prior path to preserve, and the new one is plotpress.backends.svg/
    plotpress.core.artists/etc. This does change any code importing
    plotpress.svg/plotpress.raster/plotpress.vega/plotpress.vega_lite/
    plotpress._interactive/plotpress.artists/plotpress.primitives/
    plotpress.transform directly (all internal-facing, underscore-prefixed
    members throughout) -- update those imports to the backends./core.
    prefixed path.
  • Moved the package to a src/ layout. plotpress/ now lives at
    src/plotpress/ rather than the repo root, so an accidental import plotpress while developing can't silently resolve to the working tree
    instead of the actually-installed package. No public API or import path
    changed (import plotpress is identical); this only moves where the
    source lives on disk. If you have a local editable install, re-run pip install -e . after pulling this.
  • figure.py split into plotpress/figure/. The root object had grown to
    5000+ lines; it's now six submodules -- _core (Figure itself, plus the
    grid/group machinery it's genuinely mutually coupled to), the two leaf
    modules it depends on (_layout, _html_options), and three things built
    from a Figure rather than needed by it (_template, _report,
    _io) -- all re-exported from plotpress/figure/__init__.py. No public
    API changed.
  • svg.py split into plotpress/svg/. The SVG backend had grown to
    3300+ lines across 90 top-level functions with no classes to group
    them; it's now eight submodules by concern (formatting, ticks/frame,
    text/annotations, legend, per-artist rendering, Figure.group() boxes,
    the entry point, and the interactive-HTML metadata payloads), all
    re-exported from plotpress/svg/__init__.py so every existing
    from plotpress.svg import X keeps resolving unchanged. No public API
    changed.
  • The interactive toolbar's JS moved out of _interactive.py into
    plotpress/_js/*.js.
    The Python file had grown to ~5650 lines, nearly
    all of it one JS string literal that had roughly doubled this session
    from the Slice tool alone. The JS is now split into ten real .js files
    by tool/feature (toolbar/pan-zoom, mode buttons, Slice, ticks/pins,
    Extract, Save, sliders, ...), assembled into the same script at import
    time -- verified byte-for-byte identical to the pre-split output. No
    public API changed.

Fixed

  • set_aspect/set_box_aspect reject a non-positive value. aspect=0
    used to crash the whole render with a ZeroDivisionError; a negative
    aspect silently corrupted that axes' geometry instead. Both now raise
    ValueError at the call site.
  • An explicit, non-positive limit on a log axis now raises, instead of
    crashing or silently blanking the axis.
    set_ylim(-5, -1) on a
    log-scaled axis used to crash tick generation with ValueError: math domain error; set_xlim(-1, 100) used to silently NaN-blank the whole
    axis with no error. Both close at the same shared validation point,
    regardless of the order set_xscale/set_xlim/set_ylim were called in.
    A one-sided limit (the open end left to autoscale, e.g. set_ylim(bottom= -5)) used to slip past this same check and still silently blank the
    axis -- it is now validated per end, not only when both ends are set.
  • violinplot()/kdeplot() no longer crash on a constant-valued sample
    once it's large enough (>= 4000 points) to use the binned KDE estimator --
    the degenerate grid produced NaN positions that crashed np.bincount.
  • get_cmap()/register_cmap() no longer hand out the shared colormap
    array by reference.
    Mutating a value returned from either function, or
    a LUT already passed to register_cmap(), used to silently corrupt that
    colormap for every other figure/artist in the process; both now always
    return/store a copy, on every path (registering a colormap and then
    mutating what it handed back used to still corrupt the registry).
  • Huge-line decimation no longer lets a NaN mask a real peak. A pixel
    column containing both a genuine extreme value and a NaN (a common
    shape for a real time series with missing samples) used to pick the NaN
    as that column's rendered maximum, silently dropping the true peak.
  • Figure.to_vega()/to_vega_lite() now honor errorbar(..., errorevery=)
    and export a mapped marker shape (square/diamond/the four
    triangle_*) instead of always rendering a circle. A marker shape with no
    native Vega/Vega-Lite equivalent (+/x/|/_) still falls back to a
    circle, now with a caveat naming the axes instead of silently.
  • hist() now ignores non-finite values, matching every sibling
    distribution method (kdeplot/ecdfplot/rugplot/boxplot/
    violinplot) -- it used to raise a bare, context-free NumPy error on a
    NaN in the data. It also now warns when a dataset had no finite values at
    all and was dropped entirely, matching boxplot()/violinplot() below.
  • boxplot()/violinplot() warn when a dataset is dropped entirely
    (all its values were non-finite) instead of silently rendering one fewer
    box/violin with no indication anything was missing.
  • Figure.print_layout_summary()/Axes.print_summary() no longer report
    a false "OK"
    when to_vega()/to_vega_lite() actually raised -- the
    crash now shows up as a gap in the summary instead of being swallowed.
    Axes.print_summary() now also flags when there is a figure-level export
    gap it can't attribute to that specific axes, rather than only checking
    its own axes and missing one it can't name.

Documentation

  • Several docs gallery scripts and cross-references still pointed at
    pre-reorganization module paths
    (plotpress.raster, plotpress. artists, and stale :mod:/:class:/:func: roles across several
    docstrings) after the moves above. None were caught by this session's own
    earlier docs-build checks, because sphinx-gallery caches each script's
    execution by a hash of its own source text -- a script whose only problem
    was its import target moving elsewhere never re-executed, and kept
    reusing its stale pre-move result build after build. Fixed, and confirmed
    this time with every gallery cache wiped first so every script actually
    re-ran.

0.42.4

Choose a tag to compare

@jrvannucci jrvannucci released this 22 Sep 02:03

One Slice fix on top of 0.42.3.

Fixed

  • Slice: Point Picking pins no longer float over the "profile replaces heatmap" view. A pin anchored in an axes' own data space kept its old pixel while that view stood in for the data, so a reading of x=0.292, y=0.125, z=1.212 sat at the y=0.125 pixel of an axis that had started reading in the slice's value, pointing at a mesh (or, since 0.42.3, a line) that is not on screen. Annotations anchored the same way did the same. They are now hidden with the series they belong to and restored with them -- on a view change, or on switching Slice off. Pins placed on the profile itself stay, since the thing they point at is what is showing. Hidden, never deleted: Extract still carries them, and they come back untouched. This predates 0.42.3, which only widened it from mesh pins to line and scatter pins as well. A pin selected with an arrow key active is also deselected when it's hidden this way, rather than staying the live target of arrow-key stepping while invisible.

Full Changelog: 0.42.3...0.42.4

0.42.3

Choose a tag to compare

@jrvannucci jrvannucci released this 22 Sep 00:23

One Slice fix on top of 0.42.2.

Fixed

  • Slice: the "profile replaces heatmap" view no longer leaves overlaid series stranded on an axis that has changed meaning. That view re-labels the vertical axis in the slice's value, and hid the mesh but nothing else drawn on the same axes -- so a plot() line at y=0.5, or a scatter() at y=0.25, kept its old pixel position while the ticks beside it started reading in z, showing a value it never had (a line marking y=0.5 sat at 0.41 on the new axis). The same rect was also claimed whole by the profile's own hit-test, so those series could not be picked while the view was on, and a click on one reported the mesh underneath at a shifted x. Everything the axes draws in its own data space is now hidden with the mesh and restored with it -- switching views, or switching Slice off entirely. The companion and cursor views are untouched: they keep the heatmap and the axis still reads in y, so an overlay is still meaningful there and still picks. Note this is deliberately all-or-nothing: for an X slice the horizontal axis does survive, so a strictly vertical reference line would still have been readable, but singling those out would hide one series and keep another for reasons invisible to the reader.

Full Changelog: 0.42.2...0.42.3

0.42.2

Choose a tag to compare

@jrvannucci jrvannucci released this 21 Sep 12:53

Two layout fixes on top of 0.42.1.

Fixed

  • subplot_size= now works without an explicit tight_layout() call. Only tight_layout() measures the decorations the solve works around, and the render-time re-fit skipped any figure that had never had it called by hand -- so subplot_size= quietly did nothing for a caller who just built the figure and saved it, which is exactly what its own documentation said would work. Asking for a subplot size now arms that re-fit. subplots_adjust() still overrides it, since that sets the margins directly and leaves nothing to solve, and so does set_size_inches(): an explicit figure size is the caller taking control back, and the pending solve would otherwise resize the figure away from it on the next render.
  • A wrong-typed group= now says so. remove_group() and set_group_visible() accepted anything non-None in their group= slot and only failed later, somewhere else: set_group_visible(grp, False) -- a natural order to guess, since remove_group() really does take the group first -- bound group=False and died on 'bool' object has no attribute '_raw', and remove_group("G0") bound the title positionally and died on 'str' object has no attribute 'flat_axes'. Both now raise a TypeError naming what was passed, and the string case suggests title= instead.

Full Changelog: 0.42.1...0.42.2

0.42.1 — Slice pin and config fixes

Choose a tag to compare

@jrvannucci jrvannucci released this 21 Sep 03:34

Fixed

  • Slice: rebuilding the sliders no longer throws away the profile pins.
    buildSliceSliders() tears the sliders down and rebuilds them on every
    change, and that teardown deleted every pin placed on a companion strip -- so
    toggling "Link all matching axes" or changing which axes are sliced silently
    lost them, although neither changes what a pin points at. The two cases that
    do are handled where they happen: switching Slice off (no profile left to
    point at) and changing orientation (a pin's index means a sample along the
    other axis afterwards). An axes leaving the scope still drops its own.
  • Slice: a null in the embedded option config no longer reads as a number.
    isFinite(null) is true in JavaScript and +null is 0, so panel_size,
    range_min/range_max and index each took a null as a real value --
    panel_size: null collapsed the companion strip onto its minimum size
    instead of the 0.3 default. Figure.to_html() rejects a null before it can
    get there, so this only reached a saved page edited by hand, but the config
    is read the same way either way.

0.42.0 — size by the subplot, not the figure

Choose a tag to compare

@jrvannucci jrvannucci released this 20 Sep 23:46

Added

  • subplot_size= on subplots()/subplots_from_groups() -- give the size
    of one subplot in inches and have figsize solved for it, instead of sizing
    the whole figure and discovering what that left each panel::

    fig, axes = plotpress.subplots(20, 25, subplot_size=(1.2, 0.9))
    

    A readable panel is a fixed size, so on a large grid what a caller knows is
    how big one panel should be -- not what 500 of them plus their tick labels,
    titles, colorbars, group boxes and supxlabel add up to.
    figsize=(ncols * 1.2, nrows * 0.9) is the usual guess and is always wrong,
    because none of that surrounding furniture scales with the grid the way the
    panels do. The room it needs is measured and added on top, so the panels come
    out the size asked for. tight_layout() does the solving, so the answer
    reflects the labels actually set rather than a guess made before they
    existed. A colorbar, a group box or a suptitle grows the figure rather than
    eating into the panels -- subplot_size is the size of the plotting box,
    not of the grid cell the colorbar is carved out of.

Fixed

  • The global slider bar could grow off the top of the window. "Link all
    matching axes" makes one global slider per compatible group, so a figure
    whose meshes come in many different grid shapes stacks many of them in the
    bar fixed to the bottom of the window. Nothing bounded that column: 24 groups
    made a 1319px bar in a 900px viewport, and since a position: fixed element
    does not scroll with the page, the eight sliders above the top edge could not
    be reached at all. The bar is now capped to the room it actually has and
    scrolls inside itself, and re-caps when the window resizes.

Documentation

  • A worked example for subplot_size=, which opens by multiplying out a
    figsize the obvious way and printing what the panels actually came to
    (1.00 x 0.45in where 1.40 x 1.10 was intended), then states the same
    requirement with subplot_size= and holds it across three grid sizes and a
    per-axes colorbar. The 500-panel grouped demo and the 500-panel scale example
    now use it too -- both were hand-multiplying figsize=(NCOLS * 1.6, NROWS * 1.6), the pattern this replaces, and neither was getting the panel
    size it named.

0.41.1 — tick, menu and gallery fixes

Choose a tag to compare

@jrvannucci jrvannucci released this 20 Sep 15:25

Documentation

  • README and docs homepage corrected and tidied. Both still described the
    toolbar as it stood before the Slice tool and options= existed; both are now
    documented. Several claims had drifted and are fixed: 24 built-in colormaps
    (the list beside it already named 27), "a hundred-odd" real-application
    figures (there are over 160), "one font-metric family" (four are bundled), and
    a homepage claim that 3-D axes project onto the 2-D core -- there is no 3-D at
    all; polar is the thing that projects. barbs was listed as upcoming though it
    ships, and the plot-types table was missing it along with ecdfplot,
    kdeplot and pcolormesh_frames. "Supported plot types" is rewritten from a
    single run-on paragraph into one table per grouping, the Roadmap section is
    gone, and a new GIF shows the Slice companion panel driven by one coupled
    slider across two panels.

Changed

  • Gallery figures no longer open with Slice switched on. Every tool is still
    offered in the toolbar; none starts active. Slice reshapes the axes it runs on
    (carving out a companion strip, docking a slider), so starting it enabled meant
    a reader met a modified figure before choosing to modify it. The five examples
    that specifically demonstrate Slice still turn it on themselves.

Fixed

  • A toolbar menu could open past the edge of an embedded figure. A figure
    embedded in a page -- a Report panel, a docs gallery iframe -- gets whatever
    height the host gave it, and the Slice menu is the tallest of them. With
    nothing bounding it, the bottom of the list fell outside the document: clipped
    away rather than scrolled off, since an iframe has no viewport of its own to
    scroll, leaving those options unreachable. An open menu is now measured and
    kept inside the viewport -- scrolling internally, or flipping above its button
    when that side has more room -- and the same clamp keeps a menu near the right
    edge on screen.

  • Tick decades are computed from the decimal literal, not pow. Neither
    Python's ** nor JavaScript's Math.pow is required to return the correctly
    rounded power of ten, and both miss: 10.0**23 and 10.0**126 differ from
    their literals, and some JavaScript engines return a different double for
    10**-5 than others. On an engine that rounds it the other way, an axis over
    [1, 1.0001] lost its first tick entirely once zoomed -- ceil(vmin / step)
    landed on the next integer -- so the interactive view disagreed with the ticks
    the static render had already drawn. Both sides now build a decade by parsing
    the decimal literal, which is correctly rounded by spec in both languages.

0.41.0 — Interactive Slice tool

Choose a tag to compare

@jrvannucci jrvannucci released this 20 Sep 13:34

Added

  • Interactive Slice tool for pcolormesh/imshow (options=["slice"]).
    Scrub any row or column of a mesh as a 1-D profile with a play/step
    slider, toggle between the heatmap (with a dashed cursor) and the profile,
    fix the value axis to auto / colorbar / custom bounds, and couple every
    mesh sharing the same grid to one global slider with "Link all matching
    axes" -- built for figures with hundreds of meshes, where per-axes link
    checkboxes are not workable. Off until "Enable Slice" is checked; curvilinear
    meshes are excluded (no single row/column to slice).

  • Modular toolbar via options=. to_html/save/show/
    show_in_jupyter/Report.save (and PlotPressWidget) take
    options=[...] for tools beyond the core toolbar (Pan/Zoom, Home, Fit
    Width, Axes, Point Picking, Annotate, File), which every page keeps.
    Nothing existing changes unless asked for.

  • Slice companion panel. The Slice menu's profile can be drawn in a
    strip carved out of the mesh's own axes (above it for an X slice, to its
    left for a Y slice), beside the heatmap and aligned through every pan/zoom,
    instead of replacing it. A radio in the menu switches between that
    (the default), the profile replacing the heatmap, and a cursor-only heatmap.
    Point Picking works on the strip -- a click pins the nearest profile sample
    (x, y, value), which follows the slider and pan/zoom, steps with the
    arrow keys (staying on the strip's edge, value in red, if it goes off the
    range) -- and Snap pins to
    slice
    (a checkbox in the menu's own "Slice view" group) links pins in both
    directions: a heatmap pin gets a mirror on the shown profile, and a pin placed
    on the profile gets one on the heatmap cell it points at. Extract ignores profile
    pins and reports the heatmap cell instead (never the same cell twice). When profile pins are left out
    (Snap off), the Extract panel warns and points to Snap pins to slice. The original stays
    put and each pair shares a label color.
    A colorbar attached to a sliced mesh shrinks to the heatmap's share of its axes
    when the strip is above it, so the two stay aligned.

  • A colorbar shared by several axes shrinks with their heatmaps when every one of
    them gets a strip (it stays as drawn if only some do).

  • Save / Save As keep the Slice state. A saved page reopens with Slice enabled,
    in the same view and orientation, on the same chosen axes, with the same link/snap
    settings, value range and slider position, and with its profile pins.

  • Point Picking works on the "Profile replaces heatmap" view too. A click picks a
    profile sample instead of a hidden heatmap cell, and pins carry across a switch
    between the two profile views.

  • The companion strip now works on more axes. Axes with fixed ticks
    (set_xticks/set_yticks), axis("off"), and axes with a twinx/twiny
    or secondary axes (which shrink together with their parent) get a strip too;
    their static ticks are remapped to the smaller heatmap. An inset axes is left
    out of Slice altogether, and the axes it sits on still gets just the cursor and
    slider. Also
    fixes a twin's ticks being redrawn on the wrong edge (and its parent's axis
    doubled) when the view changed.

  • Choose which axes to slice. The Slice menu has an "Axes to slice" radio --
    All axes or Selected axes -- where selecting axes is done by clicking them on the
    figure; only the chosen axes get a slider, cursor and strip,
    and "Link all matching axes" couples just those. The axes startup setting takes
    the axes (or indices) to start with.

  • Startup settings for a tool. Pass options as a dict to start a tool in
    a chosen state -- e.g. options={"slice": {"enabled": True, "view": "companion", "orientation": "y", "link_all": True, "range": "colorbar"}} --
    rather than switched off; values are validated up front.

  • Gridlines on the Slice profile. A "Gridlines on profile" checkbox (and a
    grid startup setting, default on) draws light lines on the strip and on the
    profile-replaces-heatmap view at both the value ticks and the heatmap's own ticks.

  • Docs: Slice examples. Four examples in the gridded-data gallery (a heatmap
    row, columns with the replace view, linked meshes, chosen axes) and one at scale
    (a thousand meshes). Every live pcolormesh/image figure in the plot-type
    reference and real-application galleries now opens with Slice enabled, and an
    example can set its own state with _gallery_interactive_options.

Performance

  • Slice on a figure with ~1000 pcolormeshes. Enabling Slice went from ~14 s
    (without "Link all") to under a second, a linked slider step from ~3 s to under
    0.1 s, and view/orientation switches from 7-9 s to under 0.5 s. Choosing a subset
    of axes stays at tens of milliseconds per step. The cost was layout thrashing
    (measuring while writing) and per-axes scans of the whole document.

Changed

  • The Extract panel now shows JSON by default, with CSV/JSON radios to switch
    the text, and its "Copy CSV" button is now "Copy" (it copies whichever format is
    showing). The two download buttons are unchanged.
  • New logo, docs sidebar image and favicon. Refreshed artwork for the three
    images already wired in (assets/plotpress-logo.png, docs/_static/logo.png,
    docs/_static/favicon.png); nothing that references them changed.

Fixed

  • Slice: a strip could take more than its own axes. The strip's 28px
    legibility floor ignored how tall the axes actually was, so on a stack of short
    panels (14 rows in a 5in figure is ~23px each) it claimed more than the whole
    rect and left the heatmap with a negative one -- which draws nothing at all,
    silently, since a negative width/height raises no error. The floor now gives way
    on an axes too small to hold both.
  • Slice: an all-NaN row showed the previous row's profile. Under "Profile
    replaces heatmap", stepping onto a row with no values left the last row's line on
    screen: the slider read row 4 while the profile plotted row 3. A pin on such a
    slice likewise stayed visible, still reporting the value it last read.
  • Slice no longer fights the legend. Slice and the legend's click-to-hide
    toggle both wrote a mesh's own style.display, so a slider step brought back a
    mesh the reader had just hidden. Each now has its own channel, and a mesh stays
    hidden while either wants it hidden.
  • Tick labels stayed put through a zoom. Three long-standing divergences
    between the labels Python renders and the ones the interactive view recomputes:
    an axis with a fractional set_xlocator base relabelled 0/2.5/5/7.5/10 as
    0/3/5/8/10 after the first pan; a lone tick at 0 rendered 0e0; and a tick at
    exactly +-0.5 read 0/-0 as drawn but 1/-1 once zoomed, because
    JavaScript rounds a halfway value away from zero where Python rounds half to
    even. A new 2,707-case parity sweep (tests/test_tick_parity_node.py) covers
    the whole range x locator x format x date x categorical x log grid.
  • Tick positions land on their exact step multiples. nice_ticks and
    multiple_ticks accumulated their step by repeated addition, so a tick that
    should be 5.5 came out at 5.499999999999998 and one that should be 0 at
    -3.4e-13 -- the latter printed as a literal -3.4e-13 axis label in the static
    SVG.

0.40.1

Choose a tag to compare

@jrvannucci jrvannucci released this 19 Sep 22:40

Fix interactive SVG output dropping a filled path's interior whenever the path also had an outline. fill_between(..., edgecolor=..., linewidth=...) now preserves its fill color and opacity while drawing the requested stroke, matching PNG output. Adds regression coverage for fill_between() and fill_betweenx().

0.40.0

Choose a tag to compare

@jrvannucci jrvannucci released this 16 Sep 02:05

Fixes

  • Group boxes no longer shrink/mislocate when member axes are removed. Axes.remove() used to freeze a group's box based on whatever the layout looked like the moment its last axes left it, shrinking removal-by-removal down to that lone survivor's own cell. It now freezes on the first departure instead, and a later tight_layout() reflow repositions that frozen box against its former cells' current geometry -- so it keeps matching still-populated sibling groups' width/height rather than staying pixel-locked to a stale, pre-relayout snapshot.
  • Fixed tight_layout() skipping margin reservation for a fully-emptied group's own title band, which could let its title overlap the row above.

Added

  • Axes.set_xlabel()/set_ylabel() now take size=/fontsize=, matching set_title()'s existing per-axes override -- independently size one axes' x/y label without a whole Style copy per figure. Wired through layout margins, SVG/PNG rendering, align_xlabels()/align_ylabels(), tight_layout(auto_label_scale=True), and the template save/load round-trip.