Skip to main content

mosaickit

mosaickit

PyPI Python License Tests

A domain-neutral toolkit for assembling two-dimensional diagrams from reusable scenes, layers, styles, parameters, and renderers.

Domain libraries define their own models and semantic roles; MosaicKit composes their visual layers and renders the result. It knows nothing about any domain and does not depend on a model package or a curve-fitting library.

Installation

Add MosaicKit to a project with uv:

uv add mosaickit

A plain pip install mosaickit also works. MosaicKit supports Python 3.10–3.13. GlyphKit (>=1.0.0b2,<2) is installed as a required dependency for font processing.

Python API

from mosaickit import (
    Canvas,
    Fill,
    FillLayer,
    MarkerLayer,
    PathLayer,
    Stroke,
    TextLayer,
    quadrant_axes,
)

canvas = Canvas().extend(quadrant_axes(10, 10))
canvas.add(
    FillLayer(
        [(1, 1), (1, 7), (8, 1)],
        fill=Fill(color="#377EB8", opacity=0.12),
        z_index=-1,
    )
)
canvas.add(
    PathLayer(
        [(1, 8), (2, 5), (4, 3), (7, 1.5), (9, 1)],
        stroke=Stroke(color="#984EA3", width=2),
        id="curve",
    )
)
canvas.add(MarkerLayer([(4, 3)], id="point"))
canvas.add(TextLayer((4, 3), "A", offset=(8, 8)))
canvas.save("diagram.svg")

Canvas.add(), extend(), remove(), and clear() are fluent builder operations. snapshot() returns an immutable Scene; copy() and bind() produce a new canvas without changing the original. Public layers never contain Matplotlib artists, and importing mosaickit does not import Matplotlib.

Scenes and layers

The scene graph is deliberately small:

  • PathLayer joins ordered finite (x, y) coordinates with straight segments.
  • FillLayer describes filled regions.
  • MarkerLayer, TextLayer, and ArrowLayer add annotations.
  • RegionLabelLayer names a filled region (inside it, or as a callout), and PointLabelLayer names a point with text placed right beside it. Both are placed after everything else is drawn so they cover no line, marker, filled region, or other text; a LayoutWarning names any label that cannot avoid everything.
  • LegendLayer builds legends from stable layer IDs.
  • AxisMarkLayer, AxisNoteLayer, and BraceLayer annotate values and spans on an axis.
  • SpanBraceLayer braces a horizontal or vertical span between two points inside the plot, with its label placed so it covers nothing (on a leader when there is no room).
  • GroupLayer groups layers without adding renderer-specific state.

AxisSpec and build_axes() construct axes from the same ordinary path and text layers. quadrant_axes(), crosshair_axes(), and box_frame() cover common layouts. CanvasSpec owns ranges, physical size, DPI, labels, and title.

Saving to a .tex path writes the scene as TikZ, for a Canvas or a CanvasGrid; pass standalone=False to get just the tikzpicture. Paths are written as polylines unless a geometry package registers a serializer for its curve type with register_path_serializer, which lets it emit native Bézier segments without MosaicKit depending on it. Curve construction itself stays outside MosaicKit.

Axis marks, notes, and braces

from mosaickit import AxisMarkLayer, AxisNoteLayer, BraceLayer, Canvas, quadrant_axes

canvas = Canvas().extend(quadrant_axes(10, 10))
for value, symbol, note in [(7, "a_1", "Upper\nvalue"), (5, "a_0", "Lower\nvalue")]:
    canvas.add(AxisMarkLayer("y", value, symbol, math=True))
    canvas.add(AxisNoteLayer("y", value, note))
canvas.add(BraceLayer("y", 5, 7, "Span", side="outside"))

The y axis is the plot's left edge and the x axis its bottom edge; the space outside them is the gutter. Columns run outward from the axis: marks, then outside braces, then notes, each as wide as its widest text. Marks and notes are spread along the axis so none overlap, keeping their order and moving as little as possible. An "inside" brace sits just inside the plot and its label is placed so it covers no line, point, region, or text. Notes use the axes.note role, which the default theme sets smaller and lighter than marks.

Saving grows the canvas just enough to include text drawn past its edges and never crops; pass expand=False to save() to keep the exact CanvasSpec size. Axis titles from build_axes() sit past the arrow tips: the x title to the right, the y title above.

Styles and themes

Set values shared by every role with defaults. Individual roles and layer styles can still override them:

from mosaickit import StyleBundle, TextStyle, Theme

