Line Chart

This tutorial creates a multi-series line chart from generated data. It uses Noise for smooth fake values, scales for positioning, markers for ticks, and utility functions for label and tick placement.

The data setup uses optional packages:

pip install polars pendulum

Imports

import pendulum
import polars as pl
import pydreamplet as dp

from pydreamplet.colors import generate_colors
from pydreamplet.markers import Marker, TICK_BOTTOM
from pydreamplet.noise import Noise
from pydreamplet.scales import LinearScale, PointScale
from pydreamplet.utils import calculate_ticks, force_distance, sample_uniform

Generate Data

products = ["bicycle", "apple", "ham", "spoon", "boat", "starship"]

data = {}
for index, product in enumerate(products):
    noise = Noise(0, 250, 0.1, seed=index + 1)
    data[product] = [noise.int_value for _ in range(100)]

start_date = pendulum.date(2024, 7, 1)
end_date = start_date.add(days=99)

df = pl.DataFrame(data).with_columns(
    date=pl.date_range(start_date, end_date, interval="1d", eager=True)
).select(["date", *products])

Canvas and Scales

PointScale.map() returns float | None because a requested domain value may be missing. Here every date comes from the scale domain, so the assertion is both valid and useful for type checkers.

svg = dp.SVG(800, 400)
defs = svg.ensure_defs()
axis_layer = dp.G()
svg.append(axis_layer)

margin = {"left": 50, "right": 105, "top": 18, "bottom": 50}
dates = df["date"].to_list()

min_value = df.select(products).min_horizontal().min()
max_value = df.select(products).max_horizontal().max()

scale_x = PointScale(dates, (margin["left"], svg.w - margin["right"]))
scale_y = LinearScale((min_value, max_value), (svg.h - margin["bottom"], margin["top"]))

x_values = []
for date in dates:
    x = scale_x.map(date)
    assert x is not None
    x_values.append(x)

Draw the Series

colors = generate_colors("#cc340c", len(products))
last_points_y = []

for i, product in enumerate(products):
    points = []
    for j, value in enumerate(df[product].to_list()):
        points.extend([x_values[j], scale_y.map(value)])

    svg.append(
        dp.Polyline(
            points,
            stroke=colors[i],
            stroke_width=2,
            fill="none",
        )
    )
    last_points_y.append(points[-1])

Labels and Axes

Use force_distance() to spread labels near the final data points. Use sample_uniform() to pick a small number of date ticks from the full domain.

label_y_positions = force_distance(last_points_y, 16)

for i, product in enumerate(products):
    svg.append(
        dp.Text(
            product,
            x=x_values[-1] + 8,
            y=label_y_positions[i] + 5,
            font_size=14,
            fill=colors[i],
        )
    )

axis_y = scale_y.output_range[0]
axis_layer.append(
    dp.Line(
        x1=margin["left"],
        y1=axis_y,
        x2=svg.w - margin["right"],
        y2=axis_y,
        stroke="currentColor",
        stroke_width=1,
    )
)

tick_indices = sample_uniform(scale_x.domain, 5, None)
tick_points = []
for index in tick_indices:
    tick_points.extend([x_values[index], axis_y])

tick_path = dp.Polyline(tick_points, stroke="none", fill="none")
axis_layer.append(tick_path)

marker = Marker("bottom-tick", TICK_BOTTOM, 10, 10, fill="currentColor")
defs.append(marker)
tick_path.marker_start = marker.url
tick_path.marker_mid = marker.url
tick_path.marker_end = marker.url

for index in tick_indices:
    tick_date = pendulum.instance(dates[index])
    axis_layer.append(
        dp.Text(
            tick_date.format("D MMM 'YY"),
            x=x_values[index],
            y=axis_y + 30,
            font_size=13,
            fill="currentColor",
            text_anchor="middle",
        )
    )

Grid Lines

for tick in calculate_ticks(min_value, max_value, 5):
    y = scale_y.map(tick)
    axis_layer.append(
        dp.Line(
            x1=margin["left"],
            y1=y,
            x2=svg.w - margin["right"],
            y2=y,
            stroke="currentColor",
            opacity=0.18,
        )
    )
    axis_layer.append(
        dp.Text(
            str(tick),
            x=margin["left"] - 10,
            y=y + 4,
            font_size=13,
            fill="currentColor",
            text_anchor="end",
        )
    )

svg.save("line-chart.svg")