---
title: "SparkChart"
description: "A smooth canvas trend chart with optional axes, active crosshair and accessible hover values."
category: Visualization
status: beta
since: 0.3.9
tags: [ai, chart, canvas, data]
syncStatus: reviewed
verified: true
---

## Usage

### SparkChart + ChartScrubber

::::TuffDemoWrapper{demo="SparkChartSparkChartDemo" code-lang="vue" title="A scrubbable trend snapshot" description="Smooth paired trends, axes and a value readout under the pointer."}
---
code: |
  <script setup lang="ts">
  import { computed, ref } from 'vue'

  const data = [0.22, 0.31, 0.45, 0.58, 0.51, 0.74, 0.91]
    .map((value, index) => ({ time: index * 5, value }))
  const activeIndex = ref<number | null>(null)
  const series = [{ id: 'trend', data }]
  const rows = computed(() => activeIndex.value === null
    ? []
    : [{ label: 'Trend', value: `${data[activeIndex.value]?.value ?? 0}` }])
  </script>

  <template>
    <TxChartScrubber
      :point-count="data.length"
      :active-index="activeIndex"
      :rows="rows"
      @update:active-index="activeIndex = $event"
    >
      <TxSparkChart
        :series="series"
        :active-index="activeIndex"
        x-axis
        y-axis
        grid
        aria-label="Trend snapshot"
      />
    </TxChartScrubber>
  </template>
---
::::

### Best Practices

- Give the stage a definite height, otherwise a zero-height container cannot paint.
- Use `aria-label` and `SparkSeries.label`; hover values then remain available outside the visual tooltip.
- Use the monotone default with evenly spaced, reasonably dense samples. Sparse samples are still sparse data; smoothing should not invent them.
- Bind one `activeIndex` to `TxChartScrubber` and `TxSparkChart` when both are present.
- Both map the pointer through the **plot box**, not the stage, so the crosshair lands on the sample it reports. The chart publishes its own gutters as `--tx-bui-plot-left` / `--tx-bui-plot-right` on its root; the scrubber reads them off the element it wraps, so there is nothing to keep in sync by hand.
- The tooltip is centred on the cursor and only pulled inward when it would overhang the stage. Keep it narrower than the stage or it stops travelling.

## API Reference

### SparkChart Props

| Name | Type | Default | Description |
|------|------|---------|-------------|
| `series` | `SparkSeries[]` | — | Series list, `{ id, data, color?, label? }`. |
| `theme` | `'light' \| 'dark' \| 'auto'` | `'auto'` | `auto` follows `data-theme` or `.dark`. |
| `grid` | `boolean` | `false` | Draws horizontal hairline gridlines. |
| `gridLines` | `number` | `4` | Number of gridlines. |
| `lineWidth` | `number` | `2.25` | Stroke width in CSS pixels. |
| `curve` | `LineCurve` | `'monotone'` | `linear`, `monotone`, `natural`, or `step`. |
| `xAxis` / `yAxis` | `boolean` | `false` | Draw compact x/y baselines and labels. |
| `xTicks` / `yTicks` | `number` | `3` / `4` | Number of x/y labels. |
| `xTickFormat` / `yTickFormat` | `(value: number) => string` | — | Override x/y label formatting. |
| `padding` | `Partial<SparkChartPadding>` | `{ top: 24, right: 0, bottom: 22, left: 0 }` | Inner inset; axes reserve their own minimum edges. |
| `domain` | `[number, number]` | — | Fixed value range; omit to fit the data. |
| `activeIndex` | `number \| null` | — | Controlled highlighted sample. |
| `interactive` | `boolean` | `true` | Enables pointer and keyboard hover state. |
| `baseline` | `boolean` | `true` | Dashed rule at each series' **own starting value**. Without a reference the eye can see that a line wobbles but not whether it ended up above or below where it began. Per series rather than one shared zero line, because two series on one spark chart rarely share a scale. |
| `endpoint` | `boolean` | `true` | Filled dot on each series' last sample. The line's end is the current value — the one number the reader is after — and a stroke alone gives it no more weight than any midpoint. |
| `animation` | `boolean` | `true` | ECharts-parity 1000ms enter reveal and 500ms updates. |
| `ariaLabel` | `string` | — | Accessible name for the canvas. |

