Parametric Curve Plot — Bokeh

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 Bokeh

Python source (Bokeh)

""" anyplot.ai
line-parametric: Parametric Curve Plot
Library: bokeh 3.9.1 | Python 3.13.14
Quality: 91/100 | Updated: 2026-06-20
"""

import os
import sys


# Prevent bokeh.py from shadowing the bokeh package (script dir added to sys.path[0])
_here = os.path.dirname(os.path.abspath(__file__))
sys.path = [p for p in sys.path if os.path.abspath(p) != _here]
import time
from pathlib import Path

import numpy as np
from bokeh.io import output_file, save
from bokeh.layouts import gridplot
from bokeh.models import BasicTicker, ColorBar, ColumnDataSource, LinearColorMapper
from bokeh.plotting import figure
from selenium import webdriver
from selenium.webdriver.chrome.options import Options


# Theme tokens
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"


# Imprint sequential colormap — green (#009E73) → blue (#4467A3)
def _lerp_hex(c0, c1, t):
    r0, g0, b0 = (int(c0[i : i + 2], 16) for i in (1, 3, 5))
    r1, g1, b1 = (int(c1[i : i + 2], 16) for i in (1, 3, 5))
    return "#{:02X}{:02X}{:02X}".format(
        int(round(r0 + (r1 - r0) * t)), int(round(g0 + (g1 - g0) * t)), int(round(b0 + (b1 - b0) * t))
    )


IMPRINT_SEQ256 = [_lerp_hex("#009E73", "#4467A3", i / 255.0) for i in range(256)]

# Data — 1000 points for smooth curves
n_points = 1000
n_seg = n_points - 1

# Lissajous figure: x = sin(3t), y = sin(2t), t ∈ [0, 2π]
t_liss = np.linspace(0, 2 * np.pi, n_points)
x_liss = np.sin(3 * t_liss)
y_liss = np.sin(2 * t_liss)

# Archimedean spiral: x = t·cos(t), y = t·sin(t), t ∈ [0, 4π]
t_spiral = np.linspace(0, 4 * np.pi, n_points)
x_spiral = t_spiral * np.cos(t_spiral)
y_spiral = t_spiral * np.sin(t_spiral)


def make_segment_source(x, y):
    n = len(x) - 1
    return ColumnDataSource(
        data={
            "xs": [[x[i], x[i + 1]] for i in range(n)],
            "ys": [[y[i], y[i + 1]] for i in range(n)],
            "color": [IMPRINT_SEQ256[int(i / (n - 1) * 255)] for i in range(n)],
        }
    )


seg_liss = make_segment_source(x_liss, y_liss)
seg_spiral = make_segment_source(x_spiral, y_spiral)

# Two panels side-by-side → 1600 + 1600 = 3200 px wide × 1800 px tall
PANEL_W = 1600
PANEL_H = 1800

p1 = figure(
    width=PANEL_W,
    height=PANEL_H,
    title="line-parametric · python · bokeh · anyplot.ai",
    x_axis_label="x(t) = sin(3t)",
    y_axis_label="y(t) = sin(2t)",
    toolbar_location=None,
    match_aspect=True,
    min_border_bottom=160,
    min_border_left=180,
    min_border_top=110,
    min_border_right=220,
)

p1.multi_line(xs="xs", ys="ys", source=seg_liss, line_color="color", line_width=9)
p1.scatter(
    x=[x_liss[0]],
    y=[y_liss[0]],
    size=30,
    fill_color="#009E73",
    line_color=PAGE_BG,
    line_width=4,
    legend_label="Start  t = 0",
)
p1.scatter(
    x=[x_liss[-1]],
    y=[y_liss[-1]],
    size=30,
    marker="square",
    fill_color="#4467A3",
    line_color=PAGE_BG,
    line_width=4,
    legend_label="End  t = 2π",
)

liss_cbar = ColorBar(
    color_mapper=LinearColorMapper(palette=IMPRINT_SEQ256, low=0, high=round(2 * np.pi, 2)),
    ticker=BasicTicker(desired_num_ticks=5),
    title="t (rad)",
    title_text_font_size="28pt",
    title_text_color=INK,
    major_label_text_font_size="24pt",
    major_label_text_color=INK_SOFT,
    label_standoff=14,
    width=44,
    padding=24,
    background_fill_color=PAGE_BG,
    border_line_color=INK_SOFT,
)
p1.add_layout(liss_cbar, "right")

