User guide#

Semantic lines#

ggstyle.line() draws named tidy-data columns on a caller-owned Matplotlib axes:

result = gs.line(
    frame,
    x="date",
    y="value",
    color="series",
    linestyle="status",
    style={"linewidth": 2},
    sort="x",
    ax=ax,
)

color= and linestyle= are column mappings. Fixed Matplotlib properties belong in the separate style= mapping; supplying a mapped aesthetic there is an error before the axes changes. The result retains the exact axes, ordinary Line2D artists, and read-only trained scales through ggstyle.LineResult.

Categorical color and linestyle imply grouping. group= partitions lines without assigning an aesthetic and may be combined with those mappings. Numeric color is continuous; because a Line2D has one color, that value must be constant inside every resolved line. Use an explicit group column when several constant-valued lines share one continuous mapping. Boolean and pandas categorical color remain discrete.

The default sort="input" preserves row order. sort="x" stably orders each line by x; duplicate positions retain input order and are never aggregated. Missing explicit group values use the visible group_missing= policy: "drop" (default), "keep", or "raise". Dropped rows and accessibility warnings appear in result.diagnostics.

Default inference can be overridden with immutable scale policy:

result = gs.line(
    frame,
    x="date",
    y="value",
    color="code",
    color_scale=gs.DiscreteScale(
        order=(1, 2, 3),
        values=("#0072B2", "#D55E00", "#009E73"),
        missing="drop",
        name="Series",
    ),
    ax=ax,
)

Use ggstyle.ContinuousScale with a sequential or diverging ggstyle.Palette to set explicit limits and missing/infinite policy. A ggstyle.DiscreteScale is aesthetic-independent until applied, so custom values must be hexadecimal colors for color_scale= or one of the supported named line styles for linestyle_scale=.

Scales are shared by aesthetic and source-column name across calls on one axes. Discrete assignments remain stable as levels are added. Expanding an automatic continuous domain recolors earlier lines managed by this helper so all participating layers remain consistent. Calls sharing a mapping must therefore use the same explicit scale policy; conflicts fail before drawing. An externally removed complete layer is pruned on the next mapped call.

The data helpers perform no aggregation, smoothing, interpolation, or axes creation. They validate scale and grouping policy before drawing and roll back partial artists, axes state, earlier colors/styles, and registry state if rendering or date-axis refresh fails. If the axes already has a ggstyle date handle, the handle is refreshed automatically, including in collapsed mode.

Semantic points and ribbons#

ggstyle.points() reuses the line helper’s x, y, color, group, color_scale, and fixed style vocabulary. Discrete color creates one native PathCollection per level; continuous color is mapped independently for every point. The helper does not aggregate or jitter observations.

points = gs.points(
    frame,
    x="date",
    y="value",
    color="score",
    style={"marker": "o", "size": 32, "edgecolor": "white"},
    ax=ax,
)

ggstyle.ribbon() draws caller-provided lower and upper columns with native PolyCollection artists. It never computes an interval or synthesizes a legend label. Missing coordinates break a ribbon by default; missing="drop" explicitly connects across the gap, while missing="raise" rejects it. Crossed bounds are allowed unless validate_order=True.

band = gs.ribbon(
    intervals,
    x="date",
    lower="low",
    upper="high",
    color="series",
    label="95% interval",
    alpha=0.2,
    ax=ax,
)

Line, point, and ribbon calls on one axes share trained color state when they map the same source-column name. A later automatic-domain expansion transactionally updates all earlier managed artists. Continuous ribbon color, like continuous line color, must be constant within each resolved group.

Automatic semantic guides#

Call ggstyle.guides() after adding semantic layers. It reads the complete trained registry and returns native Matplotlib legends and colorbars through ggstyle.GuideResult:

gs.line(frame, x="date", y="value", color="series", ax=ax)
gs.points(events, x="date", y="value", color="series", ax=ax)
guide_result = gs.guides(ax)

Discrete mappings create legends; continuous color mappings create colorbars. A scale’s name= becomes its guide title, or the source-column name is used by default. Missing discrete values mapped by scale policy receive an explicit (missing) entry, and requested unobserved levels remain visible.

Color and linestyle guides merge only when they use the same source variable, title, ordered levels, and missing entry. Different variables or titles stay separate. This is deliberately stricter than merging guides merely because their displayed labels happen to match.

Once guide construction is activated, subsequent semantic layer calls refresh the managed guides after successful scale training. A failed legend or colorbar build leaves the previous guides, artists, and registry revision unchanged. Repeated calls within one revision return the same native guide objects. Pass enabled=False to remove managed guides and disable live refresh.

