Skip to main content

plot3

ggplot2's grammar of graphics for Python, drawn with WebGL (three.js) in the notebook and saved as journal-ready PNG, SVG, or PDF.

ggplot(df, aes(x="dose", y="response", colour="arm")) + geom_point() + geom_smooth(method="lm")
  • ggplot2 grammar: + layers, aes(), geoms, stats, scales, facets, themes, and ggsave(), with ggplot2's defaults and names.
  • Interactive 2D and 3D: pan, zoom, hover, click a legend entry to hide it, orbit 3D point clouds and surfaces, play animations, drag sliders.
  • Publication output: ggsave("fig.pdf", p, width=3.5, height=2.6, units="in") with real fonts, 300 dpi metadata, and theme_bw by default.
  • Maths built in: plot "y = x^2 + 1", implicit equations, surfaces, densities (dbeta, dnorm), probability areas, tangents, and LaTeX.
  • Your data as it is: pandas, Polars, tidy3, or NumPy arrays, locally or on a remote GPU kernel (CRAFT / SolveIt).

Contents: Install · Quick start · Statistical plots · Annotation · Scales · Themes and titles · Facets and multi-panel figures · Saving for a paper · Functions and maths · Animation and sliders · 3D · Notebooks, SolveIt, CRAFT · API reference · Differences from ggplot2

Install

pip install "plot3[jupyter,export]"

The latest unreleased code installs straight from GitHub:

pip install "plot3[jupyter,export] @ git+https://github.com/rleyvasal/plot3"

To work on plot3 itself:

git clone https://github.com/rleyvasal/plot3 && cd plot3
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev,jupyter,export]"
Extra Adds
export cairosvg: PNG with real fonts and PDF. Needs the Cairo C library (brew install cairo, apt install libcairo2). SVG needs nothing.
fast contourpy: faster implicit-curve contours (matplotlib users have it)
jupyter IPython integration (bare column names, %plot3)
polars Polars tables

Python 3.10 or newer; NumPy and pandas are the only required packages.

Quick start

The examples below share this data:

import numpy as np
import pandas as pd
from plot3 import *

rng = np.random.default_rng(1)
trial = pd.DataFrame({
    "arm": rng.choice(["placebo", "low", "high"], 150),
    "sex": rng.choice(["F", "M"], 150),
    "dose": rng.uniform(0, 10, 150),
})
trial["response"] = (2 + 0.6 * trial.dose + 1.5 * (trial.arm == "high")
                     + rng.normal(0, 1.2, 150))
sales = pd.DataFrame({"month": pd.date_range("2021-01-01", periods=36, freq="MS")})
sales["units"] = 100 + np.cumsum(rng.normal(1, 4, 36))
sales["lo"], sales["hi"] = sales.units - 8, sales.units + 8

A plot is data, an aesthetic mapping, and layers:

p = (ggplot(trial, aes(x="dose", y="response", colour="arm"))
     + geom_point(alpha=0.7)
     + geom_smooth(method="lm")
     + labs(title="Response by dose", x="Dose (mg)", y="Response"))

In a notebook, p on its own line displays the interactive figure. Save it:

ggsave("response.pdf", p, width=3.5, height=2.6, units="in")   # vector, journal column
ggsave("response.png", p, width=7, height=4, units="in", dpi=300)
ggsave("response.html", p)                                      # interactive page

In notebooks, bare column names work too: aes(x=dose, y=response). Plain .py files use strings, as above.

Statistical plots

# Distributions
ggplot(trial, aes(x="response", fill="sex")) + geom_histogram(bins=20)            # stacked by group
(ggplot(trial, aes(x="response", y="after_stat(density)"))                         # density scale
 + geom_histogram(bins=20) + geom_density())
ggplot(trial, aes(x="response", fill="arm")) + geom_density(alpha=0.4)
ggplot(trial, aes(x="arm", y="response")) + geom_boxplot(outliers=False) + geom_jitter(width=0.15, height=0)
ggplot(trial, aes(x="arm", y="response", fill="arm")) + geom_violin()
ggplot(trial, aes(x="response", colour="arm")) + stat_ecdf()
ggplot(trial, aes(sample="response")) + geom_qq() + geom_qq_line()

