V

Vuer-Viz

vuer-vizv1.0.2

Publication-Quality Visualizations

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:

Threshold Detection with Markers00.20.40.60.8100.20.40.60.81SampleValue
Signal
Upper threshold: 75 (41 crossings)
Lower threshold: 25 (4 crossings)
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 VerticalMarker to mark crossing points
  • Calculate crossings by filtering the data array
  • Use strokeDasharray to distinguish threshold lines from data

Range Selection

Select and analyze specific regions of data:

Range Selection00.20.40.60.8100.20.40.60.81SampleValue
Data
Range Statistics (x = 20.0 to 60.0)
Count: 41
Mean: 45.07
Min: 21.41
Max: 85.45
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 VerticalMarker components for range boundaries
  • Render a semi-transparent <rect> to shade the selected region
  • Handle click events 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:

System Load with Event Annotations00.20.40.60.8100.20.40.60.81Deploy v2.1High MemoryService RestartScale UpTime (minutes)Load (%)
System Load
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>
  );
}