Markers & Ranges
Annotate your charts with markers, ranges, and event indicators to highlight important data points and regions.
Overview
While the base VerticalMarker component provides simple vertical lines, you can combine it with custom SVG elements to create sophisticated annotations:
- Threshold markers - Highlight when data crosses specific values
- Range selections - Mark and analyze specific data regions
- Event annotations - Annotate significant events in time-series data
- Custom overlays - Use any SVG elements as chart overlays
All overlay components can access the chart's coordinate system via useChartContext(), making it easy to position custom elements in data coordinates.
Threshold Markers
Detect and mark when data crosses predefined thresholds:
import { useState } from "react";
import { LineChart, VerticalMarker } from "@vuer-ai/vuer-viz";
// Generate data with some values exceeding thresholds
const x = Array.from({ length: 100 }, (_, i) => i);
const y = x.map(i => 50 + Math.sin(i / 10) * 30 + Math.random() * 20);
export default function ThresholdMarkersExample() {
const [selectedPoint, setSelectedPoint] = useState<number | null>(null);
const series = [
{
label: "Signal",
x,
y,
color: "#3498db",
},
];
// Define thresholds
const upperThreshold = 75;
const lowerThreshold = 25;
// Find threshold crossings
const upperCrossings = x.filter((_, i) => y[i] > upperThreshold);
const lowerCrossings = x.filter((_, i) => y[i] < lowerThreshold);
return (
<div>
<LineChart
series={series}
width={700}
height={350}
xLabel="Sample"
yLabel="Value"
title="Threshold Detection with Markers"
yMin={0}
yMax={100}
onChange={(e) => setSelectedPoint(e.x)}
>
{/* Threshold lines (horizontal) */}
<line
x1={60}
y1={40 + (100 - upperThreshold) * 2.6}
x2={720}
y2={40 + (100 - upperThreshold) * 2.6}
stroke="#e74c3c"
strokeWidth={2}
strokeDasharray="8,4"
pointerEvents="none"
/>
<line
x1={60}
y1={40 + (100 - lowerThreshold) * 2.6}
x2={720}
y2={40 + (100 - lowerThreshold) * 2.6}
stroke="#f39c12"
strokeWidth={2}
strokeDasharray="8,4"
pointerEvents="none"
/>
{/* Vertical markers at threshold crossings */}
{upperCrossings.slice(0, 3).map((x) => (
<VerticalMarker
key={`upper-${x}`}
x={x}
color="#e74c3c"
lineWidth={1}
dashArray="2,2"
/>
))}
{lowerCrossings.slice(0, 3).map((x) => (
<VerticalMarker
key={`lower-${x}`}
x={x}
color="#f39c12"
lineWidth={1}
dashArray="2,2"
/>
))}
{/* Hover marker */}
{selectedPoint !== null && (
<VerticalMarker x={selectedPoint} color="#f00" lineWidth={2} />
)}
</LineChart>
<div style={{ marginTop: "10px", fontSize: "14px", color: "#666" }}>
<div>
<span style={{ color: "#e74c3c" }}>●</span> Upper threshold: {upperThreshold} (
{upperCrossings.length} crossings)
</div>
<div>
<span style={{ color: "#f39c12" }}>●</span> Lower threshold: {lowerThreshold} (
{lowerCrossings.length} crossings)
</div>
</div>
</div>
);
}
Key techniques:
- Combine horizontal
<line>elements for threshold lines - Use
VerticalMarkerto mark crossing points - Calculate crossings by filtering the data array
- Use
strokeDasharrayto distinguish threshold lines from data
Range Selection
Select and analyze specific regions of data:
import { useState } from "react";
import { LineChart, VerticalMarker } from "@vuer-ai/vuer-viz";
const x = Array.from({ length: 100 }, (_, i) => i);
const y = x.map(i => Math.sin(i / 10) * 30 + 50 + Math.random() * 10);
const series = [
{
label: "Data",
x,
y,
color: "#3498db",
},
];
export default function RangeSelectionExample() {
const [rangeStart, setRangeStart] = useState<number | null>(20);
const [rangeEnd, setRangeEnd] = useState<number | null>(60);
const [clickMode, setClickMode] = useState<"start" | "end">("start");
const handleChange = (e: any) => {
if (e.type === "click" && e.x !== null) {
if (clickMode === "start") {
setRangeStart(e.x);
setClickMode("end");
} else {
setRangeEnd(e.x);
setClickMode("start");
}
}
};
// Calculate statistics for the selected range
const stats =
rangeStart !== null && rangeEnd !== null
? (() => {
const start = Math.min(rangeStart, rangeEnd);
const end = Math.max(rangeStart, rangeEnd);
// Filter y values where corresponding x is in range
const values = y.filter((_, i) => x[i] >= start && x[i] <= end);
return {
count: values.length,
mean: values.reduce((a, b) => a + b, 0) / values.length,
min: Math.min(...values),
max: Math.max(...values),
};
})()
: null;
const start = rangeStart && rangeEnd ? Math.min(rangeStart, rangeEnd) : null;
const end = rangeStart && rangeEnd ? Math.max(rangeStart, rangeEnd) : null;
return (
<div>
<div style={{ marginBottom: "10px" }}>
<button
onClick={() => setClickMode("start")}
style={{
padding: "8px 16px",
marginRight: "8px",
backgroundColor: clickMode === "start" ? "#3498db" : "#ecf0f1",
color: clickMode === "start" ? "white" : "#2c3e50",
border: "none",
borderRadius: "4px",
cursor: "pointer",
}}
>
Set Start
</button>
<button
onClick={() => setClickMode("end")}
style={{
padding: "8px 16px",
backgroundColor: clickMode === "end" ? "#e74c3c" : "#ecf0f1",
color: clickMode === "end" ? "white" : "#2c3e50",
border: "none",
borderRadius: "4px",
cursor: "pointer",
}}
>
Set End
</button>
</div>
<LineChart
series={series}
width={700}
height={300}
xLabel="Sample"
yLabel="Value"
title="Range Selection"
onChange={handleChange}
>
{/* Shaded range */}
{start !== null && end !== null && (
<rect
x={60 + (start / 100) * 640}
y={40}
width={((end - start) / 100) * 640}
height={260}
fill="#3498db"
opacity={0.1}
pointerEvents="none"
/>
)}
{/* Start marker */}
{rangeStart !== null && (
<VerticalMarker x={rangeStart} color="#3498db" lineWidth={2} dashArray="" />
)}
{/* End marker */}
{rangeEnd !== null && (
<VerticalMarker x={rangeEnd} color="#e74c3c" lineWidth={2} dashArray="" />
)}
</LineChart>
{stats && (
<div
style={{
marginTop: "10px",
padding: "12px",
backgroundColor: "#ecf0f1",
borderRadius: "4px",
fontSize: "14px",
}}
>
<div style={{ fontWeight: "bold", marginBottom: "8px" }}>
Range Statistics (x = {start?.toFixed(1)} to {end?.toFixed(1)})
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "8px" }}>
<div>Count: {stats.count}</div>
<div>Mean: {stats.mean.toFixed(2)}</div>
<div>Min: {stats.min.toFixed(2)}</div>
<div>Max: {stats.max.toFixed(2)}</div>
</div>
</div>
)}
</div>
);
}
Key techniques:
- Use two
VerticalMarkercomponents for range boundaries - Render a semi-transparent
<rect>to shade the selected region - Handle
clickevents to set range start/end - Calculate statistics for the selected range
Interactive pattern:
const handleChange = (e: ChartChangeEvent) => { if (e.type === "click") { if (selectingStart) { setRangeStart(e.x); setSelectingStart(false); } else { setRangeEnd(e.x); setSelectingStart(true); } } };
Event Annotations
Annotate time-series data with event markers and labels:
import { useState } from "react";
import { LineChart, VerticalMarker } from "@vuer-ai/vuer-viz";
interface Event {
x: number;
label: string;
type: "warning" | "error" | "info";
}
const x = Array.from({ length: 120 }, (_, i) => i);
const y = x.map(i => 30 + Math.sin(i / 15) * 20 + Math.random() * 10);
const series = [
{
label: "System Load",
x,
y,
color: "#3498db",
},
];
export default function EventAnnotationsExample() {
const [hoverX, setHoverX] = useState<number | null>(null);
const [selectedEvent, setSelectedEvent] = useState<Event | null>(null);
// Define events/annotations
const events: Event[] = [
{ x: 25, label: "Deploy v2.1", type: "info" },
{ x: 45, label: "High Memory", type: "warning" },
{ x: 72, label: "Service Restart", type: "error" },
{ x: 95, label: "Scale Up", type: "info" },
];
const getEventColor = (type: Event["type"]) => {
switch (type) {
case "error":
return "#e74c3c";
case "warning":
return "#f39c12";
case "info":
return "#2ecc71";
}
};
const handleChange = (e: any) => {
if (e.type === "hover") {
setHoverX(e.x);
// Check if hovering near an event
const nearEvent = events.find((evt) => Math.abs(evt.x - e.x) < 3);
setSelectedEvent(nearEvent || null);
}
};
return (
<div>
<LineChart
series={series}
width={700}
height={300}
xLabel="Time (minutes)"
yLabel="Load (%)"
title="System Load with Event Annotations"
onChange={handleChange}
>
{/* Event markers */}
{events.map((event) => (
<VerticalMarker
key={event.x}
x={event.x}
color={getEventColor(event.type)}
lineWidth={selectedEvent?.x === event.x ? 3 : 2}
dashArray=""
/>
))}
{/* Event labels */}
{events.map((event) => {
const svgX = 60 + (event.x / 120) * 640;
return (
<g key={`label-${event.x}`}>
{/* Label background */}
<rect
x={svgX - 40}
y={15}
width={80}
height={20}
fill={getEventColor(event.type)}
opacity={0.9}
rx={3}
/>
{/* Label text */}
<text
x={svgX}
y={28}
textAnchor="middle"
fontSize={11}
fill="white"
fontWeight="500"
>
{event.label}
</text>
</g>
);
})}
{/* Hover marker */}
{hoverX !== null && !selectedEvent && (
<VerticalMarker x={hoverX} color="#95a5a6" lineWidth={1} dashArray="4,4" />
)}
</LineChart>
{selectedEvent && (
<div
style={{
marginTop: "10px",
padding: "12px",
backgroundColor: "#ecf0f1",
borderRadius: "4px",
fontSize: "14px",
borderLeft: `4px solid ${getEventColor(selectedEvent.type)}`,
}}
>
<div style={{ fontWeight: "bold", marginBottom: "4px" }}>
{selectedEvent.label}
</div>
<div style={{ color: "#7f8c8d" }}>
Time: {selectedEvent.x} min • Type: {selectedEvent.type}
</div>
</div>
)}
</div>
);
}
Key techniques:
- Array of event objects with position, label, and type
- Render labels using SVG
<text>and<rect>for backgrounds - Use different colors for different event types
- Detect hover near events with distance calculation:
Math.abs(evt.x - e.x) < threshold
Custom Overlays with useChartContext
Create custom overlay components by accessing the chart's coordinate system:
import { useChartContext } from "vuer-viz"; function HorizontalLine({ y, label, color = "#e74c3c" }) { const { toSVGY, margin, plotWidth } = useChartContext(); const svgY = toSVGY(y); return ( <g> {/* Horizontal line */} <line x1={margin.left} y1={svgY} x2={margin.left + plotWidth} y2={svgY} stroke={color} strokeWidth={2} strokeDasharray="8,4" /> {/* Label */} <text x={margin.left + plotWidth + 5} y={svgY + 4} fontSize={12} fill={color} > {label} </text> </g> ); } // Usage <LineChart series={data}> <HorizontalLine y={75} label="Upper Limit" color="#e74c3c" /> <HorizontalLine y={25} label="Lower Limit" color="#f39c12" /> </LineChart>
Shaded Regions
Highlight specific regions with semi-transparent rectangles:
function ShadedRegion({ xStart, xEnd, color = "#3498db", opacity = 0.2 }) { const { toSVGX, margin, plotHeight } = useChartContext(); return ( <rect x={toSVGX(xStart)} y={margin.top} width={toSVGX(xEnd) - toSVGX(xStart)} height={plotHeight} fill={color} opacity={opacity} pointerEvents="none" /> ); } // Usage <LineChart series={data}> <ShadedRegion xStart={20} xEnd={40} color="#2ecc71" label="Good" /> <ShadedRegion xStart={60} xEnd={80} color="#e74c3c" label="Critical" /> </LineChart>
Data Point Markers
Highlight specific data points with circles:
function DataPointMarker({ x, y, color = "#e74c3c", radius = 5 }) { const { toSVGX, toSVGY } = useChartContext(); return ( <circle cx={toSVGX(x)} cy={toSVGY(y)} r={radius} fill={color} stroke="white" strokeWidth={2} /> ); } // Usage - mark peak values const peaks = data.filter(d => d.y > threshold); <LineChart series={series}> {peaks.map((point, i) => ( <DataPointMarker key={i} x={point.x} y={point.y} color="#e74c3c" /> ))} </LineChart>
Text Annotations
Add text labels at specific positions:
function TextAnnotation({ x, y, text, color = "#2c3e50" }) { const { toSVGX, toSVGY } = useChartContext(); return ( <text x={toSVGX(x)} y={toSVGY(y) - 10} textAnchor="middle" fontSize={12} fill={color} fontWeight="600" > {text} </text> ); } // Usage <LineChart series={data}> <TextAnnotation x={50} y={75} text="Peak" color="#e74c3c" /> <DataPointMarker x={50} y={75} color="#e74c3c" /> </LineChart>
Arrow Indicators
Point to specific features with arrows:
function Arrow({ x, y, direction = "down", color = "#2c3e50" }) { const { toSVGX, toSVGY } = useChartContext(); const svgX = toSVGX(x); const svgY = toSVGY(y); const offset = direction === "down" ? -20 : 20; return ( <g> <line x1={svgX} y1={svgY + offset} x2={svgX} y2={svgY + (direction === "down" ? -5 : 5)} stroke={color} strokeWidth={2} markerEnd="url(#arrowhead)" /> <defs> <marker id="arrowhead" markerWidth="10" markerHeight="10" refX="5" refY="5" orient="auto" > <polygon points="0 0, 10 5, 0 10" fill={color} /> </marker> </defs> </g> ); }
Combining Multiple Overlays
Build complex visualizations by layering multiple overlay types:
<LineChart series={series} onChange={handleChange}> {/* Background shaded regions */} <ShadedRegion xStart={0} xEnd={30} color="#2ecc71" opacity={0.1} /> <ShadedRegion xStart={70} xEnd={100} color="#e74c3c" opacity={0.1} /> {/* Threshold lines */} <HorizontalLine y={75} label="Max" color="#e74c3c" /> <HorizontalLine y={25} label="Min" color="#f39c12" /> {/* Event markers */} {events.map((evt) => ( <VerticalMarker key={evt.x} x={evt.x} color={evt.color} /> ))} {/* Peak markers */} {peaks.map((p) => ( <DataPointMarker key={p.x} x={p.x} y={p.y} color="#9b59b6" /> ))} {/* Interactive cursor */} <VerticalMarker x={cursorX} color="#34495e" /> <Tooltip x={cursorX} /> </LineChart>
Best Practices
1. Layer Order
SVG renders in document order, so place elements in the correct sequence:
<LineChart series={series}> {/* Background elements first */} <ShadedRegion {...} /> {/* Mid-layer elements */} <HorizontalLine {...} /> <VerticalMarker {...} /> {/* Foreground elements last */} <DataPointMarker {...} /> <TextAnnotation {...} /> <Tooltip {...} /> </LineChart>
2. Pointer Events
Disable pointer events on decorative overlays to keep chart interactions working:
<rect {...} pointerEvents="none" // Important! />
3. Conditional Rendering
Only render overlays when data is available:
{selectedRange && ( <ShadedRegion xStart={selectedRange.start} xEnd={selectedRange.end} /> )} {peaks.length > 0 && peaks.map(...)}
4. Performance
For many markers, use memoization:
const markers = useMemo( () => events.map((evt) => ( <VerticalMarker key={evt.x} x={evt.x} color={evt.color} /> )), [events] ); <LineChart series={series}> {markers} </LineChart>
5. Accessibility
Add ARIA labels for screen readers:
<g role="img" aria-label={`Event marker at ${event.label}`}> <VerticalMarker {...} /> <text>{event.label}</text> </g>
useChartContext API
All overlay components can access the chart's coordinate system:
interface ChartContextValue { bounds: { minX: number; maxX: number; minY: number; maxY: number; }; toSVGX: (dataX: number) => number; // Data → SVG pixels toSVGY: (dataY: number) => number; toDataX: (svgX: number) => number; // SVG pixels → Data toDataY: (svgY: number) => number; plotWidth: number; // Plot area dimensions plotHeight: number; margin: { top: number; right: number; bottom: number; left: number; }; } // Usage const { toSVGX, toSVGY, margin, plotWidth, plotHeight } = useChartContext();
Pattern Library
Detection Bands
function DetectionBand({ yMin, yMax, color, label }) { const { toSVGY, margin, plotWidth } = useChartContext(); return ( <g> <rect x={margin.left} y={toSVGY(yMax)} width={plotWidth} height={toSVGY(yMin) - toSVGY(yMax)} fill={color} opacity={0.2} /> <text x={margin.left + 10} y={toSVGY(yMax) + 15} fontSize={11}> {label} </text> </g> ); }
Confidence Interval
function ConfidenceInterval({ data, stdDev = 1 }) { const { toSVGX, toSVGY } = useChartContext(); const upperPath = data.map((d, i) => `${i === 0 ? 'M' : 'L'} ${toSVGX(d.x)} ${toSVGY(d.y + stdDev)}` ).join(' '); const lowerPath = data.map((d, i) => `${i === 0 ? 'M' : 'L'} ${toSVGX(d.x)} ${toSVGY(d.y - stdDev)}` ).join(' '); return ( <path d={`${upperPath} ${lowerPath} Z`} fill="#3498db" opacity={0.2} /> ); }
Timeline Events
function TimelineEvent({ x, label, icon }) { const { toSVGX, margin, plotHeight } = useChartContext(); const svgX = toSVGX(x); return ( <g> <line x1={svgX} y1={margin.top} x2={svgX} y2={margin.top + plotHeight} stroke="#95a5a6" strokeWidth={1} strokeDasharray="2,2" /> <circle cx={svgX} cy={margin.top - 10} r={8} fill="#3498db" /> <text x={svgX} y={margin.top - 6} textAnchor="middle" fontSize={10} fill="white" > {icon} </text> <text x={svgX} y={margin.top - 20} textAnchor="middle" fontSize={10} fill="#2c3e50" > {label} </text> </g> ); }
Related Guides
- Event Handling - Learn about the
onChangeevent and interactive features - Canvas Rendering - Performance tips for large datasets with overlays