Basic Parallel Coordinates Plot — Bokeh

A parallel coordinates plot visualizes multivariate data by representing each variable as a vertical axis and each observation as a line connecting values across all axes. This technique is powerful for identifying patterns, clusters, and outliers in high-dimensional datasets where traditional 2D plots fall short. It enables simultaneous comparison of multiple variables for each data point.

Basic Parallel Coordinates Plot rendered with Bokeh

Python source (Bokeh)

""" anyplot.ai
parallel-basic: Basic Parallel Coordinates Plot
Library: bokeh 3.9.1 | Python 3.13.14
Quality: 94/100 | Updated: 2026-07-24
"""

import os
import sys
import time
from pathlib import Path


_script_dir = os.path.dirname(os.path.abspath(__file__))
sys.path = [p for p in sys.path if os.path.abspath(p or ".") != _script_dir]

import numpy as np
import pandas as pd
from bokeh.io import output_file, save
from bokeh.models import ColumnDataSource, HoverTool, Span
from bokeh.plotting import figure
from selenium import webdriver
from selenium.webdriver.chrome.options import Options


# Theme tokens (see prompts/default-style-guide.md "Theme-adaptive Chrome")
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 palette (position 1 is always the first categorical series)
IMPRINT_PALETTE = ["#009E73", "#C475FD", "#4467A3", "#BD8233", "#AE3030", "#2ABCCD", "#954477", "#99B314"]

# Data - Iris-like dataset for multivariate demonstration
np.random.seed(42)

n_per_species = 50

# Setosa: small petals, moderate sepals
setosa = pd.DataFrame(
    {
        "sepal_length": np.random.normal(5.0, 0.35, n_per_species),
        "sepal_width": np.random.normal(3.4, 0.38, n_per_species),
        "petal_length": np.random.normal(1.5, 0.17, n_per_species),
        "petal_width": np.random.normal(0.25, 0.10, n_per_species),
        "species": "setosa",
    }
)

# Versicolor: medium everything
versicolor = pd.DataFrame(
    {
        "sepal_length": np.random.normal(5.9, 0.52, n_per_species),
        "sepal_width": np.random.normal(2.8, 0.31, n_per_species),
        "petal_length": np.random.normal(4.3, 0.47, n_per_species),
        "petal_width": np.random.normal(1.3, 0.20, n_per_species),
        "species": "versicolor",
    }
)

# Virginica: large petals and sepals
virginica = pd.DataFrame(
    {
        "sepal_length": np.random.normal(6.6, 0.64, n_per_species),
        "sepal_width": np.random.normal(3.0, 0.32, n_per_species),
        "petal_length": np.random.normal(5.5, 0.55, n_per_species),
        "petal_width": np.random.normal(2.0, 0.27, n_per_species),
        "species": "virginica",
    }
)

df = pd.concat([setosa, versicolor, virginica], ignore_index=True)

# Normalize numeric columns to [0, 1] for fair comparison across axes
numeric_cols = ["sepal_length", "sepal_width", "petal_length", "petal_width"]
df_norm = df.copy()
for col in numeric_cols:
    min_val = df[col].min()
    max_val = df[col].max()
    df_norm[col] = (df[col] - min_val) / (max_val - min_val)

# Colors by species (Imprint palette, canonical order)
species_order = ["setosa", "versicolor", "virginica"]
colors = dict(zip(species_order, IMPRINT_PALETTE[:3], strict=True))

# One multi-line source: each row of xs/ys is a single observation's polyline
x_coords = list(range(len(numeric_cols)))
source = ColumnDataSource(
    data={
        "xs": [x_coords] * len(df_norm),
        "ys": df_norm[numeric_cols].values.tolist(),
        "species": df_norm["species"].str.capitalize(),
        "color": [colors[s] for s in df_norm["species"]],
    }
)

# Create figure (3200x1800 px landscape canvas)
title = "parallel-basic · python · bokeh · anyplot.ai"
p = figure(
    width=3200,
    height=1800,
    title=title,
    x_axis_label="Dimension",
    y_axis_label="Normalized Value",
    x_range=(-0.3, 3.3),
    y_range=(-0.05, 1.10),
    toolbar_location=None,  # bokeh's default toolbar adds ~30-50px above the plot
    min_border_bottom=160,  # room for 34pt x-tick labels + 42pt x-axis label
    min_border_left=180,  # room for 34pt y-tick labels + 42pt y-axis label
    min_border_top=110,  # room for 50pt title
    min_border_right=50,
)