Caller-owned legends and colorbars are never replaced or removed. Semantic legends are available through GuideResult.legends rather than ax.get_legend(), which remains reserved for a caller-owned axes legend. Automatic placement is intentionally bounded to four distinct legends and four distinct colorbars per axes; larger layouts should use facets or explicit Matplotlib guide construction.

Plot finishing#

ggstyle.finish() applies plot and axis labels to an existing Matplotlib Axes as one validated transaction. It returns a ggstyle.FinishResult containing the same axes, the native text artists affected by the request, and the immutable plan that was applied:

result = gs.finish(
    ax,
    title="Revenue",
    subtitle="Trailing twelve months",
    caption="Source: annual report",
    x=gs.axis(title="Date"),
    y=gs.axis(
        title="Revenue",
        labels=gs.label_currency("$", scale=1_000_000, suffix="M"),
    ),
)

The ggstyle.axis() factory returns an immutable ggstyle.AxisSpec. Numeric labellers are installed through the same explicit FuncFormatter adapter available as ggstyle.as_formatter().

Subtitles follow Matplotlib’s active left, centre, or right title location. Captions are right-aligned beneath the corresponding axes. Both are ordinary Text artists marked as participating in layout. If their figure has no layout engine, finish enables constrained layout; an existing tight, constrained, or custom engine is preserved. Multiline outer text contributes its full bounds to constrained layout.

Repeated calls update the same managed subtitle and caption artists. None leaves a managed value unchanged, while subtitle=False or caption=False removes it. An empty string clears a standard plot or axis title. If managed text was removed through Matplotlib directly, a later explicit value creates a safe replacement.

Pass dry_run=True to receive a ggstyle.FinishPlan without drawing a canvas or changing axes, artists, layout, or global rcParams. Commit failures restore label text, formatters, managed artists, and layout state. Data artist coordinates, transforms, labels, and colours are never changed.

Use ggstyle.FinishPlan.as_dict() for a fresh JSON-compatible representation or ggstyle.FinishPlan.describe() for deterministic formatted JSON. Both include nested policy and diagnostics without retaining artists or callables; see Inspection.

The coordinator also accepts a theme recipe; see Parameterized themes for the existing-axes safety boundary. Saving is deliberately separate through ggstyle.save(). Endpoint labels are an explicit finishing policy rather than unvalidated keyword forwarding.

Direct endpoint labels#

For labelled line series, ggstyle.end_labels() replaces legend lookup with labels anchored to the final finite data point:

ax.plot(period, revenue, label="Revenue")
ax.plot(period, forecast, label="Forecast")

result = gs.finish(
    ax,
    direct_labels=gs.end_labels(collision="avoid", fallback="legend"),
)

Each annotation is an ordinary Matplotlib Annotation in result.artists. Its data anchor remains the endpoint, its text uses the line colour, and only a display-space vertical offset is used to separate nearby labels. The annotations participate in constrained layout so the figure allocates a right margin. A successful direct-label operation removes the axes legend without changing line data, transforms, labels, or colours.

Participation follows Matplotlib’s public legend labels: labels beginning with an underscore are ignored. Every visible participant must be an ordinary Line2D on a rectilinear axes using that axes’ data transform, with a finite endpoint inside the current view. Mixed line/scatter plots therefore do not receive partial direct labels. If any participant is unsupported, an endpoint is outside the view, or all labels cannot fit vertically, fallback="legend" creates a conventional legend for the complete set. fallback="raise" instead fails during preflight before any finishing mutation.

collision="none" leaves every text offset at the exact endpoint height. direct_labels=False removes endpoint annotations previously managed by finish; None leaves them unchanged. Repeating an explicit request updates and reuses attached annotations. Call finish again after changing the participating lines. Dry runs perform the same eligibility and collision preflight without drawing a canvas.

This feature is deliberately narrower than general text repulsion: collections, bars, arbitrary annotations, and non-Cartesian axes use the explicit fallback policy.

Figure export#

ggstyle.save() exports an explicit matplotlib.figure.Figure. It never guesses the current figure, and both output dimensions are required:

path = gs.save(
    figure,
    "report.png",
    width=7,
    height=4,
    units="in",
    dpi=300,
    metadata={"Creator": "ggstyle"},
)

units accepts inches, centimetres, millimetres, or pixels. Pixel dimensions are converted through the explicit DPI even for vector output. The format is inferred from a recognized filename extension; an explicit format= is required when there is no extension and must agree with an existing extension.

