Basic Parallel Coordinates Plot — lets-plot

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 lets-plot

Python source (lets-plot)

""" anyplot.ai
parallel-basic: Basic Parallel Coordinates Plot
Library: letsplot 4.11.0 | Python 3.13.14
Quality: 87/100 | Updated: 2026-07-24
"""

import os

import pandas as pd
from lets_plot import (
    LetsPlot,
    aes,
    element_blank,
    element_line,
    element_rect,
    element_text,
    geom_line,
    geom_segment,
    geom_text,
    ggplot,
    ggsave,
    ggsize,
    labs,
    scale_alpha_identity,
    scale_color_manual,
    scale_size_identity,
    scale_x_continuous,
    scale_y_continuous,
    theme,
)


LetsPlot.setup_html()

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 positions 1-3 — first series always #009E73
IMPRINT = ["#009E73", "#C475FD", "#4467A3"]

# Data - Iris dataset with 4 dimensions
# Using 30 samples (10 per species) for clarity
data = {
    "sepal_length": [
        5.1,
        4.9,
        4.7,
        4.6,
        5.0,
        5.4,
        4.6,
        5.0,
        4.4,
        4.9,
        7.0,
        6.4,
        6.9,
        5.5,
        6.5,
        5.7,
        6.3,
        4.9,
        6.6,
        5.2,
        6.3,
        5.8,
        7.1,
        6.3,
        6.5,
        7.6,
        4.9,
        7.3,
        6.7,
        7.2,
    ],
    "sepal_width": [
        3.5,
        3.0,
        3.2,
        3.1,
        3.6,
        3.9,
        3.4,
        3.4,
        2.9,
        3.1,
        3.2,
        3.2,
        3.1,
        2.3,
        2.8,
        2.8,
        3.3,
        2.4,
        2.9,
        2.7,
        3.3,
        2.7,
        3.0,
        2.9,
        3.0,
        3.0,
        2.5,
        2.9,
        2.5,
        3.6,
    ],
    "petal_length": [
        1.4,
        1.4,
        1.3,
        1.5,
        1.4,
        1.7,
        1.4,
        1.5,
        1.4,
        1.5,
        4.7,
        4.5,
        4.9,
        4.0,
        4.6,
        4.5,
        4.7,
        3.3,
        4.6,
        3.9,
        6.0,
        5.1,
        5.9,
        5.6,
        5.8,
        6.6,
        4.5,
        6.3,
        5.8,
        6.1,
    ],
    "petal_width": [
        0.2,
        0.2,
        0.2,
        0.2,
        0.2,
        0.4,
        0.3,
        0.2,
        0.2,
        0.1,
        1.4,
        1.5,
        1.5,
        1.3,
        1.5,
        1.3,
        1.6,
        1.0,
        1.3,
        1.4,
        2.5,
        1.9,
        2.1,
        1.8,
        2.2,
        2.1,
        1.7,
        1.8,
        1.8,
        2.5,
    ],
    "species": ["Setosa"] * 10 + ["Versicolor"] * 10 + ["Virginica"] * 10,
}

df = pd.DataFrame(data)

# Dimensions to plot (shorter labels to avoid overlap)
dimensions = ["sepal_length", "sepal_width", "petal_length", "petal_width"]
dim_labels = ["Sepal\nLength (cm)", "Sepal\nWidth (cm)", "Petal\nLength (cm)", "Petal\nWidth (cm)"]

# Normalize each dimension to 0-1 range for fair comparison
df_normalized = df.copy()
for dim in dimensions:
    min_val = df[dim].min()
    max_val = df[dim].max()
    df_normalized[dim] = (df[dim] - min_val) / (max_val - min_val)

# Convert to long format for parallel coordinates
line_data = []
for idx, row in df_normalized.iterrows():
    obs_id = idx
    species = row["species"]
    for i, dim in enumerate(dimensions):
        line_data.append({"x": i, "y": row[dim], "observation": obs_id, "species": species})

line_df = pd.DataFrame(line_data)

# Fix the Imprint color order explicitly (Setosa always gets brand green,
# regardless of row/draw order) — see default-style-guide.md "First series
# is ALWAYS #009E73"
species_order = ["Setosa", "Versicolor", "Virginica"]
line_df["species"] = pd.Categorical(line_df["species"], categories=species_order, ordered=True)

