Skip to content

Render

Draw laid-out, encoded units as a matplotlib Figure.

lexograph.render.mpl.render_points(coords, *, channels=None, sizes=None, colors=None, glyphs=None, background='white', figsize=(8.0, 8.0))

Draw one mark per unit at its layout coordinate.

If glyphs are given, each unit is drawn as text; otherwise each unit is a scatter marker. Channel arrays may be passed individually or bundled in a :class:~lexograph.encode.channels.Channels; individual arguments win.

Parameters:

Name Type Description Default
coords Coords

An (N, 2) array of unit positions.

required
channels Channels | None

Resolved channels to use as defaults for sizes/colors/ glyphs.

None
sizes ndarray | None

Per-unit size in points (marker diameter, or glyph font size).

None
colors list[RGBA] | None

Per-unit RGBA colour.

None
glyphs list[str] | None

Per-unit text; when given, units are drawn as text not markers.

None
background str

Figure and axes background colour.

'white'
figsize tuple[float, float]

Figure size in inches.

(8.0, 8.0)

Returns:

Name Type Description
A Figure

class:matplotlib.figure.Figure with a single axes. Never calls

Figure

show().

Raises:

Type Description
ValueError

If coords is not (N, 2), or a channel length does not match the number of units.

Contract
  • Returns a Figure with exactly one axes.
  • Inputs are never mutated.

Examples:

>>> import numpy as np
>>> fig = render_points(np.array([[0.0, 0.0], [1.0, 1.0]]))
>>> type(fig).__name__
'Figure'
>>> len(fig.axes)
1
Source code in lexograph/render/mpl.py
def render_points(
    coords: Coords,
    *,
    channels: Channels | None = None,
    sizes: np.ndarray | None = None,
    colors: list[RGBA] | None = None,
    glyphs: list[str] | None = None,
    background: str = "white",
    figsize: tuple[float, float] = (8.0, 8.0),
) -> Figure:
    """Draw one mark per unit at its layout coordinate.

    If ``glyphs`` are given, each unit is drawn as text; otherwise each unit is a
    scatter marker. Channel arrays may be passed individually or bundled in a
    :class:`~lexograph.encode.channels.Channels`; individual arguments win.

    Args:
        coords: An ``(N, 2)`` array of unit positions.
        channels: Resolved channels to use as defaults for ``sizes``/``colors``/
            ``glyphs``.
        sizes: Per-unit size in points (marker diameter, or glyph font size).
        colors: Per-unit RGBA colour.
        glyphs: Per-unit text; when given, units are drawn as text not markers.
        background: Figure and axes background colour.
        figsize: Figure size in inches.

    Returns:
        A :class:`matplotlib.figure.Figure` with a single axes. Never calls
        ``show()``.

    Raises:
        ValueError: If ``coords`` is not ``(N, 2)``, or a channel length does
            not match the number of units.

    Contract:
        - Returns a Figure with exactly one axes.
        - Inputs are never mutated.

    Examples:
        >>> import numpy as np
        >>> fig = render_points(np.array([[0.0, 0.0], [1.0, 1.0]]))
        >>> type(fig).__name__
        'Figure'
        >>> len(fig.axes)
        1
    """
    array = _coords_2d(coords)
    n = array.shape[0]
    if channels is not None:
        sizes = sizes if sizes is not None else channels.sizes
        colors = colors if colors is not None else channels.colors
        glyphs = glyphs if glyphs is not None else channels.glyphs
    _check_lengths(n, sizes=sizes, colors=colors, glyphs=glyphs)

    fig = Figure(figsize=figsize, facecolor=background)
    ax = fig.subplots()
    ax.set_facecolor(background)

    if glyphs is not None:
        for i in range(n):
            ax.text(
                array[i, 0],
                array[i, 1],
                glyphs[i],
                fontsize=(float(sizes[i]) if sizes is not None else 12.0),
                color=(colors[i] if colors is not None else _DEFAULT_POINT_COLOR),
                ha="center",
                va="center",
            )
    elif n:
        marker_area = (
            (np.asarray(sizes, dtype=float) ** 2) if sizes is not None else 36.0
        )
        ax.scatter(
            array[:, 0],
            array[:, 1],
            s=marker_area,
            c=(colors if colors is not None else _DEFAULT_POINT_COLOR),
        )

    frame_axes(ax, array)
    fig.tight_layout()
    return fig

lexograph.render.mpl.render_path(coords, *, colors=None, color=_DEFAULT_LINE_COLOR, linewidth=1.5, background='white', figsize=(8.0, 8.0))

Draw the units as a connected path through their layout coordinates.

This is the renderer behind the text walk: consecutive units are joined by line segments. When colors is given (one colour per segment, i.e. one per unit after the first), the path is drawn as a multi-coloured :class:~matplotlib.collections.LineCollection.

Parameters:

Name Type Description Default
coords Coords

An (N, 2) array of vertices in path order.

required
colors list[RGBA] | None

