Parametric Curve Plot — Pygal

A parametric curve plot visualizes x(t) and y(t) as functions of a parameter t, tracing smooth curves in 2D space. Unlike standard function plots where y = f(x), parametric curves can loop, self-intersect, and form closed shapes such as Lissajous figures, spirals, and cardioids. This makes them essential for representing trajectories, oscillations, and classical mathematical curves that cannot be expressed as single-valued functions.

Parametric Curve Plot rendered with Pygal

Python source (Pygal)

""" anyplot.ai
line-parametric: Parametric Curve Plot
Library: pygal 3.1.3 | Python 3.13.14
Quality: 82/100 | Updated: 2026-06-20
"""

import os

import numpy as np
import pygal
from pygal.style import Style


THEME = os.getenv("ANYPLOT_THEME", "light")

# Theme-adaptive chrome tokens
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"

# Imprint categorical palette — canonical order
IMPRINT_PALETTE = (
    "#009E73",  # 1 brand green   — Lissajous start
    "#C475FD",  # 2 lavender      — Lissajous 2nd quarter
    "#4467A3",  # 3 blue          — Lissajous 3rd quarter
    "#BD8233",  # 4 ochre         — Lissajous end
    "#AE3030",  # 5 matte red     — Spiral start
    "#2ABCCD",  # 6 cyan          — Spiral 2nd quarter
    "#954477",  # 7 rose          — Spiral 3rd quarter
    "#99B314",  # 8 lime          — Spiral end
)

# Data
n_points = 800
n_segs = 4
seg_size = n_points // n_segs

# Curve 1: Lissajous (3, 2) — x = sin(3t), y = sin(2t), t in [0, 2pi] — closed curve
t_liss = np.linspace(0, 2 * np.pi, n_points)
liss_x = np.sin(3 * t_liss)
liss_y = np.sin(2 * t_liss)

# Curve 2: Archimedean spiral — x = (t/2pi)*cos(t), y = (t/2pi)*sin(t), t in [0, 2pi]
# Normalised so radius reaches 1.0 at t=2pi, fitting the [-1.2, 1.2] axis range
t_spiral = np.linspace(0, 2 * np.pi, n_points)
spiral_x = (t_spiral / (2 * np.pi)) * np.cos(t_spiral)
spiral_y = (t_spiral / (2 * np.pi)) * np.sin(t_spiral)

font = "DejaVu Sans, Helvetica, Arial, sans-serif"

custom_style = Style(
    background=PAGE_BG,
    plot_background=PAGE_BG,
    foreground=INK,
    foreground_strong=INK,
    foreground_subtle=INK_MUTED,
    colors=IMPRINT_PALETTE,
    font_family=font,
    title_font_family=font,
    title_font_size=66,
    label_font_size=56,
    major_label_font_size=44,
    legend_font_size=44,
    value_font_size=36,
    tooltip_font_size=36,
    tooltip_font_family=font,
    opacity=1.0,
    stroke_opacity=1.0,
    stroke_width=10,
)

chart = pygal.XY(
    width=2400,
    height=2400,
    style=custom_style,
    title="line-parametric · python · pygal · anyplot.ai",
    x_title="Horizontal Position  x(t)",
    y_title="Vertical Position  y(t)",
    show_legend=True,
    legend_at_bottom=True,
    legend_at_bottom_columns=2,
    legend_box_size=24,
    stroke=True,
    show_dots=False,
    show_x_guides=True,
    show_y_guides=True,
    x_value_formatter=lambda v: f"{v:.1f}",
    value_formatter=lambda v: f"{v:.1f}",
    js=[],
    xrange=(-1.2, 1.2),
    range=(-1.2, 1.2),
    print_values=False,
    min_scale=3,
    margin_bottom=440,
    margin_left=80,
    margin_right=60,
    margin_top=60,
    truncate_legend=-1,
)

# Segment labels — quarter-period intervals show traversal direction via colour
liss_labels = ["Lissajous  0 to pi/2", "Lissajous  pi/2 to pi", "Lissajous  pi to 3pi/2", "Lissajous  3pi/2 to 2pi"]
spiral_labels = ["Spiral  0 to pi/2", "Spiral  pi/2 to pi", "Spiral  pi to 3pi/2", "Spiral  3pi/2 to 2pi"]

# Lissajous (3,2) — solid lines; colour gradient encodes traversal direction
for k in range(n_segs):
    start = k * seg_size
    end = min((k + 1) * seg_size + 1, n_points)
    seg_data = [{"value": (float(liss_x[i]), float(liss_y[i]))} for i in range(start, end)]
    chart.add(
        liss_labels[k], seg_data, stroke_style={"width": 10, "linecap": "round", "linejoin": "round"}, show_dots=False
    )

# Archimedean spiral — dashed lines provide non-colour family disambiguation
# Colour gradient encodes outward growth from origin (red) to radius 1 (lime)
for k in range(n_segs):
    start = k * seg_size
    end = min((k + 1) * seg_size + 1, n_points)
    seg_data = [{"value": (float(spiral_x[i]), float(spiral_y[i]))} for i in range(start, end)]
    chart.add(
        spiral_labels[k],
        seg_data,
        stroke_style={"width": 10, "dasharray": "16, 8", "linecap": "round", "linejoin": "round"},
        show_dots=False,
    )

# Traversal markers — explicit start/end dots for both curves
# Both curves originate at the origin; Lissajous closes at (0,0), spiral ends at (1, 0)
chart.add("● Start  t = 0  (both curves)", [{"value": (0.0, 0.0)}], show_dots=True)
chart.add("◆ Spiral end  t = 2pi", [{"value": (float(spiral_x[-1]), float(spiral_y[-1]))}], show_dots=True)

# Save — PNG + interactive HTML, both theme-suffixed
chart.render_to_png(f"plot-{THEME}.png")
with open(f"plot-{THEME}.html", "wb") as f:
    f.write(chart.render())

Part of Parametric Curve Plot on anyplot.ai.

Other implementations