Event Handling
Add interactivity to your charts with event handling and overlay components.
Complete Example
Here's a fully interactive chart combining markers and tooltips. Hover over the chart to see both the vertical marker line and a tooltip showing all series values:
import { useState } from "react";
import { LineChart, VerticalMarker, SeriesTooltip } from "@vuer-ai/vuer-viz";
const xValues = Array.from({ length: 500 }, (_, i) => i);
const series = [
{
label: "Temperature",
x: xValues,
y: xValues.map(i => 20 + Math.sin(i / 5) * 8 + Math.random() * 3),
color: "#e74c3c",
},
{
label: "Humidity",
x: xValues,
y: xValues.map(i => 30 + Math.cos(i / 7) * 15 + Math.random() * 5),
color: "#3498db",
},
{
label: "Pressure",
x: xValues,
y: xValues.map(i => 70 + Math.sin(i / 10) * 5 + Math.random() * 2),
color: "#2ecc71",
},
];
export default function CompleteInteractiveExample() {
const [cursorX, setCursorX] = useState<number | null>(null);
return (
<LineChart
series={series}
width={700}
height={300}
xLabel="Time"
yLabel="Values"
title="Interactive Chart with Marker and Tooltip"
xFormat="minutes"
onChange={(e) => setCursorX(e.x)}
>
<VerticalMarker x={cursorX} color="#f00" lineWidth={1} />
<SeriesTooltip
x={cursorX}
series={series}
xFormat="minutes"
/>
</LineChart>
);
}
Overview
Charts support a unified onChange event handler that fires when the user interacts with the chart:
- Hover - Moving the mouse over the chart
- Click - Clicking on the chart
- Drag - Panning the chart (future feature)
- Scroll - Zooming the chart (future feature)
The event provides canonical data coordinates, allowing you to build interactive features like cursor tracking, value inspection, and synchronized multi-chart views.
Event API
onChange Handler
Charts accept an onChange prop that receives a ChartChangeEvent:
interface ChartChangeEvent { // Current pointer position in canonical data units x: number | null; y: number | null; // Current visible bounds bounds: { xMin: number; xMax: number; yMin: number; yMax: number; }; // Event type type: 'hover' | 'click' | 'drag' | 'scroll'; // Nearest data point (for LineChart) nearestPoint?: { seriesId: string; seriesLabel: string; dataIndex: number; x: number; y: number; }; // Original DOM event nativeEvent?: MouseEvent | WheelEvent; }
Coordinate System
Events return values in canonical data units - the same units used in your series data:
- x, y: Canonical values (e.g., Unix timestamps, sample indices, or any numeric values)
- bounds: Always in the chart's canonical units
- xFormatter/yFormatter: Convert canonical values to display strings
This design allows synchronized charts with the same canonical domain to share cursor positions directly.
Overlay Components
VerticalMarker
Renders a vertical line at a specific x-coordinate:
<VerticalMarker x={scrubX} // Canonical x value (null to hide) color="#666" // Line color lineWidth={1} // Line width in pixels dashArray="4,4" // SVG dash pattern />
Tooltip
Displays formatted values at a position:
<Tooltip x={hoverX} // Canonical x value y={hoverY} // Optional y value formatX={(x) => new Date(x).toLocaleTimeString()} // Format function formatY={(y) => y.toFixed(2)} // Format function />
Examples
Basic Hover Tracking
Track the cursor position and display a vertical marker:
import { useState } from "react";
import { LineChart, VerticalMarker } from "@vuer-ai/vuer-viz";
const x = Array.from({ length: 50 }, (_, i) => i);
const y = x.map(i => 20 + Math.sin(i / 5) * 10 + Math.random() * 3);
const series = [
{
label: "Temperature",
x,
y,
color: "#e74c3c",
},
];
export default function BasicHoverExample() {
const [hoverX, setHoverX] = useState<number | null>(null);
return (
<LineChart
series={series}
width={600}
height={300}
xLabel="Time (s)"
yLabel="Temperature (°C)"
title="Hover to Track Values"
onChange={(e) => setHoverX(e.x)}
>
<VerticalMarker x={hoverX} color="#3498db" lineWidth={2} />
</LineChart>
);
}
Synchronized Cursor Across Multiple Charts
Share a single cursor position across multiple charts with the same canonical x-domain:
import { useState } from "react";
import { LineChart, SeriesTooltip, VerticalMarker } from "@vuer-ai/vuer-viz";
import { temperatureData, pressureData, humidityData } from "./synchronized-cursor-data";
export default function SynchronizedCursorExample() {
const [scrubX, setScrubX] = useState<number | null>(null);
return (
<div style={{ display: "flex", flexDirection: "column", gap: "20px" }}>
<LineChart
series={temperatureData}
width={700}
height={200}
xLabel="Time"
yLabel="Temperature (°C)"
title="Temperature Sensors"
xFormat="time"
onChange={(e) => setScrubX(e.x)}
>
<VerticalMarker x={scrubX} />
<SeriesTooltip x={scrubX} series={temperatureData} xFormat="time" />
</LineChart>
<LineChart
series={pressureData}
width={700}
height={200}
xLabel="Time"
yLabel="Pressure (kPa)"
title="Atmospheric Pressure"
xFormat="time"
onChange={(e) => setScrubX(e.x)}
>
<VerticalMarker x={scrubX} />
<SeriesTooltip x={scrubX} series={pressureData} xFormat="time" />
</LineChart>
<LineChart
series={humidityData}
width={700}
height={200}
xLabel="Time"
yLabel="Humidity (%)"
title="Relative Humidity"
xFormat="time"
onChange={(e) => setScrubX(e.x)}
>
<VerticalMarker x={scrubX} />
<SeriesTooltip x={scrubX} series={humidityData} xFormat="time" />
</LineChart>
</div>
);
}
Key Insight: All three charts use the same canonical x-domain (Unix timestamps in milliseconds). The scrubX state is shared directly because the charts have compatible units. Each chart applies its own xFormatter to display the timestamps in a human-readable format.
Click to Pin Marker
Click to lock the marker position for detailed inspection:
import { useState } from "react";
import { LineChart, VerticalMarker } from "@vuer-ai/vuer-viz";
const x = Array.from({ length: 50 }, (_, i) => i);
const y = x.map(i => Math.sin(i / 3) * 50 + 50 + Math.random() * 10);
const series = [
{
label: "Signal",
x,
y,
color: "#3498db",
},
];
export default function ClickToPinExample() {
const [pinnedX, setPinnedX] = useState<number | null>(25);
const [hoverX, setHoverX] = useState<number | null>(null);
const handleChange = (e: any) => {
if (e.type === "click") {
// Toggle pin: if clicking near the pinned position, unpin
if (pinnedX !== null && Math.abs(e.x - pinnedX) < 2) {
setPinnedX(null);
} else {
setPinnedX(e.x);
}
} else if (e.type === "hover") {
setHoverX(e.x);
}
};
return (
<div>
<LineChart
series={series}
width={600}
height={300}
xLabel="Sample"
yLabel="Amplitude"
title="Click to Pin Marker (Click again to unpin)"
onChange={handleChange}
>
{/* Pinned marker (solid red) */}
{pinnedX !== null && (
<VerticalMarker x={pinnedX} color="#e74c3c" lineWidth={2} />
)}
{/* Hover marker (dashed blue) - only show when not pinned at same location */}
{hoverX !== null && hoverX !== pinnedX && (
<VerticalMarker x={hoverX} color="#3498db" lineWidth={1} dashArray="4,1" />
)}
</LineChart>
<div className="mt-2.5 text-sm text-gray-600 dark:text-gray-400">
{pinnedX !== null ? (
<div>
<strong>Pinned at x = {pinnedX.toFixed(1)}</strong> (click near marker to unpin)
</div>
) : (
<div>No marker pinned. Click anywhere on the chart to pin.</div>
)}
</div>
</div>
);
}
Working with Different Data Domains
Same Canonical Domain
When charts share the same canonical x-domain, synchronization is straightforward:
const [sharedX, setSharedX] = useState<number | null>(null); // All charts use timestamps in milliseconds <LineChart series={temperatureData} // x: 1699123456000, 1699123457000, ... xFormatter={(ms) => new Date(ms).toLocaleTimeString()} onChange={(e) => setSharedX(e.x)} > <VerticalMarker x={sharedX} /> </LineChart> <LineChart series={pressureData} // x: 1699123456000, 1699123457000, ... xFormatter={(ms) => new Date(ms).toLocaleTimeString()} onChange={(e) => setSharedX(e.x)} > <VerticalMarker x={sharedX} /> </LineChart>
Different Canonical Domains
If charts have incompatible domains (e.g., one uses timestamps, another uses sample indices), you must provide domain mapping functions:
const [referenceX, setReferenceX] = useState<number | null>(null); // Convert between domains const TIME_START = 1699123400000; const SAMPLE_RATE = 10; // samples per second const timestampToIndex = (ms: number) => ((ms - TIME_START) / 1000) * SAMPLE_RATE; const indexToTimestamp = (index: number) => TIME_START + (index / SAMPLE_RATE) * 1000; // Chart 1: Timestamps <LineChart series={timeSeriesData} onChange={(e) => setReferenceX(e.x)} // Store timestamp > <VerticalMarker x={referenceX} /> </LineChart> // Chart 2: Sample indices <LineChart series={sampleData} onChange={(e) => setReferenceX(indexToTimestamp(e.x))} > <VerticalMarker x={referenceX ? timestampToIndex(referenceX) : null} /> </LineChart>
BarChart Event Handling
BarCharts emit discrete selection events:
- x: Bar index (0, 1, 2, ...)
- y: Bar value
- nearestPoint: Contains the bar's label and data
const [selectedBar, setSelectedBar] = useState<number | null>(null); <BarChart data={barData} onChange={(e) => { if (e.type === 'click') { setSelectedBar(e.x); } }} />
Canvas Mode Compatibility
Event handling works with both SVG (canvas={false}) and hybrid canvas modes (canvas={true}):
- Data renders on canvas for performance
- Event layer and overlays use SVG
- Full interactivity is preserved
<LineChart series={largeDataset} canvas={true} // High-performance rendering onChange={(e) => setX(e.x)} // Events still work! > <VerticalMarker x={x} /> // SVG overlay </LineChart>
Future Features
The event system is designed to support additional interactions:
- Pan/Drag:
onChangewill emit updatedboundsas the user drags - Zoom/Scroll:
onChangewill emit newboundswhen scrolling to zoom - Series Toggle: Click legend items to show/hide series
These features will be added while maintaining API compatibility.
Best Practices
- Use canonical coordinates - Store positions in data units, not pixels
- Share domains for synchronization - Synced charts should use the same x-units
- Provide formatters - Use
xFormatter/yFormatterfor display - Handle null values - Overlay components should check for
x === null - Distinguish event types - Use
e.typeto handle hover vs click differently
API Reference
LineChart / BarChart Props
{ // Event handling onChange?: (event: ChartChangeEvent) => void; // Formatting xFormatter?: (x: number) => string; yFormatter?: (y: number) => string; // Overlays children?: React.ReactNode; }
ChartChangeEvent
interface ChartChangeEvent { x: number | null; y: number | null; bounds: { xMin: number; xMax: number; yMin: number; yMax: number }; type: 'hover' | 'click' | 'drag' | 'scroll'; nearestPoint?: { seriesId: string; seriesLabel: string; dataIndex: number; x: number; y: number; }; nativeEvent?: MouseEvent | WheelEvent; }
VerticalMarker Props
interface VerticalMarkerProps { x: number | null; // Canonical x position color?: string; // Default: "#666" lineWidth?: number; // Default: 1 dashArray?: string; // Default: "4,4" }
Tooltip Props
interface TooltipProps { x: number | null; y?: number | null; content?: React.ReactNode; formatX?: (x: number) => string; formatY?: (y: number) => string; offset?: { x: number; y: number }; backgroundColor?: string; textColor?: string; fontSize?: number; padding?: number; }