# Plot parallel coordinates - one polyline per observation, colored by species.
# line_alpha=0.4 (down from 0.5) eases the densest crossover region (Sepal Width)
# while still preserving the crossing pattern. muted_alpha lets a legend click
# isolate a single species - a bokeh-distinctive touch beyond the plain HoverTool.
renderer = p.multi_line(
    xs="xs",
    ys="ys",
    source=source,
    line_color="color",
    line_alpha=0.4,
    line_width=2.5,
    legend_field="species",
    muted_alpha=0.05,
)

# Hover shows which species a given line belongs to - bokeh's signature interactive feature
hover = HoverTool(renderers=[renderer], tooltips=[("Species", "@species")], line_policy="nearest")
p.add_tools(hover)

# Vertical axis line per dimension - bolder than the shared 0.15-alpha grid so
# each of the four parallel-coordinate axes reads as a distinct anchor line.
for x in x_coords:
    p.add_layout(Span(location=x, dimension="height", line_color=INK_SOFT, line_width=2, line_alpha=0.6))

p.legend.click_policy = "mute"
p.legend.title = "Species"
p.legend.location = "top_right"
p.legend.label_text_font_size = "30pt"
p.legend.title_text_font_size = "32pt"
p.legend.background_fill_color = ELEVATED_BG
p.legend.border_line_color = None
p.legend.label_text_color = INK_SOFT
p.legend.title_text_color = INK

# Custom x-axis labels with original scale ranges
axis_labels = [
    f"Sepal Length\n({df['sepal_length'].min():.1f}-{df['sepal_length'].max():.1f} cm)",
    f"Sepal Width\n({df['sepal_width'].min():.1f}-{df['sepal_width'].max():.1f} cm)",
    f"Petal Length\n({df['petal_length'].min():.1f}-{df['petal_length'].max():.1f} cm)",
    f"Petal Width\n({df['petal_width'].min():.1f}-{df['petal_width'].max():.1f} cm)",
]
p.xaxis.ticker = x_coords
p.xaxis.major_label_overrides = dict(enumerate(axis_labels))

# Text sizes and typography for 3200x1800 px canvas - helvetica throughout for
# a deliberate, publication-grade look rather than bokeh's default font stack
p.title.text_font = "helvetica"
p.title.text_font_size = "50pt"
p.xaxis.axis_label_text_font = "helvetica"
p.yaxis.axis_label_text_font = "helvetica"
p.xaxis.axis_label_text_font_size = "42pt"
p.yaxis.axis_label_text_font_size = "42pt"
p.xaxis.major_label_text_font = "helvetica"
p.yaxis.major_label_text_font = "helvetica"
p.xaxis.major_label_text_font_size = "34pt"
p.yaxis.major_label_text_font_size = "34pt"
p.legend.label_text_font = "helvetica"
p.legend.title_text_font = "helvetica"

# Theme-adaptive chrome
p.background_fill_color = PAGE_BG
p.border_fill_color = PAGE_BG
p.outline_line_color = INK_SOFT

p.title.text_color = INK
p.xaxis.axis_label_text_color = INK
p.yaxis.axis_label_text_color = INK
p.xaxis.major_label_text_color = INK_SOFT
p.yaxis.major_label_text_color = INK_SOFT
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

# Grid styling - subtle
p.xgrid.grid_line_color = INK
p.ygrid.grid_line_color = INK
p.xgrid.grid_line_alpha = 0.15
p.ygrid.grid_line_alpha = 0.15

# Save outputs - write HTML then screenshot with headless Chrome (bokeh's export_png
# is unreliable in this environment; see prompts/library/bokeh.md)
output_file(f"plot-{THEME}.html", title=title)
save(p)

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)
driver.get(f"file://{Path(f'plot-{THEME}.html').resolve()}")
# Headless Chrome's --window-size sets the OUTER window (a phantom ~143px
# title bar eats into it even headless), so innerHeight ends up short of H.
# Override the viewport directly via CDP for an exact WxH capture.
driver.execute_cdp_cmd(
    "Emulation.setDeviceMetricsOverride", {"width": W, "height": H, "deviceScaleFactor": 1, "mobile": False}
)
time.sleep(3)  # let bokeh's JS render the canvas
driver.save_screenshot(f"plot-{THEME}.png")
driver.quit()

Part of Basic Parallel Coordinates Plot on anyplot.ai.

Other implementations