p2 = figure(
    width=PANEL_W,
    height=PANEL_H,
    title="Archimedean Spiral · line-parametric · python · bokeh · anyplot.ai",
    x_axis_label="x(t) = t·cos(t)",
    y_axis_label="y(t) = t·sin(t)",
    toolbar_location=None,
    match_aspect=True,
    min_border_bottom=160,
    min_border_left=180,
    min_border_top=110,
    min_border_right=220,
)

p2.multi_line(xs="xs", ys="ys", source=seg_spiral, line_color="color", line_width=9)
p2.scatter(
    x=[x_spiral[0]],
    y=[y_spiral[0]],
    size=30,
    fill_color="#009E73",
    line_color=PAGE_BG,
    line_width=4,
    legend_label="Start  t = 0",
)
p2.scatter(
    x=[x_spiral[-1]],
    y=[y_spiral[-1]],
    size=30,
    marker="square",
    fill_color="#4467A3",
    line_color=PAGE_BG,
    line_width=4,
    legend_label="End  t = 4π",
)

spiral_cbar = ColorBar(
    color_mapper=LinearColorMapper(palette=IMPRINT_SEQ256, low=0, high=round(4 * np.pi, 2)),
    ticker=BasicTicker(desired_num_ticks=5),
    title="t (rad)",
    title_text_font_size="28pt",
    title_text_color=INK,
    major_label_text_font_size="24pt",
    major_label_text_color=INK_SOFT,
    label_standoff=14,
    width=44,
    padding=24,
    background_fill_color=PAGE_BG,
    border_line_color=INK_SOFT,
)
p2.add_layout(spiral_cbar, "right")

# Theme-adaptive chrome for both panels
for p in [p1, p2]:
    p.background_fill_color = PAGE_BG
    p.border_fill_color = PAGE_BG
    p.outline_line_color = None  # L-shaped frame: axes provide the bottom+left boundary

    p.title.text_color = INK
    p.title.text_font_size = "36pt"
    p.title.text_font_style = "normal"

    p.xaxis.axis_label_text_color = INK
    p.yaxis.axis_label_text_color = INK
    p.xaxis.axis_label_text_font_size = "32pt"
    p.yaxis.axis_label_text_font_size = "32pt"
    p.xaxis.axis_label_text_font_style = "italic"
    p.yaxis.axis_label_text_font_style = "italic"
    p.xaxis.major_label_text_color = INK_SOFT
    p.yaxis.major_label_text_color = INK_SOFT
    p.xaxis.major_label_text_font_size = "26pt"
    p.yaxis.major_label_text_font_size = "26pt"
    p.xaxis.axis_line_color = INK_SOFT
    p.yaxis.axis_line_color = INK_SOFT
    p.xaxis.major_tick_line_color = INK_SOFT
    p.yaxis.major_tick_line_color = INK_SOFT
    p.axis.minor_tick_line_color = None

    p.xgrid.grid_line_color = INK
    p.ygrid.grid_line_color = INK
    p.xgrid.grid_line_alpha = 0.12
    p.ygrid.grid_line_alpha = 0.12

    p.legend.background_fill_color = ELEVATED_BG
    p.legend.border_line_color = INK_SOFT
    p.legend.label_text_color = INK_SOFT
    p.legend.label_text_font_size = "26pt"
    p.legend.glyph_width = 36
    p.legend.glyph_height = 36
    p.legend.location = "top_right"
    p.legend.padding = 14
    p.legend.spacing = 8

# Layout
grid = gridplot([[p1, p2]], merge_tools=False, toolbar_location=None)

# Save interactive HTML
html_path = Path(f"plot-{THEME}.html")
output_file(str(html_path))
save(grid)

# Strip default body margin so the plot fills the Selenium viewport exactly
html = html_path.read_text()
html = html.replace("<body>", f'<body style="margin:0;padding:0;overflow:hidden;background:{PAGE_BG};">', 1)
html_path.write_text(html)

# Screenshot with headless Chrome (Selenium) at canvas dimensions
W, H = 3200, 1800
opts = Options()
for arg in (
    "--headless=new",
    "--no-sandbox",
    "--disable-dev-shm-usage",
    "--disable-gpu",
    f"--window-size={W},{H}",
    "--hide-scrollbars",
):
    opts.add_argument(arg)
driver = webdriver.Chrome(options=opts)
driver.set_window_size(W, H)
# Compensate for any browser chrome that reduces the actual viewport
actual_h = driver.execute_script("return window.innerHeight")
if actual_h < H:
    driver.set_window_size(W, H + (H - actual_h))
driver.get(f"file://{html_path.resolve()}")
time.sleep(3)
driver.save_screenshot(f"plot-{THEME}.png")
driver.quit()

Part of Parametric Curve Plot on anyplot.ai.

Other implementations