The default bbox="tight" crops the nominal canvas to all decorated content with 0.1-inch padding. Use bbox="standard" when the final raster or vector canvas must retain exactly width by height. Output is opaque unless transparent=True. Metadata keys are backend-specific strings passed through to Matplotlib; SVG and PDF timestamps are suppressed unless explicitly supplied, and SVG identifiers use a stable salt for repeatable output.

Overwrite is an explicit policy. The default raises FileExistsError before rendering. With overwrite=True, rendering still occurs in a same-directory temporary file and the destination is replaced only after a non-empty artifact succeeds. Render failures leave the old destination intact, temporary files are cleaned up, and the figure’s original physical size and global rcParams are restored before publication. Parent directories are never created implicitly.

Date-axis model#

A ggstyle.DateAxis is attached to one matplotlib Axes. Repeated calls to ggstyle.dates() return the same handle. The handle has two coordinate modes:

show

Use matplotlib date numbers. Missing calendar dates occupy space.

collapse

Use a registered scale that maps sorted, unique observations to ordinal display positions. Artist data and limits remain matplotlib date numbers.

Ticks and labels#

ggstyle.DateAxis.ticks() controls positions. Named cadences include daily, weekly, monthly, quarterly, and yearly. Anchored forms such as month-start and month-end control which observation represents a period.

ggstyle.DateAxis.fmt() controls text without changing positions. Presets include concise, month-year, quarter, year, iso, and time.

Numeric labels#

Numeric label factories are separate from date formatting. They return immutable, one-value callables that can also be used for report text. Use ggstyle.as_formatter() to cross the Matplotlib boundary explicitly:

currency = gs.label_currency("$", scale=1_000_000, decimals=1, suffix="M")
ax.yaxis.set_major_formatter(gs.as_formatter(currency))

percent = gs.label_percent(scale=1.0, decimals=1)
assert percent(0.125) == "12.5%"

ggstyle.label_number() provides fixed decimal precision and optional comma grouping. ggstyle.label_si() selects a power-of-1000 prefix such as k, M, or µ and accepts an explicit unit.

The scale argument is always explicit. For percentages it is the input value that means 100 percent; for number and currency labels it is a positive divisor. Formatting uses Python’s fixed-point, round-half-even behavior and never reads the process locale. Negative values use a leading minus by default or parentheses when negative="parentheses". The complete default labels for non-finite values are "NaN", "∞", and "-∞"; callers may replace the first two strings.

Creating a labeller or adapter does not install it, alter axis limits, or mutate global Matplotlib settings. Installation remains an ordinary Matplotlib operation, and the returned matplotlib.ticker.FuncFormatter remains available for further customization.

Palettes#

ggstyle.palette() returns an immutable ggstyle.Palette. The qualitative palette is the eight-colour cycle shared by every ggstyle theme. Selection preserves its reviewed order and a request above eight raises instead of silently creating colours that are difficult to distinguish:

series_colors = gs.palette("qualitative", n=4).colors
ax.set_prop_cycle(color=series_colors)

Sequential and diverging palettes provide deterministic CIELAB interpolation over an already-normalized interval. Diverging samples require an odd count so the neutral colour is always represented, and midpoint= controls its normalized position:

sequential = gs.palette("sequential", n=5)
diverging = gs.palette("diverging", n=5, midpoint=0.4)

assert sequential.at(0.0) == sequential.colors[0]
assert diverging.at(0.4) == "#F7F7F7"

None and NaN return missing_color. Values outside zero through one are clipped by default; out_of_bounds="raise" rejects them, while "color" requires explicit under_color and over_color values. These policies are part of the palette object and survive resampling.

The qualitative cycle has regression gates for pairwise separation under the Machado–Oliveira–Fernandes colour-vision simulations and against light, grey, and dark theme surfaces. Sequential lightness and both sides of the diverging palette are also ordered under those simulations. These tests reduce predictable accessibility failures; they do not replace checking a finished figure with its actual line weights, markers, background, and labels.

Palette construction is pure: it neither imports Matplotlib nor changes rcParams. The public semantic helpers train color domains using these palette policies through ggstyle.ContinuousScale; ggstyle.guides() renders the corresponding native colorbars.

Ranges#

ggstyle.DateAxis.zoom() accepts partial strings. "2024" covers the full year, and "2024-03" covers the full month. last= measures backward from the final observation rather than from the current date.

Collapsed axes#