paper = Theme(
    "paper",
    {"title": StyleBundle(text=TextStyle(size=16, color="grey-900"))},
    defaults=StyleBundle(text=TextStyle(size=12)),
)

TextLayer and TextStyle remain public. Color, size, opacity, and rotation continue to be supported. Use GlyphKit for new font selection and processing; MosaicKit's TextStyle.family and TextStyle.weight fields are deprecated. The following legacy example still renders through Matplotlib in this release, but each style supplying a font field emits one DeprecationWarning:

paper = Theme(
    "paper",
    {
        "title": StyleBundle(text=TextStyle(family="Noto Serif TC")),
    },
    defaults=StyleBundle(text=TextStyle(family="Noto Sans TC")),
)
from mosaickit import Config, Stroke, StyleBundle, Theme, use_config

paper = Theme(
    "paper",
    {"mypkg.boundary": StyleBundle(stroke=Stroke(color="#984EA3", width=2))},
)

with use_config(Config(theme=paper)):
    canvas = Canvas()

Style resolution proceeds from layer style to canvas role override, configuration role override, theme, and finally primitive defaults. A field set to None inherits; falsey values such as opacity=0, width=0, and LegendStyle(visible=False) remain explicit overrides.

Colors by name

Style color and edge_color fields take a Color, a "#hex" string, or a palette name such as "blue". Names stay names in themes and styles; they are looked up in the active palette (Config.palette, default DEFAULT_PALETTE) when a canvas renders, so renderers only ever receive concrete colors. The built-in default theme names its colors (primary is "blue", secondary is "red", accent is "teal", neutrals are the grey-* names), so changing a color once in the palette recolors every role that names it:

from mosaickit import (
    DEFAULT_PALETTE,
    Canvas,
    Config,
    Palette,
    Stroke,
    StyleBundle,
    Theme,
    use_config,
)
from mosaickit.themes import default

palette = Palette("brand", {**DEFAULT_PALETTE.colors, "blue": "#0072B2", "accent": "#984EA3"})

# A domain package can name colors in its theme without knowing the palette.
mypkg = Theme(
    "mypkg",
    {
        **default.roles,
        "mypkg.line": StyleBundle(stroke=Stroke(color="blue", width=2)),
        "mypkg.boundary": StyleBundle(stroke=Stroke(color="accent")),
    },
)

with use_config(Config(theme=mypkg, palette=palette)):
    canvas = Canvas()  # primary and mypkg.line draw in #0072B2

A name the palette does not define raises ConfigurationError at render time, naming the role, the style field, and the palette. A Python Palette replaces the default palette, so start from DEFAULT_PALETTE.colors (as above) to keep the names the built-in theme uses.

Domain packages register dotted role names through ThemeRegistry. Typed RolePack dataclasses and expand_roles() let those packages author themes without making MosaicKit import their domain types.

Configuration can also be loaded from TOML:

[canvas]
x_range = [0, 20]
dpi = 150

[palette]
blue = "#0072B2"     # override a default color
accent = "#984EA3"   # add a new name

[styles."mypkg.boundary".stroke]
color = "accent"     # a palette name or a "#hex" value
width = 2
dash = "dashed"

[styles."mypkg.line".stroke]
color = "blue"

The [palette] table layers named hex colors over DEFAULT_PALETTE, and the loaded palette is available as config.palette. Overriding blue there recolors the built-in primary role and any theme or style that names blue, with no styles needed. Style color and edge_color values accept palette names as well as hex; an unknown name raises ConfigurationError naming the file and key when the file is loaded.

Use Config.load(path) to validate it. Runtime defaults are isolated with contextvars, while explicit constructor arguments always take precedence.

Math fonts

By default math text (TextLayer(math=True), axis marks, and any label written as $...$) goes through Matplotlib's mathtext, so letters come out in DejaVu Sans. To set math in a font of your choice, give the configuration a MathFont:

from mosaickit import Canvas, Config, MathFont, TextLayer, TextStyle, use_config

config = Config(math_font=MathFont("fonts/STIXTwoMath.otf"))
with use_config(config):  # or Canvas(config=config)
    canvas = Canvas().add(TextLayer((4, 3), r"u(x_1, x_2) = x_1^\alpha x_2^{1-\alpha}", math=True))
canvas.save("diagram.svg")

No rc_context is needed, and Matplotlib's global settings are untouched. The same setting works in TOML (a relative path is read from the file's directory):

[math]
font = "fonts/STIXTwoMath.otf"
italic = true   # default: letters in the font's math italic alphabet