Per-segment RGBA colours (length N - 1). None draws a single-colour path.

None
color str

The path colour when colors is None.

_DEFAULT_LINE_COLOR
linewidth float

Line width in points.

1.5
background str

Figure and axes background colour.

'white'
figsize tuple[float, float]

Figure size in inches.

(8.0, 8.0)

Returns:

Name Type Description
A Figure

class:matplotlib.figure.Figure with a single axes. Never calls

Figure

show().

Raises:

Type Description
ValueError

If coords is not (N, 2), or colors has the wrong length.

Contract
  • Returns a Figure with exactly one axes.
  • Inputs are never mutated.

Examples:

>>> import numpy as np
>>> fig = render_path(np.array([[0.0, 0.0], [1.0, 0.0], [1.0, 1.0]]))
>>> type(fig).__name__
'Figure'
Source code in lexograph/render/mpl.py
def render_path(
    coords: Coords,
    *,
    colors: list[RGBA] | None = None,
    color: str = _DEFAULT_LINE_COLOR,
    linewidth: float = 1.5,
    background: str = "white",
    figsize: tuple[float, float] = (8.0, 8.0),
) -> Figure:
    """Draw the units as a connected path through their layout coordinates.

    This is the renderer behind the text walk: consecutive units are joined by
    line segments. When ``colors`` is given (one colour per segment, i.e. one
    per unit after the first), the path is drawn as a multi-coloured
    :class:`~matplotlib.collections.LineCollection`.

    Args:
        coords: An ``(N, 2)`` array of vertices in path order.
        colors: Per-segment RGBA colours (length ``N - 1``). ``None`` draws a
            single-colour path.
        color: The path colour when ``colors`` is ``None``.
        linewidth: Line width in points.
        background: Figure and axes background colour.
        figsize: Figure size in inches.

    Returns:
        A :class:`matplotlib.figure.Figure` with a single axes. Never calls
        ``show()``.

    Raises:
        ValueError: If ``coords`` is not ``(N, 2)``, or ``colors`` has the wrong
            length.

    Contract:
        - Returns a Figure with exactly one axes.
        - Inputs are never mutated.

    Examples:
        >>> import numpy as np
        >>> fig = render_path(np.array([[0.0, 0.0], [1.0, 0.0], [1.0, 1.0]]))
        >>> type(fig).__name__
        'Figure'
    """
    array = _coords_2d(coords)
    n = array.shape[0]
    fig = Figure(figsize=figsize, facecolor=background)
    ax = fig.subplots()
    ax.set_facecolor(background)

    if n >= 2:
        if colors is not None:
            if len(colors) != n - 1:
                msg = f"colors must have length N-1 ({n - 1}), got {len(colors)}"
                raise ValueError(msg)
            segments = list(np.stack([array[:-1], array[1:]], axis=1))
            ax.add_collection(
                LineCollection(segments, colors=colors, linewidths=linewidth)
            )
        else:
            ax.plot(array[:, 0], array[:, 1], color=color, linewidth=linewidth)

    frame_axes(ax, array)
    fig.tight_layout()
    return fig

lexograph.render.mpl3d.render_path_3d(coords, *, colors=None, color=_DEFAULT_LINE_COLOR, linewidth=1.5, linewidths=None, background='white', figsize=(8.0, 10.0), elev=18.0, azim=-60.0, axes_off=True)

Draw a 3-D path through the layout coordinates as a corkscrew.

Parameters:

Name Type Description Default
coords Coords

An (N, 3) array of vertices in path order.

required
colors list[RGBA] | None

Per-segment RGBA colours (length N - 1). None draws a single-colour path.

None
color str

The path colour when colors is None.

_DEFAULT_LINE_COLOR
linewidth float

Line width in points, used when linewidths is None.

1.5
linewidths Sequence[float] | None

Per-segment line widths (length N - 1); overrides linewidth so the size channel can vary the stroke.

None
background str

Figure and axes background colour.

'white'
figsize tuple[float, float]

Figure size in inches.

(8.0, 10.0)
elev float

Camera elevation angle in degrees.

18.0
azim float

Camera azimuth angle in degrees.

-60.0
axes_off bool

Hide the 3-D axes, panes, and ticks for a clean plate.

True

Returns:

Name Type Description
A Figure

class:matplotlib.figure.Figure with one 3-D axes. Never calls

Figure

show().

Raises:

Type Description
ValueError

If coords is not (N, 3) or colors has the wrong length.

Contract
  • Returns a Figure with exactly one axes.
  • Inputs are never mutated.

Examples:

