Skip to content

Styles and colors

Sparse styles

Every style is a frozen dataclass whose fields may all be None. None means inherit: the value comes from a style below it (Themes and configuration). Falsy values are not None, so opacity=0, width=0 and LegendStyle(visible=False) are explicit overrides.

Definition · Sparse merge

Let \(a\) and \(b\) be styles of the same type with fields \(F\). The style \(a \triangleright b\) (a.merged_over(b)) has, for each \(\phi \in F\),

\[ (a \triangleright b)_\phi = \begin{cases}\begin{aligned}a_\phi & \text{if} a_\phi \ne \text{None}\end{aligned} \\\ \begin{aligned}b_\phi & \text{otherwise}.\end{aligned}\end{cases} \]

The empty style \(\epsilon\) has every field None. For style bundles the merge is taken slot by slot, a None slot acting as \(\epsilon\).

Proposition · Merging is a monoid

For styles \(a, b, c\) of one type, (i) \((a \triangleright b) \triangleright c = a \triangleright (b \triangleright c)\); (ii) \(\epsilon \triangleright a = a \triangleright \epsilon = a\); (iii) \(a \triangleright a = a\). Field by field, \(a_1 \triangleright \cdots \triangleright a_n\) takes the first value that is not None.

A stack of styles can therefore be merged in any grouping. The result acts as a priority list: the first source that sets a field wins.

style.merged_over(base)

The merge of Sparse merge. Styles of different types raise TypeError.

Style types

Stroke(
    color=None,
    width=None,
    dash=None,
    arrow=None,
    opacity=None
)

Line styles. width is in points; dash is a DashStyle (SOLID, DASHED, DOTTED, DASHDOT); arrow is an ArrowStyle (OPEN, TRIANGLE, FANCY, WEDGE) drawn at the layer's ArrowPlacement (START, END, BOTH).

Fill(
    color=None,
    opacity=None,
    hatch=None
)

Styles for region interiors. hatch is a Matplotlib hatch pattern such as "//"; "" means no hatch.

Marker(
    color=None,
    size=None,
    shape=None,
    opacity=None,
    edge_color=None,
    edge_width=None
)

Point-marker styles. As in Matplotlib's scatter, size is the area in square points (36 is a 6 pt disc). shape is a Matplotlib marker such as "o", "s" or "X".

TextStyle(
    color=None,
    size=None,
    family=None,
    weight=None,
    opacity=None,
    rotation=None
)

Text styles. size is in points, family is a font-family name, weight may be a value such as "bold", and rotation is in degrees counter-clockwise.

LegendStyle(
    visible=None,
    location=None,
    frame=None,
    size=None
)

Legend styles, including a Matplotlib location such as "best" or "upper right", a frame setting and the font size.

Sizes, widths and opacities are checked when a style is created: sizes must be finite and non-negative, opacities in \([0, 1]\), rotation finite.

grey-800, width 1.5, solid, opacity 1

grey-200, opacity 0.3, no hatch

grey-800, size 36, "o", opacity 1, edge grey-800 of width 0

grey-900, 12 pt, DejaVu Sans, normal weight, opacity 1, no rotation

visible, location "best", no frame, 10 pt

Colors

Color(
    red,
    green,
    blue,
    alpha=1.0
)
Color.from_hex("#RGB" | "#RRGGBB" | "#RRGGBBAA")

An immutable RGBA color with channels in \([0, 1]\). channels returns the four values, from_channels() builds one, and to_hex(include_alpha=None) writes #RRGGBB, adding AA when include_alpha is true, or by default when alpha is not 1. TRANSPARENT is Color(0, 0, 0, 0).

Proposition · Hex round trip

For every string \(h\) of the form #RRGGBB or #RRGGBBAA in hexadecimal digits, Color.from_hex(h).to_hex(include_alpha=len(h) == 9) equals \(h\) in upper case. A three-digit #RGB is read as #RRGGBB.

A style color or edge_color takes a Color, a "#hex" string (parsed at once) or any other string, which is kept as a palette name.

Palettes

Palette(name, colors)

A named table of colors. Values may be Colors or hex strings. palette[name] looks up a color and raises ConfigurationError, naming the palette, when the name is unknown. name in palette tests whether a name exists.

#222222

Used by the default theme for — Text

#333333

Used by the default theme for — Lines, markers, axes

#666666

Used by the default theme for — Axis notes

#999999

Used by the default theme for — Guides

#CCCCCC

Used by the default theme for — Fills

#E6E6E6

Used by the default theme for —

#FFFFFF

Used by the default theme for — The canvas background

#01A2D9

Used by the default theme for — primary

#E3120B

Used by the default theme for — secondary

#00887D

Used by the default theme for — accent

Themes and styles name their colors, and the names are looked up in the active palette (Config.palette) when a canvas builds its render plan, so renderers only ever see concrete colors. Changing a palette entry recolors every role that names it. A missing name raises ConfigurationError at render time, naming the role, the style field and the palette. A Python Palette replaces the default palette, so build it from DEFAULT_PALETTE.colors to keep the names the built-in theme uses:

from mosaickit import (
    DEFAULT_PALETTE,
    Canvas,
    Config,
    Palette,
    use_config,
)

brand = Palette(
    "brand", {**DEFAULT_PALETTE.colors, "blue": "#0072B2"}
)
with use_config(Config(palette=brand)):
    # primary now draws in #0072B2
    canvas = Canvas()

Comments