The size and color come from the label's TextStyle, like any other text.

  • Fonts are explicit. MathFont takes a path to an OpenType font with a MATH table, such as STIX Two Math, Libertinus Math, or Fira Math. MosaicKit and GlyphKit never search the system for fonts, and GlyphKit's FontSource has no installed-name lookup; pass the file. A font without a MATH table, or a file that is not a font, raises ConfigurationError when the canvas renders.
  • Which labels. A label whose whole text is one $...$ span (or a math=True text) is laid out by GlyphKit and drawn from its glyph outlines, in TextLayer, PointLabelLayer, RegionLabelLayer, AxisMarkLayer, AxisNoteLayer, and brace labels. Placement measures the same layout it draws, so labels keep avoiding each other. Text that mixes plain words with $...$, legends, and the title keep using Matplotlib, as does everything when no MathFont is set.
  • Supported math. GlyphKit parses a TeX-like subset: scripts, \frac, \sqrt, Greek letters, accents, \left...\right, and big operators (see GlyphKit's math grammar). Anything else, such as \mathrm or \cdot, raises RenderError naming the formula instead of silently changing fonts. TikZ export is unchanged.

Scene documents

A Scene can be written as a versioned, JSON-compatible document and read back, so a figure can be stored, diffed or handed to another program without Python objects.

from mosaickit.document import scene_from_json, scene_to_json

text = scene_to_json(canvas.snapshot())
scene = scene_from_json(text)
  • Format. {"format": "mosaickit.scene", "version": 1, "metadata": {...}, "layers": [...]}. Anything that is not a number, string, boolean or null is an object with a "$" tag naming its kind (a layer class, a style, a color, an enum member, a Parameter or an expression), and sequences are lists. An unbound scene keeps its parameters and expressions; a bound snapshot holds resolved numbers.
  • Strict loading. An unknown version, tag, field or operation, a missing field, a non-finite number, or a value a layer rejects raises ConfigurationError with the path, for example layers[3].anchor: .... Documents are limited to MAX_NODES values, MAX_DEPTH levels and MAX_STRING characters per text, so an untrusted document can be checked before anything is built.
  • Not serialized. A layer's model link, and geometry that is not plain points (a layer holding a BezierKit curve, for example, raises ConfigurationError naming the layer).
  • Compatibility. Version 1 is frozen: tests/fixtures/scene_v1.json must keep loading, and a scene read from it is written back with the same values. An optional field added later (such as TextStyle.font) is written as null when unused. Loading is strict about unknown fields, so a document written with a field this release does not know is refused. A change that is not additive is a new version, read alongside the old.
  • Scope. This is data only. Diffs between documents and ways to send one belong elsewhere.

Text fonts

Text is set in one of three generic fonts, chosen with TextStyle.font: "sans" (the default), "serif" or "mono". MosaicKit supplies all three, DejaVu Sans, DejaVu Serif and DejaVu Sans Mono from the files Matplotlib installs, and GlyphKit lays them out, so a figure looks the same on every machine and nothing searches the system for fonts.

from mosaickit import Canvas, Config, TextFonts, TextLayer, TextStyle

canvas = Canvas()
canvas.add(
    TextLayer((5, 5), "Total revenue", style=TextStyle(size=12, font="serif", weight="bold"))
)

To use other fonts, pass the files. Each slot you give replaces that generic font; the others keep their defaults.

fonts = TextFonts(
    sans=("fonts/SourceSans3-Regular.ttf", "fonts/SourceSans3-Bold.ttf"),
    mono="fonts/JetBrainsMono-Regular.ttf",
)
canvas = Canvas(config=Config(fonts=fonts))
[fonts]
sans = ["fonts/SourceSans3-Regular.ttf", "fonts/SourceSans3-Bold.ttf"]
mono = ["fonts/JetBrainsMono-Regular.ttf"]
  • Weight. TextStyle.weight is "normal" (400), "bold" (700) or an integer from 1 to 1000. GlyphKit uses the nearest listed face, or sets the wght axis of a variable font. It never makes a bold face up: list one, or the nearest face is used.
  • Fonts are explicit. Paths only. A file that is not a font raises ConfigurationError when the canvas renders. The order of the files in a slot is the order a face is tried for a character the first lacks. A character no listed face has raises RenderError naming the text: the default fonts have no CJK glyphs, so Chinese, Japanese and Korean text needs a font file in the slot.
  • Which text. Strings without $...$ in TextLayer, labels, axis marks and notes, and brace labels. Math keeps using the math font, text mixed with math, legends, arrow labels and the title keep using Matplotlib's default font.
  • TextStyle.family is deprecated. A style that still sets it is drawn by Matplotlib with that installed family, as before. Use font and TextFonts instead; weight is not deprecated.
  • TextFonts(paths) from 0.9.0 still works: the first argument is the sans slot, and [fonts] files is another name for sans.

Grids, parameters, and animation

Grid sweeps and parameter binding remain supported. The Animation.sweep() line below is a deprecated compatibility example:

from mosaickit import Animation, Canvas, CanvasGrid, Parameter, TextLayer

position = Parameter("position", value_type=float)
template = Canvas().add(TextLayer((position, 5), "moving"))
values = position.values([1.0, 3.0, 5.0])

CanvasGrid.sweep(template, values, cols=3).save("sweep.svg")
Animation.sweep(template, values, fps=2).save("sweep.gif")  # Deprecated: use KineticKit.

CanvasGrid accepts flat canvases, nested rows, None cells, and Span(canvas, rows=..., cols=...). A flat list infers a roughly square grid; explicit rows and cols may leave trailing cells empty.

Parameters and expressions support arithmetic, ordered comparisons, partial binding, and stable structural equality. Animation.frames() yields backend-neutral scenes. GIF output uses Pillow; MP4 output requires ffmpeg.

Font and animation migration

Use GlyphKit for font processing and KineticKit for new animation work. Install KineticKit separately when needed; MosaicKit does not depend on it. This transition release retains the Matplotlib font path (TextStyle.family) and exports Animation from both mosaickit and mosaickit.canvas. Direct construction and Animation.sweep() each emit one DeprecationWarning at the caller. Frame iteration, GIF/MP4 saving, and the existing Matplotlib playback helper continue to work. Internal style copying and rendering do not repeat warnings.

The next release may remove the legacy family/weight font-selection path and MosaicKit's animation APIs and renderer helpers. Migrate those uses before upgrading; retain TextLayer, non-font TextStyle fields, CanvasGrid, and parameter binding for static scene composition. No new cross-library scene adapter is introduced in this release.

Rendering

PNG, SVG, and PDF use the built-in Matplotlib renderer:

result = canvas.render()
try:
    result.show()
finally:
    result.close()

canvas.save() closes its temporary render result automatically. The public Renderer protocol and RendererRegistry allow other backends without placing backend state in the scene graph. Render plans and contexts remain private while the built-in backends evolve.

Each render, grid, or animation job owns its cache. Pass an explicit RenderCache to reuse work across jobs. Non-hashable models bypass caching and emit one warning per model type and cache.

Scope

MosaicKit owns domain-neutral scene composition, styles, themes, parameter binding, grid layout, renderer contracts, and static output. GlyphKit owns font processing; KineticKit owns animation. MosaicKit retains deprecated font and animation compatibility paths for this release. Domain semantics belong to the packages that build on it. Mathematical curve construction and native TikZ Bézier segments belong to geometry packages such as BezierKit, which plug into the scene-level TikZ export through a path serializer.

Development

uv sync --locked
uv run pre-commit install
uv run pytest
uv run ruff check .
uv run ruff format --check .
uv run mypy
uv run lint-imports
uv build

The lockfile pins all development dependencies.

Metadata

Release files for mosaickit 0.9.1

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

Source distribution (sdist)

Source distribution for mosaickit 0.9.1
File Size Uploaded Provenance
mosaickit-0.9.1.tar.gz 276.6 kB With provenance Details

Built distribution (wheel)

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

Total release size: 393.4 kB

Release files / mosaickit-0.9.1.tar.gz

Download URL mosaickit-0.9.1.tar.gz
Size 276.6 kB
Tags Source
SHA-256 checksum
How to use checksums
ca0ca3fd7ecf138222fe1b3566a1f03cc20bef7b6ab267e8e248d5bae25f91ff
BLAKE2b-256 checksum
How to use checksums
76f9b77afe747835dbe6f655a8fc21e45f27bbe078dfed720504730becb70cd9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 21, 2026.

Transparency log

Release files / mosaickit-0.9.1-py3-none-any.whl

Download URL mosaickit-0.9.1-py3-none-any.whl
Size 116.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
883e2ae1798fb8abb59f94bf2ca8910d4d5c5e779c611d1e5149e91474573b58
BLAKE2b-256 checksum
How to use checksums
15c62e53c5cec688bb030fffffb1d399bb00bf93a71c04e0562ca6f8aebae7f6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 21, 2026.

Transparency log

Security for mosaickit 0.9.1

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.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for mosaickit-0.9.1.tar.gz

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 21, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

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