Skip to content

Axis annotations

The y axis is the plot's left edge and the x axis its bottom edge; the space outside them is the gutter. Three layer types place text in the gutter, and a fourth draws braces inside the plot.

Marks, notes and braces

AxisMarkLayer(
    axis,
    value,
    label,
    math=False,
    style=None,
    *,
    role="axes",
    ...
)

A short symbol next to axis "x" or "y" at value, outside the plot, such as \(a_0\) or \(p^*\).

AxisNoteLayer(
    axis,
    value,
    text,
    style=None,
    *,
    role="axes.note",
    ...
)

An explanation of value, possibly on several lines, in the outermost gutter column. The axes.note role makes notes smaller and lighter than marks in the default theme (9 pt, grey-600).

BraceLayer(
    axis,
    start,
    end,
    label=None,
    side="inside",
    *,
    role="axes",
    math=False,
    style=None,
    stroke=None,
    ...
)

A curly brace over start..end on an axis, with an optional label. side="outside" draws it in the gutter, past the marks; "inside" draws it just inside the plot and places its label so that it covers no line, point, region or text, on a leader when nothing past the tip is free.

SpanBraceLayer(
    start,
    end,
    label=None,
    side="below",
    *,
    role="axes",
    math=False,
    style=None,
    stroke=None,
    ...
)

A brace between two points inside the plot. The span must be horizontal (side "above" or "below") or vertical ("left" or "right"); the brace bulges to side, 4 pt clear of the points and 8 pt deep, and its label sits past the tip, placed so it covers nothing (Region and point labels).

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"))
canvas.add(AxisMarkLayer("x", 6, "b", math=True))

Marks, notes and an outside brace in the y gutter (guide lines added).

A span brace between two points, labelled to the left.

Gutter columns

Columns run outward from the axis line, starting 5 pt away and separated by 8 pt: marks, then one column per lane of outside braces, then notes. Each column is as wide as its widest item. An empty column takes no space and adds no gap (Marks, notes and an outside brace in the y gutter (guide lines added).). The layout must also keep neighbouring texts within a column from overlapping and decide which braces may share a lane.

Spreading text along an axis

Each text in a column is an interval along the axis, centred on the value it labels. When intervals overlap they are moved apart, keeping their order and moving as little as possible in the least-squares sense.

Definition · Order-preserving packing

Let \(c_1, \ldots, c_n\) be centres, \(s_1, \ldots, s_n \ge 0\) sizes and \(g \ge 0\) a gap, numbered so that \(c_1 \le \cdots \le c_n\) (ties in input order). A packing is a vector \(x \in \mathbb{R}^n\) with

\[ x_{k+1} - x_k \ge (s_k + s_{k+1}) / 2 + g, \quad k = 1, \ldots, n - 1, \]

and the order-preserving packing problem is to minimize \(\sum_k (x_k - c_k)^2\) over all packings.

mosaickit.layout.stack1d.spread(
    centers,
    sizes,
    gap=0.0
) -> tuple[float, ...]

Solves the problem by merging runs of overlapping items into clusters, packing each cluster tightly around the mean of its members' targets, and merging again while a cluster runs into the one before it. The result is returned in input order. Mismatched lengths or a negative size raise ValueError.

Theorem · Spreading is optimal

spread returns the unique solution of the order-preserving packing problem of Order-preserving packing.

The procedure is the pool-adjacent-violators algorithm (PAVA) from isotonic regression Ayer (1955): subtracting the packed offsets turns the gap constraints into \(y_1 \le \cdots \le y_n\).

Corollary · Properties of a spread

Let \(x\) be the result of spread. Then (i) any two items \(i \ne j\) satisfy \(|x_i - x_j| \ge (s_i + s_j) / 2 + g\); (ii) if the centres already form a packing, \(x = c\); (iii) within every cluster the displacements sum to zero, so the cluster's mean position equals its members' mean target.

Targets (top) and their spread (bottom).

In Targets (top) and their spread (bottom). the first three items overlap and form one cluster centred at their mean target. The other two are already clear and do not move.

Lanes for braces

Braces whose spans (with their labels) come too close must go in different lanes, that is, at different distances from the axis.

Definition · Conflicting intervals

Intervals \([a, b]\) and \([a', b']\) conflict for a gap \(g \ge 0\) when neither \(b + g \le a'\) nor \(b' + g \le a\). A lane assignment gives each interval a lane number so that no two conflicting intervals share a lane.

mosaickit.layout.stack1d.assign_lanes(
    intervals,
    gap=0.0
) -> tuple[int, ...]

First fit takes the intervals in input order and gives each the lowest lane in which it conflicts with nothing already there. Endpoints may be given in either order.

Theorem · First fit uses the fewest lanes

assign_lanes always returns a lane assignment. If the intervals are given in increasing order of their lower ends and each satisfies \(b - a + g > 0\), it uses exactly \(\omega\) lanes, where \(\omega\) is the largest number of pairwise conflicting intervals; no lane assignment uses fewer.

In other orders first fit can use more lanes than necessary, so add braces along an axis from low to high values when they may overlap.

mosaickit.layout.gutter.gutter_columns(
    mark_width,
    brace_widths,
    note_width,
    *,
    start,
    gap
)

The pure column layout behind the gutter: returns GutterColumns with marks, braces (one Band per lane, innermost first) and notes, each a Band(near, far) measured outward from the axis, and extent, the farthest edge.

Comments