Skip to content

Text walk

Each sentence steps forward and turns, space-filling — the 2-D walk.

lexograph.presets.text_walk.text_walk(text, *, colour=None, colour_kind='auto', size=None, mode='path', helix=False, z_step=1.0, font=None, font_size=12.0, width_step=True, turn=-90.0, background='white', figsize=None)

Draw a text as a space-filling turtle walk over its sentences.

Parameters:

Name Type Description Default
text str

The source text.

required
colour Sequence[object] | None

One value per sentence for the colour channel (category labels or numeric values). None colours by position in the text.

None
colour_kind ColourKind

How to read colour: "categorical", "continuous", or "auto" (numeric values continuous, everything else categorical).

'auto'
size Sequence[float] | None

One scalar per sentence for the size channel. None sizes by sentence length.

None
mode WalkMode

"path" draws a multi-coloured ribbon; "glyphs" sets each sentence's text along its segment. Only "path" is supported when helix is set.

'path'
helix bool

If True, lift the walk into a 3-D corkscrew (each sentence also climbs z_step) and render it in matplotlib 3-D.

False
z_step float

The vertical lift per sentence for the corkscrew (used when helix is set).

1.0
font FontProperties | str | Path | None

Font to measure widths and (in glyph mode) draw with.

None
font_size float

Base font size in points for width measurement and glyphs.

12.0
width_step bool

If True, step by each sentence's rendered width; if False, step by its character count.

True
turn float

Degrees to turn between sentences (-90 is the rectangular walk).

-90.0
background str

Figure and axes background colour.

'white'
figsize tuple[float, float] | None

Figure size in inches. Defaults to (10, 10) flat, or (8, 10) for the helix.

None

Returns:

Name Type Description
A Figure

class:matplotlib.figure.Figure with one axes (2-D, or 3-D for the

Figure

helix). Never calls show().

Raises:

Type Description
ValueError

If the text has fewer than two sentences, a channel length does not match the sentence count, or helix is combined with glyph mode.

Contract
  • Returns a Figure with exactly one axes.
  • The colour and size channels stay aligned with the sentences.

Examples:

>>> from lexograph import load_demo_text
>>> fig = text_walk(load_demo_text())
>>> type(fig).__name__
'Figure'
>>> helix = text_walk(load_demo_text(), helix=True)
>>> type(helix).__name__
'Figure'
Source code in lexograph/presets/text_walk.py
def text_walk(
    text: str,
    *,
    colour: Sequence[object] | None = None,
    colour_kind: ColourKind = "auto",
    size: Sequence[float] | None = None,
    mode: WalkMode = "path",
    helix: bool = False,
    z_step: float = 1.0,
    font: FontProperties | str | Path | None = None,
    font_size: float = 12.0,
    width_step: bool = True,
    turn: float = -90.0,
    background: str = "white",
    figsize: tuple[float, float] | None = None,
) -> Figure:
    """Draw a text as a space-filling turtle walk over its sentences.

    Args:
        text: The source text.
        colour: One value per sentence for the colour channel (category labels or
            numeric values). ``None`` colours by position in the text.
        colour_kind: How to read ``colour``: ``"categorical"``, ``"continuous"``,
            or ``"auto"`` (numeric values continuous, everything else categorical).
        size: One scalar per sentence for the size channel. ``None`` sizes by
            sentence length.
        mode: ``"path"`` draws a multi-coloured ribbon; ``"glyphs"`` sets each
            sentence's text along its segment. Only ``"path"`` is supported when
            ``helix`` is set.
        helix: If ``True``, lift the walk into a 3-D corkscrew (each sentence also
            climbs ``z_step``) and render it in matplotlib 3-D.
        z_step: The vertical lift per sentence for the corkscrew (used when
            ``helix`` is set).
        font: Font to measure widths and (in glyph mode) draw with.
        font_size: Base font size in points for width measurement and glyphs.
        width_step: If ``True``, step by each sentence's rendered width; if
            ``False``, step by its character count.
        turn: Degrees to turn between sentences (``-90`` is the rectangular walk).
        background: Figure and axes background colour.
        figsize: Figure size in inches. Defaults to ``(10, 10)`` flat, or
            ``(8, 10)`` for the helix.

    Returns:
        A :class:`matplotlib.figure.Figure` with one axes (2-D, or 3-D for the
        helix). Never calls ``show()``.

    Raises:
        ValueError: If the text has fewer than two sentences, a channel length
            does not match the sentence count, or ``helix`` is combined with
            glyph mode.

    Contract:
        - Returns a Figure with exactly one axes.
        - The colour and size channels stay aligned with the sentences.

    Examples:
        >>> from lexograph import load_demo_text
        >>> fig = text_walk(load_demo_text())
        >>> type(fig).__name__
        'Figure'
        >>> helix = text_walk(load_demo_text(), helix=True)
        >>> type(helix).__name__
        'Figure'
    """
    if helix and mode == "glyphs":
        msg = "glyph mode is not supported for the 3-D helix walk"
        raise ValueError(msg)
    units = split_sentences(text)
    n = len(units)
    if n < 2:
        msg = f"need at least two sentences to walk, got {n}"
        raise ValueError(msg)

    if width_step:
        steps = rendered_widths(units, prop=font, size=font_size)
    else:
        steps = np.array([len(u) for u in units], dtype=float)

    colours = _resolve_colours(colour, n, colour_kind)
    raw_size = [float(s) for s in size] if size is not None else [len(u) for u in units]
    if len(raw_size) != n:
        msg = f"size must have one entry per sentence ({n}), got {len(raw_size)}"
        raise ValueError(msg)
    weight = normalize_size(raw_size, lo=0.0, hi=1.0)

    if helix:
        coords3d = walk3d_layout(steps, turn=turn, z_step=z_step)
        return render_path_3d(
            coords3d,
            colors=colours,
            linewidths=[0.6 + 6.0 * float(w) for w in weight],
            background=background,
            figsize=figsize or (8.0, 10.0),
        )

    coords = walk_layout(steps, turn=turn)
    fig = Figure(figsize=figsize or (10.0, 10.0), facecolor=background)
    ax = fig.subplots()
    ax.set_facecolor(background)

    if mode == "glyphs":
        angles = heading_angles(steps, turn=turn)
        mids = (coords[:-1] + coords[1:]) / 2.0
        glyph_sizes = font_size * (0.6 + 2.4 * weight)
        for i, unit in enumerate(units):
            ax.text(
                mids[i, 0],
                mids[i, 1],
                unit,
                fontsize=glyph_sizes[i],
                color=colours[i],
                rotation=angles[i],
                rotation_mode="anchor",
                ha="center",
                va="center",
                fontproperties=font,
            )
    else:
        segments = list(np.stack([coords[:-1], coords[1:]], axis=1))
        linewidths = 0.6 + 6.0 * weight
        ax.add_collection(
            LineCollection(segments, colors=colours, linewidths=linewidths)
        )

    frame_axes(ax, coords)
    # No tight_layout: the axes are off, so there are no decorations to fit, and
    # calling it only emits a spurious "tight layout not applied" warning.
    return fig