Parallel Axis Chart Examples
Publication-quality parallel coordinates charts following matplotlib styling conventions. Visualize multivariate data with smooth curves, interactive highlighting, and support for both continuous and categorical axes.
Type Interface
import type { BaseChartProps } from '../../types';
import type { LegendLayout } from '../../components';
export type AxisType = 'continuous' | 'categorical';
export interface AxisDimension {
/** Name of the dimension (displayed as axis label) */
name: string;
/** Type of axis: continuous (numeric) or categorical */
type: AxisType;
/** For continuous: min value. For categorical: undefined */
min?: number;
/** For continuous: max value. For categorical: undefined */
max?: number;
/** For categorical: ordered category values (max 10) */
categories?: string[];
}
export interface ParallelDataSeries {
/** Label for this data series */
label: string;
/** Values for each dimension (must match dimensions array length) */
values: (number | string)[];
/** Optional color override */
color?: string;
/** Optional line width override */
lineWidth?: number;
/** Optional opacity */
opacity?: number;
}
export interface ParallelAxisProps extends BaseChartProps {
/** Axis dimensions configuration (2-10 axes supported) */
dimensions: AxisDimension[];
/** Data series */
series: ParallelDataSeries[];
/** Chart title */
title?: string;
/** Show legend */
legend?: boolean;
/** Layout of the legend */
layout?: LegendLayout;
/** Custom axis spacing (default: auto-calculated) */
axisSpacing?: number;
/** Line opacity when not highlighted (default: 0.6) */
defaultOpacity?: number;
/** Line opacity when highlighted (default: 1.0) */
highlightOpacity?: number;
/** Line width when highlighted (default: 3x normal) */
highlightLineWidth?: number;
/** Layout position for hover value indicators relative to the chart: "above" (above chart), "below" (below chart), or "auto" (automatic) */
hoverLayout?: "above" | "below" | "auto";
/** Callback when hovering over a line */
onHover?: (seriesIndex: number | null) => void;
/** Currently selected series indices */
selectedIndices?: number[];
/** Callback when selection changes */
onSelectionChange?: (selectedIndices: number[]) => void;
/** Allow multiple selections (default: true) */
multiSelect?: boolean;
/** Children components (for overlays) */
children?: React.ReactNode;
}
Iris Dataset
Classic Iris dataset visualization showing four continuous dimensions. Hover over lines to highlight individual data points.
import { ParallelAxis } from '@vuer-ai/vuer-viz';
// Classic Iris dataset example
const dimensions = [
{ name: 'Sepal Length', type: 'continuous' as const, min: 4, max: 8 },
{ name: 'Sepal Width', type: 'continuous' as const, min: 2, max: 5 },
{ name: 'Petal Length', type: 'continuous' as const, min: 1, max: 7 },
{ name: 'Petal Width', type: 'continuous' as const, min: 0, max: 3 },
];
const series = [
{
label: 'Setosa',
values: [5.1, 3.5, 1.4, 0.2],
color: '#fa2525',
},
{
label: 'Setosa 2',
values: [4.9, 3.0, 1.4, 0.2],
color: '#fa2525',
},
{
label: 'Versicolor',
values: [7.0, 3.2, 4.7, 1.4],
color: '#1080dc',
},
{
label: 'Versicolor 2',
values: [6.4, 3.2, 4.5, 1.5],
color: '#1080DC',
},
{
label: 'Virginica',
values: [6.3, 3.3, 6.0, 2.5],
color: '#1080DC',
},
{
label: 'Virginica 2',
values: [5.8, 2.7, 5.1, 1.9],
color: '#1080DC',
},
];
export default function IrisDatasetExample() {
return (
<ParallelAxis
dimensions={dimensions}
series={series}
title="Iris Dataset Features"
width={700}
legend
hoverLayout="above"
/>
);
}
Event Handling & Selection API
ParallelAxis supports interactive hover and selection events, allowing you to build rich data exploration interfaces.
Hover Events
Move your mouse over lines to highlight them with increased opacity and a thicker border:
- Value indicators appear near the legend showing current values for all dimensions
- Configure position with
hoverLayoutprop: "above" (above chart), "below" (below chart), or "auto" (automatic) - Use
onHovercallback to receive hover events:(seriesIndex: number | null) => void
Selection Events
Click on lines to select/deselect experiments:
- Controlled via
selectedIndicesprop: array of selected series indices - Triggered via
onSelectionChangecallback:(selectedIndices: number[]) => void multiSelectprop enables/disables multiple selection (default:true)- Selected lines maintain highlight styling with increased opacity
- Unselected lines fade to 20% opacity when selections exist
- Useful for filtering, comparison, and detailed inspection workflows
Basic Selection Example
import { useState } from 'react'; import { ParallelAxis } from '@vuer-ai/vuer-viz'; function MyChart() { const [selectedIndices, setSelectedIndices] = useState<number[]>([]); return ( <> <ParallelAxis dimensions={dimensions} series={series} selectedIndices={selectedIndices} onSelectionChange={setSelectedIndices} multiSelect={true} // Allow multiple selections /> {/* Display selected items */} <div> Selected: {selectedIndices.map(i => series[i].label).join(', ')} </div> </> ); }
Single Selection Mode
Set multiSelect={false} to allow only one selection at a time:
<ParallelAxis dimensions={dimensions} series={series} selectedIndices={selectedIndices} onSelectionChange={setSelectedIndices} multiSelect={false} // Only one selection allowed />
Programmatic Selection
You can programmatically control selections:
// Select top performers const selectTopN = (n: number) => { const sorted = series .map((s, i) => ({ index: i, score: s.values[4] as number })) .sort((a, b) => b.score - a.score) .slice(0, n) .map(item => item.index); setSelectedIndices(sorted); }; // Clear all selections const clearSelection = () => setSelectedIndices([]); // Select all const selectAll = () => setSelectedIndices(series.map((_, i) => i));
Selection with Table Example
Interactive selection example showing how to use selectedIndices and onSelectionChange to build data exploration interfaces. Click lines to select/deselect experiments and view details in the table below.
Selected Experiments (0)
| Config | Learning Rate | Batch Size | Hidden Dim | Num Layers | Reward |
|---|---|---|---|---|---|
| this table is empty | |||||
import { ParallelAxis } from '@vuer-ai/vuer-viz';
import { useState } from 'react';
// Hyperparameter sweep example data
const dimensions = [
{ name: 'Learning Rate', type: 'continuous' as const, min: 0.0001, max: 0.01 },
{ name: 'Batch Size', type: 'categorical' as const, categories: ['32', '64', '128', '256'] },
{ name: 'Hidden Dim', type: 'continuous' as const, min: 128, max: 512 },
{ name: 'Num Layers', type: 'categorical' as const, categories: ['2', '3', '4', '5'] },
{ name: 'Reward', type: 'continuous' as const, min: 500, max: 950 },
];
const series = [
{
label: 'config-0',
values: [0.001, '64', 256, '3', 850],
color: '#fa2525',
},
{
label: 'config-1',
values: [0.0001, '128', 512, '4', 920],
color: '#1080dc',
},
{
label: 'config-2',
values: [0.01, '32', 128, '2', 650],
color: '#23aaff',
},
{
label: 'config-3',
values: [0.005, '256', 384, '3', 780],
color: '#ff9500',
},
{
label: 'config-4',
values: [0.0003, '64', 256, '4', 890],
color: '#34c759',
},
{
label: 'config-5',
values: [0.002, '128', 192, '3', 720],
color: '#af52de',
},
{
label: 'config-6',
values: [0.0005, '256', 448, '5', 860],
color: '#ff2d55',
},
];
export default function SelectionWithTableExample() {
const [selectedIndices, setSelectedIndices] = useState<number[]>([]);
// Get selected experiments
const selectedExperiments = selectedIndices.map(i => series[i]);
return (
<div className="space-y-6">
{/* Parallel Coordinates Chart */}
<div>
<ParallelAxis
dimensions={dimensions}
series={series}
title="Hyperparameter Sweep (Click lines to select)"
width={800}
height={300}
legend={false}
hoverLayout="below"
selectedIndices={selectedIndices}
onSelectionChange={setSelectedIndices}
multiSelect={true}
/>
</div>
{/* Selected Experiments Table */}
<div>
<h3 className="text-lg font-semibold mb-3">
Selected Experiments ({selectedExperiments.length})
</h3>
<div className="overflow-x-auto">
<table className="min-w-full border border-gray-300 dark:border-gray-700">
<thead className="bg-gray-100 dark:bg-gray-800">
<tr>
<th className="px-4 py-2 text-left border-b border-gray-300 dark:border-gray-700">
Config
</th>
<th className="px-4 py-2 text-left border-b border-gray-300 dark:border-gray-700">
Learning Rate
</th>
<th className="px-4 py-2 text-left border-b border-gray-300 dark:border-gray-700">
Batch Size
</th>
<th className="px-4 py-2 text-left border-b border-gray-300 dark:border-gray-700">
Hidden Dim
</th>
<th className="px-4 py-2 text-left border-b border-gray-300 dark:border-gray-700">
Num Layers
</th>
<th className="px-4 py-2 text-left border-b border-gray-300 dark:border-gray-700">
Reward
</th>
</tr>
</thead>
<tbody>
{selectedExperiments.length === 0 ? (
<tr>
<td
colSpan={6}
className="px-4 py-8 text-center text-gray-400 dark:text-gray-600 italic"
>
this table is empty
</td>
</tr>
) : (
selectedExperiments.map((exp, idx) => (
<tr
key={exp.label}
className={idx % 2 === 0 ? 'bg-white dark:bg-gray-900' : 'bg-gray-50 dark:bg-gray-800'}
>
<td className="px-4 py-2 border-b border-gray-200 dark:border-gray-700 font-mono">
<span
className="inline-block w-3 h-3 rounded-full mr-2"
style={{ backgroundColor: exp.color }}
/>
{exp.label}
</td>
<td className="px-4 py-2 border-b border-gray-200 dark:border-gray-700">
{exp.values[0]}
</td>
<td className="px-4 py-2 border-b border-gray-200 dark:border-gray-700">
{exp.values[1]}
</td>
<td className="px-4 py-2 border-b border-gray-200 dark:border-gray-700">
{exp.values[2]}
</td>
<td className="px-4 py-2 border-b border-gray-200 dark:border-gray-700">
{exp.values[3]}
</td>
<td className="px-4 py-2 border-b border-gray-200 dark:border-gray-700 font-semibold">
{exp.values[4]}
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
{/* Controls */}
<div className="flex gap-3 text-sm">
<button
onClick={() => setSelectedIndices([])}
className="px-3 py-1.5 bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 rounded"
disabled={selectedExperiments.length === 0}
>
Clear Selection
</button>
<button
onClick={() => setSelectedIndices(series.map((_, i) => i))}
className="px-3 py-1.5 bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 rounded"
>
Select All
</button>
<button
onClick={() => {
// Select top 3 by reward
const sorted = series
.map((s, i) => ({ index: i, reward: s.values[4] as number }))
.sort((a, b) => b.reward - a.reward)
.slice(0, 3)
.map(item => item.index);
setSelectedIndices(sorted);
}}
className="px-3 py-1.5 bg-blue-500 hover:bg-blue-600 text-white rounded"
>
Select Top 3
</button>
</div>
</div>
);
}
Categorical Data
Demonstration of mixed continuous and categorical axes. Categories are ordered and can have up to 10 values per axis.
import { ParallelAxis } from '@vuer-ai/vuer-viz';
// Example with categorical axes
const dimensions = [
{
name: 'Size',
type: 'categorical' as const,
categories: ['Small', 'Medium', 'Large'],
},
{ name: 'Price', type: 'continuous' as const, min: 0, max: 100 },
{
name: 'Quality',
type: 'categorical' as const,
categories: ['Low', 'Medium', 'High'],
},
{ name: 'Rating', type: 'continuous' as const, min: 0, max: 5 },
];
const series = [
{
label: 'Product A',
values: ['Small', 25, 'High', 4.5],
color: '#f12e2e',
},
{
label: 'Product B',
values: ['Large', 75, 'Medium', 3.8],
color: '#15c6b9',
},
{
label: 'Product C',
values: ['Medium', 50, 'High', 4.2],
color: '#0ea589',
},
{
label: 'Product D',
values: ['Large', 90, 'Low', 2.5],
color: '#facc17',
},
];
export default function CategoricalDataExample() {
return (
<ParallelAxis
dimensions={dimensions}
series={series}
title="Product Comparison"
width={700}
legend
hoverLayout="auto"
/>
);
}
Weather Patterns
Seasonal weather data comparison across multiple metrics using smooth curves.
import { ParallelAxis } from '@vuer-ai/vuer-viz';
// Weather data across different metrics
const dimensions = [
{ name: 'Temperature (°C)', type: 'continuous' as const, min: -10, max: 40 },
{ name: 'Humidity (%)', type: 'continuous' as const, min: 0, max: 100 },
{ name: 'Wind Speed (km/h)', type: 'continuous' as const, min: 0, max: 50 },
{ name: 'Precipitation (mm)', type: 'continuous' as const, min: 0, max: 100 },
];
const series = [
{
label: 'Spring',
values: [15, 60, 15, 40],
color: '#1dc186',
},
{
label: 'Summer',
values: [30, 50, 10, 10],
color: '#ed7420',
},
{
label: 'Autumn',
values: [12, 70, 20, 60],
color: '#e6251b',
},
{
label: 'Winter',
values: [-5, 80, 25, 50],
color: '#1769ec',
},
];
export default function WeatherPatternsExample() {
return (
<ParallelAxis
dimensions={dimensions}
series={series}
title="Seasonal Weather Patterns"
width={700}
legend
hoverLayout="below"
/>
);
}
Features
- Matplotlib-style appearance with Times New Roman font
- Smooth curves using Catmull-Rom splines
- Interactive hover highlighting with thicker lines
- Support for continuous and categorical axes
- Maximum 10 categories per categorical axis
- Automatic value normalization
- Prevents text selection during interaction
- Customizable colors, opacity, and line widths
- Legend support with color coding
- Export-ready at 325x220px (or custom dimensions)
Axis Interaction
- Click Axis: Click on any axis line or label to toggle between categorical and continuous types
- State persists across page refreshes using localStorage
- Each axis configuration is indexed by its name (low, high, vtype)
- Categorical mode extracts unique values and sorts them
- Cannot switch to continuous if values contain non-numeric data
- Text Selection: Prevented during mouse movement for smooth interaction
- Lines automatically fade when another line is highlighted or selected
State Management
The ParallelAxis component uses Jotai for state management. Each axis configuration is stored in an atom indexed by the axis name, persisting:
vtype: Type of axis (continuous or categorical)low: Minimum value for continuous axeshigh: Maximum value for continuous axescategories: Category values for categorical axes
You can access and modify the axis configuration state using the exported axisConfigsAtom from @vuer-ai/vuer-viz.