>>> import numpy as np
>>> coords = np.array([[0.0, 0.0, 0.0], [1.0, 0.0, 1.0], [1.0, 1.0, 2.0]])
>>> fig = render_path_3d(coords)
>>> type(fig).__name__
'Figure'
Source code in lexograph/render/mpl3d.py
def render_path_3d(
    coords: Coords,
    *,
    colors: list[RGBA] | None = None,
    color: str = _DEFAULT_LINE_COLOR,
    linewidth: float = 1.5,
    linewidths: Sequence[float] | None = None,
    background: str = "white",
    figsize: tuple[float, float] = (8.0, 10.0),
    elev: float = 18.0,
    azim: float = -60.0,
    axes_off: bool = True,
) -> Figure:
    """Draw a 3-D path through the layout coordinates as a corkscrew.

    Args:
        coords: An ``(N, 3)`` array of vertices in path order.
        colors: Per-segment RGBA colours (length ``N - 1``). ``None`` draws a
            single-colour path.
        color: The path colour when ``colors`` is ``None``.
        linewidth: Line width in points, used when ``linewidths`` is ``None``.
        linewidths: Per-segment line widths (length ``N - 1``); overrides
            ``linewidth`` so the size channel can vary the stroke.
        background: Figure and axes background colour.
        figsize: Figure size in inches.
        elev: Camera elevation angle in degrees.
        azim: Camera azimuth angle in degrees.
        axes_off: Hide the 3-D axes, panes, and ticks for a clean plate.

    Returns:
        A :class:`matplotlib.figure.Figure` with one 3-D axes. Never calls
        ``show()``.

    Raises:
        ValueError: If ``coords`` is not ``(N, 3)`` or ``colors`` has the wrong
            length.

    Contract:
        - Returns a Figure with exactly one axes.
        - Inputs are never mutated.

    Examples:
        >>> import numpy as np
        >>> coords = np.array([[0.0, 0.0, 0.0], [1.0, 0.0, 1.0], [1.0, 1.0, 2.0]])
        >>> fig = render_path_3d(coords)
        >>> type(fig).__name__
        'Figure'
    """
    array = _coords_3d(coords)
    n = array.shape[0]

    fig = Figure(figsize=figsize, facecolor=background)
    # Axes3D exposes 3-D-only methods (view_init, set_zlim, add_collection3d)
    # that the 2-D-focused matplotlib stubs do not model; treat it as ``Any``.
    ax: Any = fig.add_subplot(projection="3d")
    ax.set_facecolor(background)

    if n >= 2:
        segments = list(np.stack([array[:-1], array[1:]], axis=1))
        seg_colors = colors if colors is not None else [color] * (n - 1)
        if len(seg_colors) != n - 1:
            msg = f"colors must have length N-1 ({n - 1}), got {len(seg_colors)}"
            raise ValueError(msg)
        widths: float | list[float] = linewidth
        if linewidths is not None:
            if len(linewidths) != n - 1:
                msg = (
                    f"linewidths must have length N-1 ({n - 1}), got {len(linewidths)}"
                )
                raise ValueError(msg)
            widths = [float(w) for w in linewidths]
        ax.add_collection3d(
            Line3DCollection(segments, colors=seg_colors, linewidths=widths)
        )

    _frame_3d(ax, array, elev=elev, azim=azim, axes_off=axes_off)
    return fig

lexograph.render.mpl.frame_axes(ax, coords, *, margin=0.05)

Equalise the aspect ratio, hide the axes, and fit coords with a margin.

A small helper shared by the renderers and presets: it makes an axes show a spatial figure (equal aspect, no ticks or spines) framed to the data.

Parameters:

Name Type Description Default
ax Axes

The axes to configure.

required
coords Coords

An (N, 2) array the limits are fitted to.

required
margin float

Fractional padding added around the data extent.

0.05

Examples:

>>> import numpy as np
>>> from matplotlib.figure import Figure
>>> ax = Figure().subplots()
>>> frame_axes(ax, np.array([[0.0, 0.0], [1.0, 1.0]]))
>>> ax.get_aspect()
1.0
Source code in lexograph/render/mpl.py
def frame_axes(ax: Axes, coords: Coords, *, margin: float = 0.05) -> None:
    """Equalise the aspect ratio, hide the axes, and fit ``coords`` with a margin.

    A small helper shared by the renderers and presets: it makes an axes show a
    spatial figure (equal aspect, no ticks or spines) framed to the data.

    Args:
        ax: The axes to configure.
        coords: An ``(N, 2)`` array the limits are fitted to.
        margin: Fractional padding added around the data extent.

    Examples:
        >>> import numpy as np
        >>> from matplotlib.figure import Figure
        >>> ax = Figure().subplots()
        >>> frame_axes(ax, np.array([[0.0, 0.0], [1.0, 1.0]]))
        >>> ax.get_aspect()
        1.0
    """
    array = np.asarray(coords, dtype=float)
    ax.set_aspect("equal")
    ax.axis("off")
    if array.shape[0] == 0:
        return
    xmin, ymin = array.min(axis=0)
    xmax, ymax = array.max(axis=0)
    span_x = xmax - xmin or 1.0
    span_y = ymax - ymin or 1.0
    ax.set_xlim(xmin - margin * span_x, xmax + margin * span_x)
    ax.set_ylim(ymin - margin * span_y, ymax + margin * span_y)