# Counts and proportions
ggplot(trial, aes(x="arm", fill="sex")) + geom_bar()                     # stacked
ggplot(trial, aes(x="arm", fill="sex")) + geom_bar(position="dodge")     # side by side
ggplot(trial, aes(x="arm", fill="sex")) + geom_bar(position="fill")      # shares

# Means with uncertainty
(ggplot(trial, aes(x="arm", y="response", fill="sex"))
 + stat_summary(fun_data="mean_se", geom="col", position="dodge")
 + stat_summary(fun_data="mean_cl_normal", geom="errorbar", position="dodge", width=0.3))

# Trends and time series
ggplot(trial, aes(x="dose", y="response")) + geom_point() + geom_smooth()        # loess + 95% band
(ggplot(sales, aes(x="month", y="units"))
 + geom_ribbon(aes(ymin="lo", ymax="hi"), alpha=0.25) + geom_line())
Need Use
Error bars from your own columns geom_errorbar(aes(ymin="mean - se", ymax="mean + se")), geom_pointrange, geom_linerange, geom_crossbar, geom_errorbarh(aes(xmin=, xmax=))
Summaries stat_summary(fun_data="mean_se" / "mean_cl_normal" / "mean_sdl" / "median_hilow")
Heatmaps geom_tile(aes(x=, y=, fill=)) (or geom_raster), with geom_text(aes(label=))
Stacked areas geom_area(aes(fill=)), position="fill" for shares
Steps, segments, rectangles geom_step(), geom_segment(aes(xend=, yend=), arrow=arrow()), geom_rect(aes(xmin=, xmax=, ymin=, ymax=))
Horizontal layout + coord_flip() (bars, boxplots, densities, error bars)
Several datasets geom_rect(aes(...), data=periods): any layer can bring its own data
Points over grouped boxes geom_boxplot(aes(colour="sex"), outliers=False) + geom_point(position=position_jitterdodge())
Labels beside points geom_text(position=position_nudge(y=0.3)), or nudge_y=
Shapes and maps geom_polygon(aes(group="id", fill="region")), concave shapes included
Frequency lines geom_freqpoly(aes(colour="arm"), binwidth=0.5)
Big scatters, 2D distributions geom_hex(), geom_bin_2d(), geom_count(), geom_density_2d(), geom_density_2d_filled(), stat_ellipse() (95% by group)
Contours of a grid geom_contour(aes(x=, y=, z=))

aes() reads expressions over your columns, as ggplot2 does: aes(ymin="mean - se"), aes(y="log10(count)"), aes(colour="factor(cyl)"), aes(colour="dose > 5"), aes(label="round(estimate, 2)"). They use + - * / ^, comparisons, and log, log10, log2, exp, sqrt, abs, round, floor, ceiling, factor, as.numeric, ifelse, pmin, pmax, mean, median, sd, min, max, sum; nothing else runs. The expression names the axis or legend.

Rows with missing values are dropped, and plot3 says so: Removed 3 rows containing missing values (geom_point).

Annotation

(ggplot(trial, aes(x="dose", y="response"))
 + geom_point()
 + geom_hline(yintercept=5, linetype="dashed")
 + geom_vline(xintercept=[2, 8], colour="firebrick")
 + geom_abline(slope=0.6, intercept=2)
 + annotate("rect", xmin=2, xmax=8, ymin=0, ymax=12)
 + annotate("label", x=9, y=1, label="safe range")
 + annotate("segment", x=1, y=10, xend=3, yend=8, arrow=arrow()))

means = trial.groupby("arm", as_index=False).response.mean()
(ggplot(means, aes(x="arm", y="response", label="response"))
 + geom_col() + geom_text(nudge_y=0.4))     # numbers print to 4 significant figures

