plotlet v0.6.2

API reference

pt.chart(data=None, mapping=None, **opts) returns a Chart. Table-shaped marks (scatter, line, bar, hist, ecdf, …) take long-form input only: pass a data table (any object supporting data[col_name] — pandas / polars / dict-of-lists / dict-of-arrays) and map columns to aesthetics with aes(...)aes(x="col", y="col", color="col") (fill= on fill-defaulted artists). A bare string kwarg is always a literal (color="red"); only aes(...) refers to a column — no sniffing. Data and mapping set on pt.chart(df, aes(...)) propagate to every mark, so per-call values are optional once set at chart level. heatmap is also table-shaped (df, aes(x=), values=, see below). The remaining shape marks (imshow, contour, dendrogram, rect / polygon / polyline, the reference lines and spans, annotate) take their natural positional input — there's no aes(...) version for a 2-D matrix or a single y-value. All methods return self, so they chain.

Frame options

Pass at construction or as chained setters:

title, subtitle (smaller line under the title), caption (small right-aligned text below the chart — ggplot's labs(caption=)), xlabel, ylabel, xlim=(a, b), ylim=(a, b), xscale, yscale, gridlines=True/False (or "major"/"minor"/"both"; also c.gridlines(which="minor") — minor lines use the axes' minor= tick positions, or auto subdivisions when none are set), legend=True/False, clip=True/False, data_width, data_height, theme (see THEMES.md), font (see Fonts).

c.legend(True, position=...) places the in-frame legend. Outside tokens (reserve margin space beside the data area): "right" (default), "left", "top", "bottom". Inside tokens (overlay the data area): "top-right", "top-left", "bottom-right", "bottom-left", "center". Outside positions emit no frame; inside positions get a translucent background for readability over plot marks. ncols=N (also on pt.legend(...)) wraps discrete entries into N columns, filled down-then-across; "top" / "bottom" default to one horizontal row until ncols= switches them to the grid. A gradient-only chart (imshow / heatmap / hexbin) on "top" / "bottom" gets a horizontal colorbar — strip running vmin-left → vmax-right, ticks below. reverse=True (also on pt.legend(...)) flips the discrete entry order; entries=[{"label": ..., "color": ...}, ...] appends free-form manual rows not harvested from any artist (optional alpha; rendered as standard rect swatches).