# Setosa shows the sharpest separation on petal dimensions — rendered heavier
# and more opaque than the other species, and drawn last (on top), so it
# reads as the visual focal point.
focus_species = "Setosa"
line_df["line_size"] = (line_df["species"] == focus_species).map({True: 1.3, False: 0.65})
line_df["line_alpha"] = (line_df["species"] == focus_species).map({True: 0.9, False: 0.5})
line_df = pd.concat([line_df[line_df["species"] != focus_species], line_df[line_df["species"] == focus_species]])

# Create axis lines data (vertical lines at each x position)
axis_data = []
for i in range(len(dimensions)):
    axis_data.append({"x": i, "y": 0, "xend": i, "yend": 1})

axis_df = pd.DataFrame(axis_data)

# Horizontal rules tying the axis tops and bottoms together into one frame
frame_df = pd.DataFrame(
    {"x": [-0.3, -0.3], "y": [0, 1], "xend": [len(dimensions) - 0.7, len(dimensions) - 0.7], "yend": [0, 1]}
)

# Create label data for dimension names at the bottom
label_data = []
for i, label in enumerate(dim_labels):
    label_data.append({"x": i, "y": -0.15, "label": label})

label_df = pd.DataFrame(label_data)

# Create tick labels for each axis (showing original scale) - only min and max
tick_data = []
for i, dim in enumerate(dimensions):
    min_val = df[dim].min()
    max_val = df[dim].max()
    tick_data.append({"x": i - 0.08, "y": 0, "label": f"{min_val:.1f}"})
    tick_data.append({"x": i - 0.08, "y": 1, "label": f"{max_val:.1f}"})

tick_df = pd.DataFrame(tick_data)

anyplot_theme = theme(
    plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
    panel_background=element_rect(fill=PAGE_BG),
    # Only horizontal gridlines — vertical gridlines would create a
    # phantom-column look that competes with the 4 manually-drawn axis bars.
    panel_grid_major_x=element_blank(),
    panel_grid_major_y=element_line(color=INK_SOFT, size=0.2),
    panel_grid_minor=element_blank(),
    axis_title=element_blank(),
    axis_text=element_blank(),
    axis_ticks=element_blank(),
    axis_line=element_blank(),
    plot_title=element_text(color=INK, size=17),
    legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),
    legend_text=element_text(color=INK_SOFT, size=9),
    legend_title=element_text(color=INK, size=10),
)

# Plot
plot = (
    ggplot()
    # Vertical axis lines (theme-adaptive color)
    + geom_segment(aes(x="x", y="y", xend="xend", yend="yend"), data=axis_df, color=INK_SOFT, size=1)
    # Subtle top/bottom rule tying all axes into one frame
    + geom_segment(aes(x="x", y="y", xend="xend", yend="yend"), data=frame_df, color=INK_SOFT, size=0.5, alpha=0.3)
    # Data lines connecting observations across dimensions — Setosa rendered
    # heavier/more opaque (line_size, line_alpha) as the storytelling focal point
    + geom_line(
        aes(x="x", y="y", group="observation", color="species", size="line_size", alpha="line_alpha"), data=line_df
    )
    # Imprint palette — first series is brand green; size/alpha are literal
    # values already computed above, not separate legend-worthy aesthetics
    + scale_color_manual(values=IMPRINT)
    + scale_size_identity()
    + scale_alpha_identity()
    # Dimension labels at the bottom (theme-adaptive color, size matched to base ggsize(800,450))
    + geom_text(aes(x="x", y="y", label="label"), data=label_df, size=10, color=INK)
    # Tick value labels on the left side of axes (theme-adaptive color)
    + geom_text(aes(x="x", y="y", label="label"), data=tick_df, size=8, color=INK_SOFT, hjust=1)
    # Styling
    + scale_x_continuous(limits=(-0.4, len(dimensions) - 0.6))
    + scale_y_continuous(limits=(-0.32, 1.1))
    + labs(title="parallel-basic · python · letsplot · anyplot.ai", color="Species")
    + ggsize(800, 450)
    + anyplot_theme
)

# Save with theme-named output files
ggsave(plot, f"plot-{THEME}.png", path=".", scale=4)
ggsave(plot, f"plot-{THEME}.html", path=".")

Part of Basic Parallel Coordinates Plot on anyplot.ai.

Other implementations