Call ggstyle.DateAxis.collapse() to remove unobserved gaps and ggstyle.DateAxis.expand() to restore calendar spacing. The observations come from plotted lines, scatter offsets, native fill_between polygons, and any explicit data= passed to ggstyle.dates(). Lines, scatter, fill_between, and native data-space annotations all pass through the same scale without having their geometry rewritten. Midpoint-stepped polygons are the exception: Matplotlib retains their generated midpoints instead of all source x values, so fill_between(..., step="mid") requires the complete dates through data=.

Annotations#

Use ggstyle.DateAxis.loc(), ggstyle.DateAxis.vline(), and ggstyle.DateAxis.span() for coordinates that remain correct in both modes. ggstyle.DateAxis.loc() returns a native matplotlib date number in either mode; collapsed display positioning belongs to the registered scale. Native calls such as ax.axvline(timestamp) therefore work as expected.

The artists created by ggstyle annotation helpers are available through ggstyle.DateAxis.annotation_artists for ordinary Matplotlib styling. Call ggstyle.DateAxis.clear_annotations() to remove every managed annotation; externally removed artists are tolerated.

Axis summaries and captions#

ggstyle.DateAxis.summary() returns an immutable ggstyle.AxisSummary instead of requiring callers to inspect locators or artists. It records the observation range, inferred frequency, resolved cadences, display timezone, coordinate mode, and number of explicitly dropped dates.

Use ggstyle.DateAxis.caption() to format the same semantics for a report:

handle = gs.dates(ax)
metadata = handle.summary()
caption = handle.caption()          # return text only
handle.caption(add=True)            # also draw below the axes

Missing dates#

Missing values in explicitly supplied date data raise by default. Dropping them must be requested and remains visible in the summary:

handle = gs.dates(ax, data=dates, missing="drop")
assert handle.summary().missing_values == 2

Missing positions already embedded in plotted line artists are preserved as line breaks; they are not treated as discarded source observations.

Synchronized panels#

ggstyle.sync_dates() adopts several axes, attaches them to one live observation registry, and applies common date limits. This matters in collapsed mode: without a common registry, the same date can have a different ordinal position in each panel.

handles = gs.sync_dates(axes, mode="collapse", limits="union")

Use limits="intersection" to display only the overlapping observation range. If the panels already use different modes, pass an explicit mode rather than relying on an arbitrary panel to win.

Call ggstyle.DateAxis.refresh() after adding, changing, or removing plotted artists. Refreshing any synchronized handle rescans every live member, commits one new registry revision, and updates every member scale while preserving date-number view limits. A repeated ggstyle.dates() call refreshes an existing handle as well.

Call ggstyle.DateAxis.dispose() to disconnect callbacks and detach a handle from its registry. Disposal is idempotent and leaves existing Matplotlib artists on the axes.

Themes#

ggstyle.use_theme() changes matplotlib settings process-wide. Prefer the scoped ggstyle.theme context manager in reusable code. Importing ggstyle does not change matplotlib global state.

Nine ggplot2-inspired themes are available: minimal, grey, bw, linedraw, light, dark, classic, void, and test. minimal is the default and test is intended for stable visual tests rather than presentation output. The corresponding ggplot2 spellings, such as theme_bw and theme_classic, are accepted as aliases.

with gs.theme("classic"):
    fig, ax = plt.subplots()

Each theme is also a standalone matplotlib stylesheet returned by ggstyle.stylesheet(). Facet-strip styling has no direct core matplotlib equivalent. The void theme hides axis-label text through static matplotlib settings, which can leave some layout space reserved for a label.

Parameterized themes#

ggstyle.theme_spec() creates an immutable, validated theme recipe. base_size scales the theme’s complete text hierarchy proportionally, base_family replaces its font family, and explicit overrides are applied last:

report_theme = gs.theme_spec(
    "minimal",
    base_size=11,
    base_family="DejaVu Sans",
    overrides={"axes.titlesize": 14},
)

Unknown rcParams, invalid values, and operational settings such as backend are rejected when the recipe is created. ggstyle.theme_params() resolves a recipe to a read-only mapping without mutating global rcParams. This is the integration boundary for code that needs Matplotlib settings rather than a context manager.

The same recipe works process-wide with ggstyle.use_theme(), temporarily with ggstyle.theme, or transactionally on an existing axes:

result = gs.finish(ax, theme=report_theme)

Existing-axes theming updates figure and panel surfaces, spines, major grids, ticks, titles, axis labels, and an existing legend. It does not recolour data artists or alter the axes property cycle, figure geometry, line defaults, save settings, or global rcParams. result.diagnostics names the creation-, data-, and output-time rcParams that were preserved. Theme application participates in the same rollback contract as the rest of ggstyle.finish(), and repeated application reuses existing artists.