geom_text / geom_label take size in millimetres (ggplot2's 3.88 mm default), hjust, vjust, nudge_x, nudge_y, fontface, and check_overlap. Line types are "solid", "dashed", "dotted", "dotdash", "longdash", "twodash", R's numbers, or hex such as "44".

Map more aesthetics: aes(shape=) (circle, triangle, square, diamond, plus, cross), aes(linetype=), aes(size=) (area), aes(fill=) for filled shapes and aes(colour=) for points and lines.

Scales

(ggplot(trial, aes(x="dose", y="response", colour="arm"))
 + geom_point()
 + scale_x_continuous("Dose (mg)", breaks=[0, 2.5, 5, 7.5, 10])
 + scale_y_continuous(limits=(0, None))
 + scale_colour_manual(values={"placebo": "grey50", "low": "#56B4E9", "high": "#D55E00"},
                       breaks=["placebo", "low", "high"],
                       labels=["Placebo", "Low dose", "High dose"], name="Arm"))

(ggplot(sales, aes(x="month", y="units"))
 + geom_line()
 + scale_x_date(date_breaks="6 months", date_labels="%b %Y")
 + scale_y_continuous(labels="dollar"))
Scale Functions
Position scale_x_continuous(name, limits, breaks, labels, trans="log10"/"reverse"), scale_x_discrete(limits, labels), scale_x_date(date_breaks, date_labels), scale_x_log10, scale_x_reverse, xlim, ylim, lims (and the y versions)
Label formats "percent", "comma", "dollar", "scientific", "{:.1f} kg", a list, or a function
Discrete colour / fill scale_colour_hue (ggplot2's default colours), scale_colour_manual, scale_colour_brewer(palette="Set2"), scale_colour_viridis_d, scale_colour_grey, scale_colour_okabe_ito (colour-blind safe), scale_colour_identity (the column holds colours)
Continuous colour / fill scale_colour_gradient(low, high), scale_colour_gradient2(low, mid, high, midpoint), scale_colour_gradientn(colours, values), scale_colour_distiller(palette="RdBu"), scale_colour_viridis_c, scale_colour_continuous(trans="log10")
Size / alpha scale_size(range=(4, 23)) (by area across the data's range, as ggplot2), scale_size_area(max_size=) (area from zero), scale_alpha(range=(0.1, 1)) for aes(alpha=)
Shape / linetype scale_shape_manual, scale_linetype_manual
Limits expand_limits(y=0) makes an axis reach a value with no data there

Every colour scale has scale_fill_* and scale_color_* names. Colours can be CSS names ("steelblue"), R greys ("grey50"), "rgb(1,2,3)", or hex. Limits on a continuous axis drop the rows outside them and say how many.

Themes and titles

(ggplot(trial, aes(x="arm", y="response", fill="arm"))
 + geom_boxplot()
 + labs(title="Response", subtitle="150 patients", caption="Source: simulated", tag="A")
 + theme_classic()
 + theme(legend_position="none", axis_text_x_angle=45, plot_title_hjust=0.5))

Themes: theme_bw (default for saved files), theme_classic, theme_minimal, theme_void (the data alone), theme_light, theme_dark (default in the interactive viewer), and theme_lidar (driving scenes). Each takes base_size (points) and base_family. theme() sets legend_position ("right", "bottom", "none", or (x, y) inside the panel), legend_title=False, panel_grid=False, axis_text_x_angle, plot_title_hjust, base_size, and base_family. theme_grey (ggplot2's grey panel) and theme_linedraw are there too.

ggplot2's elements work, with R's dotted names or underscores:

(ggplot(trial, aes(x="arm", y="response")) + geom_boxplot() + theme_bw()
 + theme(**{"axis.text.x": element_text(angle=45), "panel.grid": element_blank(),
            "plot.title": element_text(hjust=0.5),
            "panel.background": element_rect(fill="grey95")}))

element_blank() hides axis text, axis titles, the grid, the panel border, or the legend title; colours in element_text, element_line, and element_rect recolour text, grid, border, and backgrounds. Parts plot3 does not draw (minor grid, tick length) are accepted, and anything else it cannot draw warns.

ggtitle("Response", subtitle=), xlab(), and ylab() are shortcuts for labs(). guides(colour="none") hides one legend (also fill, size, shape, linetype) and keeps the others; guides(colour=guide_legend(title="Arm", reverse=True)) retitles or reorders it. The stat_* spellings (stat_smooth, stat_bin, stat_count, stat_density, stat_function(fun=, args=)) work too.

Zoom without dropping data with coord_cartesian: a smoother or boxplot is still computed from every row, while xlim() and scale_x_continuous(limits=) remove the rows outside first.

(ggplot(trial, aes(x="dose", y="response"))
 + geom_point() + geom_smooth(method="lm") + geom_rug(alpha=0.4)
 + coord_cartesian(xlim=(2, 6)))

Facets and multi-panel figures

ggplot(trial, aes(x="dose", y="response")) + geom_point() + facet_wrap("arm")
ggplot(trial, aes(x="dose", y="response")) + geom_point() + facet_wrap("arm", labeller="label_both")
ggplot(trial, aes(x="dose", y="response")) + geom_point() + facet_wrap(vars("arm"))
(ggplot(trial, aes(x="dose", y="response", colour="arm"))
 + geom_point() + facet_grid("sex ~ arm"))                # rows ~ columns

a = ggplot(trial, aes(x="dose", y="response", colour="arm")) + geom_point()
b = ggplot(trial, aes(x="arm", y="response", fill="arm")) + geom_boxplot() + theme(legend_position="none")
c = ggplot(sales, aes(x="month", y="units")) + geom_line()
fig = ((a | b) / c) + plot_annotation(title="Overview", tag_levels="A")
fig2 = (a | b) + plot_layout(widths=[2, 1])

Facet panels share scales, colours, one legend, and one pair of axis titles, as in ggplot2. | puts plots side by side, / stacks them, and tag_levels is "A", "a", "1", or "I". Save a composed figure with ggsave like any plot.

Saving for a paper

ggsave("fig1.pdf", p, width=3.5, height=2.6, units="in")             # one column
ggsave("fig1.png", p, width=7, height=4.5, units="in", dpi=300)       # two columns
ggsave("fig1.svg", p, width=18, height=12, units="cm", fontsize=9, family="Arial")
  • PNG, SVG, and PDF draw the same picture with real fonts. PNGs carry their DPI, so Word and journal portals size them correctly.
  • Saved files use theme_bw unless you add a theme.
  • Crowded category labels turn or thin automatically; set theme(axis_text_x_angle=) to choose.
  • Non-Latin text (東京, 서울, ✓) uses an installed font that has the glyphs.
  • Notes such as Removed 3 rows… are printed, not drawn; notes=True draws them.
  • Without the export extra, .svg works everywhere and .png falls back to a built-in bitmap font.

The interactive viewer also has a Save button (HTML, SVG, PNG, video for animations, copy to clipboard).

Functions and maths

geom_function plots a formula with the same grammar as data. ^ is power and 2x means 2*x.

ggplot() + geom_function("y = 2x + 2")
ggplot() + geom_function("y = a x^2 + b x + c", a=2, b=-3, c=1)    # coefficients at the end
ggplot() + geom_function("x^2 + y^2 = 1")                          # implicit: a round circle
ggplot() + geom_function("z = sin(x) cos(y)", xlim=(-3, 3), ylim=(-3, 3))   # 3D surface, coloured by height
ggplot() + geom_function(r"y = \frac{\sin x}{x}")                  # LaTeX input
ggplot() + geom_function("r = 1 + cos(theta)") + coord_polar()
ggplot() + geom_function("y > x^2")                                # shaded region
ggplot(trial, aes(x="dose", y="response")) + geom_point() + geom_function("y = 2 + 0.6x")

Distributions and probability areas (no SciPy needed):

inf = float("inf")
ggplot() + geom_function("y = dbeta(x, 2, 5)") + area(0.2, 0.5)            # P(0.2 ≤ X ≤ 0.5) = 0.546
ggplot() + geom_function("y = dnorm(x)") + area(-inf, -1.96) + area(1.96, inf)
ggplot() + geom_function("y = dt(x, 3)") + area(2.353, inf)               # P(X ≥ 2.353) = 0.05
ggplot() + geom_function("y = x^3 - 3x") + tangent(at=1) + derivative()

Formulas know sin cos tan exp log ln sqrt abs floor ceil gamma lgamma beta erf erfc, the densities dnorm dbeta dt dchisq dgamma dexp dunif dlnorm, and pnorm qnorm pt qt pbeta pexp punif. A density opens on its own support (dbeta on [0, 1]). Poles (1/x, tan x) are clipped with a note; steep but finite curves are not. Pass your own functions as keywords: geom_function("y = damp(x) sin(3x)", damp=my_damp), or a lambda.

Animation and sliders

ggplot() + geom_function("y = sin(x - t)") + transition_time(t=(0, 6.28))   # travelling wave
ggplot() + geom_function("y = dbeta(x, a, b)") + slider(a=(0.5, 5), b=(0.5, 5))

Data animations follow gganimate. With a table shaped like gapminder (made-up numbers here; the real data is pl.read_csv("https://raw.githubusercontent.com/kirenz/datasets/master/gapminder.csv")):

countries = {"Brazil": "Americas", "China": "Asia", "Egypt": "Africa",
             "France": "Europe", "India": "Asia", "Mexico": "Americas"}
gapminder = pd.DataFrame([
    {"country": country, "continent": continent, "year": year,
     "gdpPercap": 800 * (1.03 + 0.01 * i) ** (year - 1952),
     "lifeExp": 45 + 0.35 * (year - 1952) + 2 * i,
     "pop": 2e7 * (1.02 + 0.002 * i) ** (year - 1952)}
    for i, (country, continent) in enumerate(countries.items())
    for year in range(1952, 2008, 5)
])

(ggplot(gapminder, aes(x="gdpPercap", y="lifeExp", size="pop", colour="continent", group="country"))
 + geom_point() + scale_x_log10() + transition_time("year") + labs(title="{frame_time}"))

{frame_time} in a title shows the current year as the animation plays.

The viewer interpolates between frames in the browser, with play, pause, scrubbing, speed, and video recording.

3D and point clouds

Map z on every layer for an orbit view. A point cloud with no colour of its own is coloured by height (viridis), as lidar viewers draw it; map colour= or set colour="steelblue" to change that.

ggplot(lidar, aes(x="x", y="y", z="z")) + geom_point3d()      # coloured by height

(ggplot(cloud, aes(x="x", y="y", z="z", colour="intensity"))
 + geom_point3d(size=0.008)
 + coord_3d(aspect="data", max_points=300_000)
 + scale_colour_viridis_c(option="turbo"))

ggplot(grid, aes(x="x", y="y", z="height", fill="height")) + geom_surface()
ggplot(points, aes(x="x", y="y", z="z")) + geom_isosurface(levels=[0.2, 0.5, 0.8])
read_bin("scan.pcd.bin")          # nuScenes-style point clouds; remote=True under CRAFT

NumPy arrays use column positions: ggplot(pts, aes(x=0, y=1, z=2, colour=3)).

A driving scene, as autonomous-driving viewers draw it: points coloured by height on black, detection boxes by class, and a chase camera behind the car.

(ggplot(sweep, aes(x="x", y="y", z="z"))
 + geom_point3d()
 + geom_box3d(aes(length="l", width="w", height="h", angle="yaw", colour="class"),
              data=boxes)
 + coord_3d(azim=180, elev=28, zoom=1.6)
 + theme_lidar())

geom_box3d takes each box's centre (x, y, z), its length along the heading, width, height, and the heading angle in radians, as nuScenes and KITTI store them. coord_3d(elev=, azim=, zoom=) sets where the camera starts, in degrees as matplotlib's view_init.

The box keeps the data's proportions, except that a tall cloud (a helix, a tree) is shortened to twice its width so it does not become a thin column; coord_3d(aspect="data") keeps true proportions always, and aspect="equal" draws a cube.

Notebooks, SolveIt, and CRAFT

  • Jupyter / SolveIt: bare column names and backticks work in aes(), facet_wrap(), and facet_grid() (aes(x=First Name)). Toggle with enable_r_style() / disable_r_style(). .py files keep quoted strings.
  • SolveIt draws figures inline and hides their HTML from the model's context (autohide(False) to opt out).
  • VS Code notebooks block WebGL in output cells, so plot3 opens figures in your browser (PLOT3_DISPLAY=browser|iframe to force a mode). The page is written to .plot3_preview/latest.html in the notebook's folder, or to the system temp folder when that folder is read-only.
  • CRAFT / %gpu: the same ggplot(...) code runs on the remote kernel; stats run where the data lives and only a compact payload comes back.
%run /path/to/plot3/plot3.py        # loads plot3 locally and seeds the GPU kernel
%plot3 df x=wt y=mpg color=cyl       # optional shortcut magic

API reference

Area Functions
Figure ggplot(data, aes(...)), data >> ggplot(aes(...)), +, p.show(), p.save(), ggsave()
Aesthetics aes(x, y, z, colour, fill, size, shape, linetype, group, label, ymin, ymax, xmin, xmax, xend, yend, sample)
Points and lines geom_point, geom_jitter, geom_line, geom_path, geom_step, geom_segment, geom_text, geom_label
Bars and areas geom_col, geom_bar, geom_histogram, geom_freqpoly, geom_area, geom_ribbon, geom_rect, geom_tile/geom_raster, geom_polygon
Distributions geom_boxplot, geom_violin, geom_density, geom_qq, geom_qq_line, stat_ecdf, stat_summary
2D distributions geom_bin_2d, geom_hex, geom_count, geom_density_2d / stat_density_2d, geom_density_2d_filled, geom_contour, stat_ellipse
Uncertainty and fits geom_errorbar, geom_errorbarh, geom_crossbar, geom_pointrange, geom_linerange, geom_smooth(method="loess"/"lm")
Reference geom_hline, geom_vline, geom_abline, geom_rug, annotate("text"/"label"/"rect"/"segment"/"point")
Positions position="stack"/"dodge"/"fill"/"identity"/"jitter", position_dodge(width), position_dodge2(padding), position_stack(), position_fill(), position_jitter(), position_jitterdodge(), position_nudge()
Functions geom_function, geom_vector_field, area, tangent, derivative
Scales see Scales
Coordinates coord_cartesian, coord_flip, coord_equal / coord_fixed, coord_polar, coord_3d
Facets and layout facet_wrap, facet_grid, labeller="label_both" / labeller(var=dict), p1 | p2, p1 / p2, plot_layout(widths, heights, height), plot_annotation
Labels and themes labs(title, subtitle, caption, tag, x, y, colour, fill, alpha), ggtitle, xlab, ylab, guides, theme_*, theme(), element_text, element_line, element_rect, element_blank
Animation transition_time, transition_states, slider
3D geom_point3d, geom_surface, geom_isosurface, geom_box3d, stat_density_3d, read_bin

Differences from ggplot2

  • Python needs quotes outside notebooks: aes(x="wt"). In Jupyter and SolveIt, aes(x=wt) works.
  • labs(x=None) (or labs(x="")) removes a title, as ggplot2's labs(x = NULL).
  • fill colours filled shapes; points and lines use colour, as in ggplot2.
  • Categories sort alphabetically (numbers numerically); use pd.Categorical or scale_x_discrete(limits=) for your own order.
  • Text size is in millimetres, as in ggplot2; geom_point(size=) is in pixels in 2D.
  • Saved files default to theme_bw, the interactive viewer to theme_dark. On light themes, a layer with no colour of its own is drawn as ggplot2 draws it: black points and lines, grey bars, white boxes and violins. The dark viewer uses its own blue instead.
  • Building a figure prints nothing. PLOT3_VERBOSE=1 prints each figure's size in KB, for embedding in slides or pages with a size cap.

Development

pytest -q                       # ~670 tests, including every example in this README
python examples/showcase_2d.py  # 2D gallery in the browser
python examples/showcase_3d.py  # 3D gallery

The code is in plot3/: geoms.py (grammar objects), scaling.py (scale functions), build.py (stats to a figure spec), stat2d.py and flip.py (statistical layers), static.py (PNG/SVG/PDF), viewer.py (the WebGL viewer), expr.py / function.py / calculus.py (formulas), compose.py (multi-panel figures). See CHANGELOG.md for changes between versions.

License

MIT. See LICENSE.

Metadata

Release files for plot3 0.4.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for plot3 0.4.2
File Size Uploaded
plot3-0.4.2.tar.gz 379.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for plot3 0.4.2
File Interpreter ABI Platform
plot3-0.4.2-py3-none-any.whl Python 3 none any Details

Total release size: 683.3 kB

Release files / plot3-0.4.2.tar.gz

Download URL plot3-0.4.2.tar.gz
Size 379.6 kB
Tags Source
SHA-256 checksum
How to use checksums
9e32e362de21368346f70068ad837ac950c18b346f02242df462901d95bba1e3
BLAKE2b-256 checksum
How to use checksums
e00331259ddabd0004da76be1bd3c941e72bcfad6d9b1da1798ae9515c13b9c8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 9, 2026.

Transparency log

Release files / plot3-0.4.2-py3-none-any.whl

Download URL plot3-0.4.2-py3-none-any.whl
Size 303.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3d9fc58e9eb08980203b8d179366d77d74e711cb74ef925b636052156592f834
BLAKE2b-256 checksum
How to use checksums
fe630f0f91cbc8dc01998e8191acf37efc8e2b1cbb4ba9c4c22f91479ed02c45
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 9, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.4.2 This release

2 release files

0.4.1

2 release files

0.4.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page