`SparkPoint` is `{ time: number, value: number }`; `time` positions a sample and need not be an epoch. `SparkSeries.label` is used in an accessible hover announcement when supplied.

### SparkChart Events

| Event | Payload | Description |
|------|------|-------------|
| `update:activeIndex` | `(index: number \| null)` | Controlled hover write-back. |
| `hover` | `(index: number)` | A new active sample was reached. |
| `leave` | — | The pointer left or Escape cleared the active sample. |

### ChartScrubber Props

| Name | Type | Default | Description |
|------|------|---------|-------------|
| `pointCount` | `number` | — | How many samples the pointer maps onto. |
| `activeIndex` | `number \| null` | — | Controlled index. Omit to let the component own it. |
| `rows` | `ChartTooltipRow[]` | — | Tooltip rows, `{ label, value, color? }`. |
| `timeLabel` | `string` | — | Caption line above the rows. |
| `tooltip` | `boolean` | `true` | Turn off for a bare cursor line. |
| `anchorMargin` | `number` | `8` | Gap kept between the tooltip and the stage edges, in px. |
| `disabled` | `boolean` | `false` | Ignores the pointer. |

### ChartScrubber Events

| Event | Payload | Description |
|------|------|-------------|
| `scrub` | `(index: number)` | The pointer reached a new sample. |
| `leave` | — | Pointer left, lifted, or was cancelled. |
| `update:activeIndex` | `(index: number \| null)` | Controlled write-back. |

## Sizing and Theme

The chart fills its container and measures that **container** rather than taking a width or height prop. Give the wrapper a definite height. A `ResizeObserver` repaints it on resize; the bitmap is scaled by `devicePixelRatio` (capped at 2) while draw maths stays in CSS pixels.

`theme` defaults to `'auto'` and follows `data-theme` or `.dark` on `<html>` and `<body>`. Canvas cannot inherit CSS variables, so a theme change repaints with the current token colours.

## Curves, Axes, and Hover

The default `curve="monotone"` uses the same d3 curve factory as the full `@talex-touch/tuffex/charts` line series: it avoids the angular polyline without overshooting local extrema. Choose `linear`, `natural`, or `step` only when that encoding is intentional.

`xAxis` and `yAxis` draw compact baselines and ticks inside the canvas. Their padding is reserved automatically; use `xTickFormat` and `yTickFormat` when raw time or value numbers are not useful to readers.

`interactive` gives the standalone chart a pointer/keyboard crosshair: arrows, Home, End and Escape move or clear the active sample. Controlled `activeIndex` lets a surrounding `TxChartScrubber` own the tooltip while the canvas still draws the highlighted dots. The chart and scrubber both announce active values through a polite live region.

## Overview

- Every series shares one value domain; x is derived from `time`, or even index spacing when timestamps collapse. A single sample is centred and painted as a round-cap dot.
- The active crosshair and dots use the controlled index when present. An outer scrubber can therefore own the DOM tooltip without losing the canvas affordance.
- Empty data, a zero-sized container, or no 2D context paint nothing and throw nothing.
- Enter motion is 1000ms `cubicInOut`; data updates use 500ms `cubicInOut`; both skip above 2000 points and under `prefers-reduced-motion: reduce`.

## Technologies

- Component source: `packages/tuffex/packages/components/src/spark-chart/src/TxSparkChart.vue`, `TxChartScrubber.vue`.
- Projection and painting: `packages/tuffex/packages/components/src/spark-chart/src/geometry.ts`, `draw.ts`.
- Types: `packages/tuffex/packages/components/src/spark-chart/src/types.ts`.
- **Tested coverage:** `packages/tuffex/packages/components/src/spark-chart/__tests__/spark-chart.test.ts` (33 cases) covers domains, projection, monotone canvas strokes, x/y axes and edge-label anchoring, hover dots, keyboard state, scrubber control and accessibility.
- Adapted from Beautiful UI (https://www.beautifului.dev), © 2026 Shane Levine, MIT.

<TuffDocSourceLink />

- The chart is intentionally canvas-first: the draw-call surface is testable even where jsdom has no 2D context.
- The full chart family now lives at `@talex-touch/tuffex/charts`; SparkChart stays a compact card primitive at the root TuffEx entry.
- No area fill is offered: the user request is a legible trend readout, not an uncertain extrapolation between sparse points.
