Utilities

The pydreamplet.utils module contains small helpers used by colors, generators, scales, charts, and label layout.

Utilities are imported from pydreamplet.utils.

from pydreamplet.utils import calculate_ticks, place_labels_1d

Visual Example

This example uses place_labels_1d() to resolve overlapping labels while keeping each label close to its original anchor.

import pydreamplet as dp
from pydreamplet.utils import place_labels_1d

anchors = [42, 55, 68, 128, 136, 206]
sizes = [36, 36, 36, 44, 44, 38]
placements = place_labels_1d(anchors, sizes, gap=4, bounds=(24, 276))

svg = dp.SVG(300, 150)
svg.append(dp.Line(24, 114, 276, 114, stroke="currentColor", opacity=0.3))

for anchor, placement in zip(anchors, placements):
    svg.append(
        dp.Line(anchor, 114, placement.position, 78, stroke="currentColor", opacity=0.35),
        dp.Circle(cx=anchor, cy=114, r=3, fill="#f83898"),
        dp.Rect(
            x=placement.start,
            y=60,
            width=placement.size,
            height=24,
            rx=4,
            fill="#14b8a6",
            opacity=0.24,
            stroke="#14b8a6",
            stroke_width=1.5,
        ),
        dp.Text(
            str(int(anchor)),
            x=placement.position,
            y=76,
            font_size=11,
            text_anchor="middle",
            fill="currentColor",
        ),
    )

svg.append(dp.Text("anchor positions", x=24, y=134, font_size=12, fill="currentColor"))
svg.append(dp.Text("resolved label positions", x=58, y=45, font_size=12, fill="currentColor"))

Numeric Helpers

math_round(x: Real) -> int

Rounds with half-up behavior by returning int(x + 0.5).

from pydreamplet.utils import math_round

assert math_round(3.4) == 3
assert math_round(3.6) == 4
constrain(value: Real, min_val: Real, max_val: Real) -> Real

Clamps value into the inclusive range [min_val, max_val].

from pydreamplet.utils import constrain

assert constrain(10, 0, 5) == 5
assert constrain(-3, 0, 5) == 0
radians(degrees: Real) -> Real
degrees(radians: Real) -> Real

Converts between degrees and radians.

from pydreamplet.utils import degrees, radians

assert radians(180) == 3.141592653589793
assert degrees(3.141592653589793) == 180.0

Ticks

calculate_ticks(
    min_val: Real,
    max_val: Real,
    num_ticks: int = 5,
    below_max: bool = True,
) -> list[Real]

Returns rounded tick values using 1, 2, 5, or 10 times a power-of-ten step. min_val must be less than max_val. When below_max is True, ticks above the maximum are removed.

from pydreamplet.utils import calculate_ticks

assert calculate_ticks(0, 42986, 5) == [0, 10000, 20000, 30000, 40000]
assert calculate_ticks(0, 1, 5) == [0.0, 0.2, 0.4, 0.6, 0.8, 1.0]
assert calculate_ticks(0, 42986, 3, below_max=False) == [0, 20000, 40000, 60000]

Pie Angles

pie_angles(
    values: Sequence[Real],
    start_angle: Real = 0,
    end_angle: Real | None = None,
) -> list[tuple[float, float]]

Splits an angular span proportionally by values. If end_angle is omitted, the span is a full turn from start_angle to start_angle + 360.

from pydreamplet.utils import pie_angles

assert pie_angles([1, 2, 3]) == [(0, 60), (60, 180), (180, 360)]
assert pie_angles([1, 2, 3], start_angle=90) == [(90, 150), (150, 270), (270, 450)]
assert pie_angles([]) == []

A list whose sum is zero raises ZeroDivisionError.

Sampling

sample_uniform(
    input_list: list[Any],
    n: int,
    precedence: Literal["first", "last"] | None = "first",
) -> tuple[int, ...]

Returns evenly spaced indices. precedence="first" anchors the first index, "last" anchors the last index, and None chooses balanced interior indices.

from pydreamplet.utils import sample_uniform

items = list(range(10))

assert sample_uniform(items, n=4, precedence="first") == (0, 3, 6, 9)
assert sample_uniform(items, n=3, precedence="last") == (1, 5, 9)
assert sample_uniform(list(range(12)), n=4, precedence=None) == (1, 3, 7, 10)

Invalid precedence values raise ValueError.

Label Layout

force_distance(values: Sequence[Real], distance: Real) -> list[Real]

Adjusts unsorted numeric positions so adjacent sorted positions are at least distance apart, then returns results in the original input order.

from pydreamplet.utils import force_distance

positions = force_distance([2, 6, 7, 8, 10, 16, 18], distance=2)
assert positions == [2, 5, 7, 9, 11, 16, 18]
resolve_collisions_1d(
    anchors: Sequence[Real],
    sizes: Sequence[Real],
    *,
    gap: Real = 0,
    bounds: tuple[Real, Real] | None = None,
) -> list[float]

Resolves centered 1D items so their extents do not overlap. anchors and sizes must have the same length. gap and every size must be non-negative.

from pydreamplet.utils import resolve_collisions_1d

assert resolve_collisions_1d([0, 1, 10], [4, 4, 4], gap=1) == [0, 5, 10]
assert resolve_collisions_1d([5, 0, 1], [4, 4, 4], gap=1) == [10, 0, 5]
place_labels_1d(
    anchors: Sequence[Real],
    sizes: Sequence[Real],
    *,
    gap: Real = 0,
    bounds: tuple[Real, Real] | None = None,
) -> list[LabelPlacement]

Wraps resolve_collisions_1d() and returns LabelPlacement records.

from pydreamplet.utils import place_labels_1d

placements = place_labels_1d([0, 1], [4, 4], gap=1)

assert [placement.position for placement in placements] == [0, 5]
assert placements[0].start == -2
assert placements[0].end == 2

LabelPlacement is a frozen dataclass with anchor, position, and size fields plus computed start and end properties.

Bounding Boxes

bboxes_overlap(a: BoundingBox, b: BoundingBox, padding: Real = 0) -> bool

Returns True when two BoundingBox objects overlap. Touching edges are not considered overlapping. Positive padding expands both boxes before testing.

from pydreamplet import BoundingBox
from pydreamplet.utils import bboxes_overlap

left = BoundingBox(0, 0, 10, 10)
right = BoundingBox(10, 0, 5, 5)

assert bboxes_overlap(left, right) is False
assert bboxes_overlap(left, right, padding=0.1) is True

Negative padding raises ValueError.