Per-artist legend={...} customizes that artist's entries wherever they render (in-frame or pt.legend() panel): {"label": ..., "ticks": [...]} on a continuous source retitles its gradient strip; {"glyph": "rect"} swaps the series swatch for the standard rectangle — the readable choice when the plot mark itself is tiny (e.g. scatter with size=1.5); aesthetic keys (alpha, size, marker, markersize, linewidth, linestyle) override that value in the legend key only — ggplot2's override.aes. So scatter(..., alpha=0.2, legend={"alpha": 1}) plots translucent points with an opaque legend key. Aesthetic guides (scatter's graded size dots) keep their own glyphs.

Sizes accept bare pixels (400) or unit-suffixed strings ("4in", "10cm", "100mm", "72pt"). The data region is the user-facing primitive — the canvas grows to fit titles and tick labels. To target a specific SVG canvas, chain .fit(canvas_width=…, canvas_height=…) after composing.

clip=False lets artists bleed past the data area into the margin space; default True clips at the data boundary so off-axis data can't paint over tick labels.

c.spines(...) styles the frame border: top-level color= / width= / linestyle= set the base style; top= / right= / bottom= / left= / walls= (the inter-sector walls) each take a bool (visibility) or a {color, width, linestyle, visible} dict. c.facecolor(color) fills the data-area background. c.x_expand(lo, hi) / c.y_expand(...) override the autoscale padding as fractions of the data span (one number pads both ends). pt.chart(margin={"left": ..., "right": ..., "top": ..., "bottom": ...}) (all four sides) raises the per-side whitespace floor around the data area.

c.aspect("equal") or c.aspect(r) locks the data-space aspect ratio (matplotlib set_aspect, ggplot coord_fixed): one y data unit renders at r× the pixel length of one x data unit. The lock survives share classes and .fit().

Coordinates

c.coordinate(pt.CircularCoordinate(...)) switches a panel to a non-Cartesian coord system. One coordinate per panel; for two coordinate systems, use two panels (composed via pt.grid / | / /).

CircularCoordinate is the only coordinate shipped in core today (ring / annulus geometry; set r_inner=0 for a full polar disc). Each coord opts in its supporting artists via pt.declare_coord_support(name, [...]); the renderer raises if you mix a coord with an artist not in that list. See plotlet-cookbook/circle/ for a worked example and the protocol notes at the top of src/plotlet/render/_coord_circular.py.

Scales

xscale / yscale values:

value notes
"linear" default
"log" log10; respect positive data
"category" for strings; auto-selected when data is string-valued. c.xscale("category", order=[...], padding=0) for explicit ordering. padding=0 makes bands contiguous (heatmap-track look). For grouped categories with visual gaps, use c.sectors({cluster: [members]}, axis="x") — see Sectors below.
"symlog" accepts linthresh= (default 1.0) sizing the linear region around zero
"power" accepts exponent=
"sqrt" shorthand for "power" with exponent=0.5
"time" auto-selected for datetime.date / datetime.datetime values. Ticks snap to calendar boundaries (year / month / day / hour / minute / second) at a resolution matching the axis span; labels format accordingly. Force with c.xscale("time") when data is already epoch-seconds floats; xlim accepts datetime or epoch-seconds endpoints.

Every non-category kind accepts reverse=True to invert the axis (c.yscale("linear", reverse=True) — depth profiles, ranks). For a category axis, reverse the order= list instead.

Tick overrides

c.xticks([0, 5, 10], ["A","B","C"], rotation=45, fontsize=12,
         direction="out", marks=False)   # `[]` to hide
c.yticks(...)                            # same shape

marks=False hides the tick marks but keeps labels; labels=False is the counterpart — keep auto positions and marks, suppress the labels. side="top" (x) / side="right" (y) moves the axis — spine, tick marks, tick labels, and the axis title as a block — to the other edge (plotly's side, ggplot2's position).

fontstyle="italic" / fontweight="bold" restyle the tick labels (sector-name labels on that axis inherit the style) with the real Italic/Bold faces of the active family — see Fonts. decoration="underline" | "overline" | "line-through" adds the matching stroke line.

format="{:.0%}" or format=lambda v: f"${v/1000:.0f}K" formats auto-generated tick labels — string and callable both work; explicit labels still override. Named formatters (pt.list_formatters()): "money", "si", "percent", "scientific", "comma", and "power10" (10000010⁵, 0.0022×10⁻³ — the natural log-axis format, c.xticks(format="power10")). pt.register_formatter(name, fn) adds your own.

minor=True adds auto-positioned minor ticks (5 per major-gap on linear scales; sub-decade on log); minor=[v1, v2, …] for explicit positions.

Density overrides: step=0.25 forces a fixed spacing; count=4 requests roughly that many major ticks (the nice-numbers algorithm still picks the actual values).

Text in labels & annotations

All text (titles, labels, ticks, legend, text/annotate) accepts any Unicode character the active font covers (Fonts; the default DejaVu Sans covers the full scientific set) — just type it, no markup: Greek (α μ Σ), super/subscripts (10⁻⁵, H₂O, μm², log₂ FC), operators (× ± ≤ ≈ → √ ∫). Unicode super/subscripts exist only for digits, + − ( ), and a few letters — no x^{n+1} markup (unbuilt). pt.superscript("-2") / pt.subscript("2") convert plain strings so you don't hunt for codepoints: "kg·m" + pt.superscript("-2")kg·m⁻². \n stacks lines: "Fold change\n(log₂)".

Fonts

Text renders as SVG path outlines from an explicitly named font file — never an OS font lookup — so output is byte-identical across machines and viewers need no font installed. Three tiers:

  1. Default — DejaVu Sans (bundled). Zero config. It stays the default for coverage: Greek, Mathematical Operators, arrows, and super/subscripts all render without tofu.
  2. Bundled familiesfont="Arimo", the journal look: metric-compatible with Helvetica/Arial, and the aliases "Helvetica" / "Arial" select it. Arimo covers Greek and super/subscripts but few math operators or arrows; glyphs a face lacks render as .notdef boxes.
  3. Escape hatchfont="/path/to/MyFont.ttf" loads any TTF/OTF on disk. Only the author needs the file at render time; the output stays self-contained and carries the font's family name, never the file path. ⚠️ Embedding a proprietary font's outlines in published SVGs may be restricted by its EULA — check before shipping.

font= at construction or chained c.font(...); per-chart like theme=, and the two compose (theme="dark", font="Arimo"). Unknown font names raise — no silent fallback, no OS resolution.

Both bundled families ship all four faces (Regular / Bold / Italic / BoldItalic), so per-text fontstyle="italic" / fontweight="bold" (on tick labels and draw.text_path) render real drawn variants. A path-loaded font is one file: italic falls back to a synthetic -12° skew, and fontweight="bold" raises — pass the bold file's path as font= instead.

Sectors

c.sectors(spec, axis="x" | "y", ...) partitions an axis into named regions. Two kinds, picked from the spec shape:

  • Continuous — values are numeric lengths. Each artist's value column (named via column=) is offset into a single global coordinate so the standard linear scale covers every sector.

    c.sectors({"warmup": 100, "training": 500, "cooldown": 50},
              column="phase")
    c.add_scatter(df, aes(x="t", y="v"))   # df has a 'phase' column
    
  • Categorical — values are lists of category labels. Sectors group the categorical axis members; the category scale inserts visual gaps between groups and reorders cells. This is the unification of heatmap row/column clustering with continuous-axis partitioning.

    c.sectors({"clusterA": ["c1", "c2"],
               "clusterB": ["c3"]}, axis="x")
    # tidy input: the `col` column holds the x labels (one table row per
    # heatmap column); the remaining columns are the value tracks.
    c.add_heatmap(df, aes(x="col"))
    
kwarg notes
axis="x" / "y" which axis to partition
column= continuous only — the data row's sector tag column
divider=True draw boundary divider lines
label=True draw sector-name labels at sector centers
gap=None inter-sector pixel pad (categorical); None → spec default (6 px)

Typical usage picks one of gap (categorical default) or divider line (continuous default) — both at once reads as redundant clutter. For heatmap clusters with visible gap whitespace and no labels, pass divider=False, label=False so sectors drive axis layout only:

c.sectors({"clusterA": ["c1", "c2"], "clusterB": ["c3"]},
          axis="x", divider=False, label=False)
c.add_heatmap(df, aes(x="col"))   # df["col"] = ["c1","c2","c3"]

Layout-level sugar fans out one declaration to every leaf:

pt.grid([[t1], [t2], [t3]]).share_x("col").sectors(PHASES, column="phase")

Mark methods

The tables below give a cross-artist at-a-glance comparison. For each artist's full docstring (usage examples, special behaviors, coord-specific kwargs like arc=False), read it directly via help(c.add_line) or c.add_line? in Jupyter — Chart.__getattr__ surfaces each artist's module docstring on the recorder.

Every data mark is long-form: a data table plus an aes(...) mapping from aesthetics to column names. A bare string kwarg is always a literal; only aes(...) maps a column. Call shapes:

df = {...}                                   # data built outside calls
c = pt.chart(df, aes(x="t", y="v"))          # chart-level mapping, inherited
c.add_line()                                 # inherits df + mapping
c.add_line(aes(color="g"))                   # adds a column mapping
c.add_line(aes(x="t", y="v"), color="red")   # literals stay bare
c.add_line(df2, aes(x="t", y="v"))           # per-call data — overrides the chart's df
c.add_line(data=df2, mapping=aes(x="t", y="v"))  # fully named spelling

The primary grouping aes is color= for stroke-defaulted artists (line, scatter, regression, density_1d, ecdf, freqpoly, rug) and fill= for fill-defaulted ones (bar, hist, area, boxplot, violin, strip, swarm). Bare color="red" is a literal; aes(color="col") maps the column — one series per unique level.

line and scatter accept additional column mappings: aes(group=) (invisible split — no color/legend burn; e.g. one polyline per subject within a cohort) and aes(alpha=) (opacity per level, linearly interpolated through alphas=(min, max), default (0.3, 1.0)). line additionally accepts linestyle= — bare it's a literal dash spec ("--", ":", "-.", …); aes(linestyle="col") cycles dashes per level. Not on scatter since there's no line to dash.

Chart-level mappings are set once on the constructor — pt.chart(df, aes(x=, y=, color=, fill=, group=, linestyle=)) — and are inherited by calls that don't override them (aes(alpha=) is per-call only). Inheritance is per-key: a mark keeps every chart-level mapping it doesn't name itself. Literal defaults (color="red", palette=) stay as plain chart kwargs. Other artists (boxplot, bar, hist, etc.) accept only the relevant subset for their geometry; chart-level mappings they don't support pass through silently.

To keep an inherited mapping off one mark: pass a constant to override it (c.add_line(color="#333") blocks the inherited color), or inherit_aes=False to drop all chart-level aes on that call — ggplot2's inherit.aes=FALSE. It affects mappings only; the chart's data is still inherited if the mark brings none.

The tables list styling kwargs; the grouping/long-form pattern is universal and not repeated.

xy and 1-D distributions

call options
.add_line(aes(x=, y=, color=, group=, linestyle=, alpha=), **opts) palette, alphas=(min, max), label, linewidth, marker, size (marker radius px), curve ("linear", "step-before", "step-after", "step-mid") — bare linestyle= ("-", "--", ":", "-.", …) → fixed dash; aes(linestyle=) → cycle dashes per level. estimator="mean"\|"median" collapses replicate rows per x with a CI band (ci="t"\|"boot"\|None, level, n_boot, seed, band_alpha) — seaborn lineplot
.add_step(aes(x=, y=), where=, **opts) sugar over line(curve=…); where= is "pre" / "post" (default) / "mid"
.add_scatter(aes(x=, y=, color=, group=, alpha=, size=, style=), **opts) palette, alphas=(min, max), label, marker, sizes=(min, max), size_legend={"breaks": [...], "labels": [...]}, cmap, vmin, vmax, normaes(color=) dispatches on dtype: numeric column → cmap, categorical → palette. Bare size=: number → fixed radius (px), list → per-point; aes(size=) → graded via sizes=(lo, hi)
.add_regression(aes(x=, y=, color=), **opts) palette, level=0.95, alpha=0.2, linewidth=1.8 — OLS fit + Student-t band. order= fits a polynomial; robust=True a Huber IRLS fit with a bootstrap band (n_boot=200, seed=0); lowess=True a LOWESS smoother (frac=2/3, it=3), line only — no band
.add_hist(aes(x=, fill=, weights=), **opts) color (stroke), palette, bins (count or explicit edges), binwidth, binrange=(lo, hi), weights (bare sequence or aes(weights=) column — sum per bin instead of count), density, cumulative, position="overlay"\|"stack"\|"fill"\|"dodge" (multi-group layout; histtype="bar" only), histtype ("bar" / "step" / "stepfilled"), orientation
.add_density_1d(aes(x=, color=), **opts) palette, bw, n_grid=200, fill=True/False, alpha — Gaussian KDE
.add_ecdf(aes(x=, color=), **opts) palette, complement=False (survival), linewidth
.add_rug(aes(x=, color=), orientation="x", **opts) palette, length=0.04, alpha — tick marks at observations
.add_freqpoly(aes(x=, color=), **opts) palette, bins, density — line version of hist
.add_qq(aes(sample=, color=), **opts) dist= accepts "normal", any scipy.stats RV, or another sample; aes(color=) → one series + reference line per level (palette=)

Categorical distributions

call options
.add_boxplot(aes(x=, y=, fill=), **opts) color (stroke), palette, orientation, notch, width, whis=1.5, flier_size
.add_violin(aes(x=, y=, fill=), **opts) color (stroke), palette, inner="box"\|"quartile"\|None, trim, bw_adjust, fill_alpha
.add_swarm(aes(x=, y=, fill=), **opts) color (outline), palette, size, linewidth — collision-resolved jitter
.add_strip(aes(x=, y=, fill=), **opts) color (outline), palette, size, jitter — raw jittered points
.add_pointplot(aes(x=, y=, color=), **opts) estimator="mean", ci="t"\|"boot"\|None, level=0.95; aes(color=) → one series per level (palette=)

Bars, areas, errorbars

call options
.add_bar(aes(x=, y=, fill=), position=, **opts) color (stroke), palette, position="stack"\|"dodge"\|"fill" for multi-series, orientation, bottom, width, gap; yerr=/xerr= (same specs as errorbar) draw whiskers at bar/slot centers with ecolor, capsize — defaults position to "dodge" and requires one row per (category, group). stat="count" (drop the y mapping; seaborn countplot) or stat="mean" (mean per cell + CI error bars: ci="t"\|"boot"\|None, level, n_boot, seed; seaborn barplot) aggregate raw rows
.add_area(aes(x=, y=, fill=), **opts) multi-series stacks when given a list-of-series or aes(fill=); palette, base, curve, alpha
.add_fill_between(aes(x=, y1=, y2=), **opts) fill (interior color — not color=), alpha, curve, label
.add_errorbar(aes(x=, y=, yerr=, xerr=), **opts) bare yerr=/xerr= take a scalar, sequence, or (lower, upper) tuple for asymmetric bars; aes(yerr=) names a column. aes(ymin=, ymax=) (and xmin=/xmax=) map columns for absolute bounds, mutually exclusive with the matching *err=; aes(color=) → grouped series (palette=), dodged on a categorical axis with bar-matching width/gap defaults

2-D distributions

call options
.add_hexbin(aes(x=, y=), **opts) gridsize=30, cmap, vmin, vmax — hexagonal bins colored by count
.add_hist2d(aes(x=, y=), **opts) bins=30, binwidth, binrange (scalar or per-axis pair), cmap, vmin, vmax — rectangular bins colored by count, empty cells transparent
.add_kde_2d(aes(x=, y=, color=), **opts) bw, n_grid=60, levels, cmap, fill=True (filled level regions), alpha — iso-density contours; aes(color=) → one single-colored density per level (palette=)
.add_contour(grid, **opts) levels, extent=(x0, x1, y0, y1), cmap, fill=True (mpl contourf), alpha — pre-computed 2-D grid
.add_ridge(aes(x=, y=, color=), **opts) overlap=1.4, bw, alpha — joyplot; aes(color=) → overlaid sub-densities per row (palette=)

Images, matrices, reference, shapes, text

call options
.add_imshow(data, **opts) cmap (~180 vendored, default "viridis"), vmin, vmax, extent, annot, fmt, annot_color, annot_fontsize
.add_heatmap(df, aes(x=, sector=), values=, **opts) Tidy input: each table row → a heatmap column (x-position from the aes(x=) column — numeric → continuous axis, string → categorical), each value column → a track row (values= selects/orders them by name, default = all columns not mapped to x/sector). A numeric x is auto-sorted (row order carries no meaning); duplicate or NaN positions raise. Opts: cmap, vmin, vmax, norm, center, palette, absent_fill, legend, annot, fmt, annot_color, annot_fontsize, linewidth, linecolor, border. aes(sector=) + c.sectors(...) draws gaps; for categorical-x clusters call c.sectors({cluster: [members]}, axis=...) — see Sectors. A bare matrix is not accepted; reshape it into a table first.
.add_dendrogram(data, **opts) orientation="top"\|"left"\|"right"\|"bottom", labels, method="single"\|"complete"\|"average"\|"ward"\|... (scipy), metric, linkage_matrix=<Z> (raw scipy Z, skip clustering math), tree=<SplitTree> (skip clustering entirely), clusters=[...] (parallel grouping vector for two-level cluster), parent=True\|<frac> (render centroid tree above per-block trees). Visual gap whitespace lives on the panel as c.sectors(...) — see Sectors.
.add_axhline(y, **opts) / .add_axvline(x, **opts) color, linewidth, linestyle, alpha, axes-fraction span limits (xmin/xmax on axhline, ymin/ymax on axvline)
.add_axline(xy1, xy2) / .add_axline(xy1, slope=) infinite line through two points or point + slope (matplotlib axline, ggplot geom_abline), clipped to the frame; color, linewidth, linestyle, alpha, label. slope= requires linear x and y scales
.add_axhspan(ymin, ymax, **opts) / .add_axvspan(xmin, xmax, **opts) color, alpha, label
.add_hlines(ys, xmins, xmaxs, **opts) / .add_vlines(xs, ymins, ymaxs, **opts) data-coordinate segments (scalars broadcast against sequences); unlike axhline/axvline they participate in autoscaling; color, linewidth, linestyle, alpha, label
.add_rect(x, y, w, h, **opts) / .add_polygon(xs, ys, **opts) / .add_polyline(xs, ys, **opts) data-coord shapes — polygon is closed-and-fillable, polyline is open stroke-only. rect/polygon follow the bar convention: fill= fill color ("none" for unfilled), color= outline stroke
.add_text(df, aes(x=, y=, label=), **opts) / .add_annotate(text, xy=, xytext=, **opts) ha, va, fontsize, arrow=True/False

Notes

  • Grouped series get auto-labels and tab10 colors; the palette is overridable per-artist via palette= — a name string ("Set2"), a color list, or a {category: color} dict (see Palettes).
  • color= is the stroke and fill= is the fill — independent aesthetics. So an outlined bar is aes(fill="col"), color="black"; previously inexpressible.
  • When aes(color=) and aes(linestyle=) (or aes(alpha=)) map the same column, the existing color legend swatches inherit the dash / opacity — the canonical pattern for colorblind-safe or B&W-print redundancy.
  • Reference lines / spans default to black, are drawn outside the data color cycle, and don't participate in autoscaling.
  • On scatter, aes(size=) maps a numeric column to per-point radius (px, rescaled into sizes=(min, max) — default (2, 7)); aes(style=) cycles markers per unique value (o, s, ^, v, x, +). color, group, size, style, alpha all compose.
  • .add_imshow emits one <rect> per cell for small grids (≤10000 cells, vector-clean at any zoom) and a base64 PNG above that. On .add_heatmap, a numeric x gives a continuous axis that share_x-aligns with a scatter/line; a string x gives categorical bands so a top/left dendrogram pairs cleanly via share_x / share_y (or attach_above / attach_left, which auto-share).
  • On both, annot=True overlays each cell's value as a text label (correlation / confusion matrices). annot=<2D array> uses custom labels — numbers formatted via fmt, strings verbatim — so labels independent of cell values (e.g. significance asterisks over a correlation cmap) are a string-array away. fmt=".2g" is the format spec (passed to format(value, fmt)); palette-mode heatmap labels skip fmt and render verbatim (identifiers/counts, not measurements). annot_color="auto" picks black or white per cell via luminance; pass any CSS color for uniform text.
  • Cluster gaps. Heatmap row/column clusters are declared via c.sectors({cluster: [members]}, axis="x" | "y") on the panel — the category scale picks up the implied split positions and inserts a 6-px (default) gap at every block boundary, and the heatmap reorders cells at draw time to match the sector cat order. Pass the same grouping info as a parallel list to .add_dendrogram(clusters=[...]) and it runs scipy per block for within-block leaf order plus once more on the per-block centroids for between-block order — a two-level cluster. The dendrogram exposes the resulting leaf order via axis_order, so the heatmap follows automatically when both share a category axis (attach_above / attach_left). parent=True on the dendrogram also renders the centroid tree above the per-block trees in the same panel. See plotlet-cookbook/heatmaps/ for the worked examples.

Clustering helpers

Independent of any artist; the built-in dendrogram builds on these, and so can any third-party tree renderer.

call returns notes
pt.linkage(data, labels=, method=, metric=) SplitTree One scipy.linkage on data; wraps as a one-block SplitTree.
pt.linkage_split(data, split=, labels=, method=, metric=) SplitTree Two-level cluster: scipy.linkage per group (within-block) plus scipy.linkage on the per-group centroids (between-block order).
pt.SplitTree(blocks=, between_order=, between_Z=) dataclass blocks: [(Z, labels), ...] + display order + the centroid linkage. Pass as dendrogram(tree=...) to skip redundant scipy work when the same cluster drives multiple charts.

Deeper layout helpers — layout_tree(tree), layout_parent(tree), fit_parent(...), leaf_position(...), block_apex_centers(...), parent_leaf_px(...), build_tree(...), tree_frame_defaults(...) — exist for writing new tree-shaped artists; import via from plotlet.cluster import <helper>. See EXTENDING.md.

Color shortcuts

  • "C0", "C1", … → tab10 cycle (wraps past "C9")
  • Named: "blue", "orange", "green", "red", "purple", "brown", "pink", "gray", "olive", "cyan" — tab10-flavored, not CSS
  • Single-letter: "b", "g", "r", "c", "m", "y", "k", "w"
  • Grayscale strings: "0" (black) – "1" (white), e.g. "0.5"
  • (r, g, b) / (r, g, b, a) tuples of floats in [0, 1]
  • Any hex / CSS color string passes through

Palettes

  • pt.palette(name) → list of hex colors. Qualitative names (pt.list_palettes(): "Set1""Set3", "Pastel1"/"Pastel2", "Dark2", "Accent", "Paired", "tab10"/"tab20"/"tab20b"/"tab20c", "colorblind") return the full palette; n truncates or cycles past the end.
  • "colorblind" is the Okabe–Ito colorblind-safe set (8 colors, black first) — safe under deuteranopia, protanopia, and tritanopia.
  • pt.palette(name, n) also samples any continuous colormap (pt.list_colormaps()) at n evenly-spaced points — e.g. pt.palette("viridis", 12) for 12 hex colors, no matplotlib round-trip.
  • "_r" suffix reverses either kind.
  • Artists' palette= accepts a name string directly: c.add_bar(aes(..., fill="group"), palette="Set2") — alongside the existing list and {category: color} dict forms.

Datasets

pt.load_dataset(name)dict[col, list], the shape pt.chart() accepts directly; pt.list_datasets() for names. Bundled: "penguins" (scatter/grouping), "flights" (month × year heatmap), "anscombe" (regression/facets), "tips" (categorical: bar, box, violin). Numeric columns arrive as float/int with missing values as nan; sources and citations in NOTICE.md.

User-defined colormaps

pt.register_colormap("bwr2", ["#2166ac", "white", "#b2182b"])
c.add_heatmap(df, aes(x="col"), cmap="bwr2", center=0)
  • Colors interpolate linearly in RGB, evenly spaced or at explicit stops= (positions in [0, 1], first 0 and last 1, strictly increasing).
  • The reversed name + "_r" variant registers alongside; the name then works everywhere a built-in does — cmap= kwargs, colorbars, pt.palette(name, n).
  • Anchoring the midpoint at a data value is the norm's job: pass center= (or vmin=/vmax=) to the artist rather than encoding data values in stops.
  • Registration is per-process, like register_theme — a serialized journal naming a user colormap needs the same register_colormap call before re-rendering in a fresh process. Built-in names can't be shadowed.

Faceting

g = pt.facet(df, by="species", col_wrap=3)
g.add_scatter(aes(x="bill_length", y="bill_depth"))
g.show()

pt.facet(data, by=) returns a chart-shaped recorder — same mark and frame methods — that splits data by group, replays your calls against each subset, and lays the panels out in a shared-scale grid (share_x / share_y on by default). by= wraps one variable into a near-square grid (col_wrap= fixes the column count); row= / col= lay a two-factor grid instead (the seaborn names; ggplot's facet_grid) — pass one form or the other, not both. Panel titles default to the group label; forwarded chart opts (theme=, data_width=, xlabel=, …) apply to every panel. See help(pt.facet) for details.

Inset axes

c.add_line(df, aes(x="x", y="y"))
inset = c.inset(rect=(0.55, 0.55, 0.42, 0.4), xlim=(0, 1), ylim=(0.8, 1))
inset.add_line(df, aes(x="x", y="y"))

c.inset(rect=(x, y, w, h)) returns a fresh Chart sized as a fraction of the parent's data area (origin at the bottom-left). It has its own scales, ticks, and frame; record artists on it normally. The parent's to_svg embeds the inset on top of the data layer.