---
title: "TimeseriesChart"
description: "Time-series lines and stacked bars: clustered markers, threshold lines, range brushing, full tooltips."
category: Charts
status: beta
since: 0.1.0
tags: [chart, timeseries, line, bar]
syncStatus: reviewed
verified: true
---

## Usage

`data` is an array of series; each series' `data` holds time-ordered `[timestamp_ms, value]` tuples. `color` is optional — series fall onto the categorical palette by position, and positions are stable, so hiding one series never recolors the rest. Hovering opens the tooltip: rows sort by value descending, duplicate names dedupe, and values resolve by binary-searching the nearest sample.

### Two-series lines

:::TuffDemoWrapper{demo="TimeseriesChartBasicDemo" code-lang="vue" title="Lines + axis formatting" description="xAxisTickFormat / tooltipValueFormat customize tick and value text."}
---
code: |
  <script setup lang="ts">
  import { TxTimeseriesChart } from '@talex-touch/tuffex/charts'

  const data = [
    { name: 'Requests', data: requests },
    { name: 'Cache hits', data: cacheHits },
  ]
  </script>

  <template>
    <TxTimeseriesChart
      :data="data"
      x-axis-name="Time"
      y-axis-name="Count"
      :x-axis-tick-format="(ts) => new Date(ts).toLocaleTimeString()"
      :tooltip-value-format="(v) => `${v} req/s`"
    />
  </template>
---
:::

### Integer ticks + tooltip footer

`yAxisMinInterval` drops ticks closer together than the interval, which suits discrete counts; `tooltipFooter` appends one line of text below the tooltip rows.

:::TuffDemoWrapper{demo="TimeseriesChartAxisDemo" code-lang="vue" title="Integer ticks + tooltip footer" description="yAxisMinInterval 1 keeps integer ticks only; tooltipFooter adds a footer row."}
---
code: |
  <script setup lang="ts">
  const data = [
    { name: 'Retries', data: retries },
  ]
  </script>

  <template>
    <TxTimeseriesChart
      :data="data"
      :y-axis-min-interval="1"
      :y-axis-tick-count="5"
      tooltip-footer="Sampled hourly · retries only"
      :x-axis-tick-format="(ts) => new Date(ts).toLocaleTimeString()"
    />
  </template>
---
:::

## API Reference

### TimeseriesChart Props

| Prop | Type | Default | Description |
|------|------|---------|------|
| `data` | `TimeseriesData[]` | — | `{ name, data: [ts, value][], color? }`; missing `color` falls onto the categorical palette by position. |
| `type` | `'line' \| 'bar'` | `'line'` | Bars stack automatically. |
| `markers` | `TimeseriesMarker[]` | — | `{ timestamp, label?, description?, color?, lineStyle? }`. |
| `thresholds` | `TimeseriesThreshold[]` | — | `{ value, label?, color }`; the y domain extends to cover them. |
| `xAxisName` / `yAxisName` | `string` | — | Axis names. |
| `xAxisTickCount` / `yAxisTickCount` | `number` | `5` | Suggested tick counts. |
| `xAxisTickFormat` / `yAxisTickFormat` | `(v: number) => string` | — | Tick formatters. |
| `yAxisMinInterval` | `number` | — | Minimum interval between y-axis ticks (ECharts `yAxis.minInterval`): closer ticks drop, the lowest and highest always stay, and integer ticks stop printing as `1.0` when set to `1`. |
| `tooltipValueFormat` | `(v: number) => string` | raw value | Tooltip value formatter. |
| `tooltipMode` | `'all' \| 'single'` | `'all'` | `single` shows only the series closest to the cursor's y value. |
| `tooltipMaxItems` | `number` | `10` | Overflow folds into "+N more". |
| `tooltipFollowCursor` | `'both' \| 'x'` | `'both'` | `x` pins the vertical position to avoid jitter. |
| `tooltipFooter` | `string` | — | Extra line of text appended below the tooltip rows. |
| `tooltipBoundary` | `TimeseriesTooltipBoundary` | `'clipping-ancestors'` | Tooltip collision boundary; by default it walks clipping ancestors (offsetParents whose `overflow` is not `visible`) and intersects them with the viewport before flipping. The `TimeseriesTooltipBoundary` type is exported from the `@talex-touch/tuffex/charts` entry. |
| `incomplete` | `{ before?, after? }` | — | Incomplete periods (line type only). |
| `gradient` | `boolean` | `false` | Gradient fill under lines. |
| `loading` | `boolean` | `false` | Skeleton state. |
| `highlightedSeries` | `string \| null` | — | Emphasized series; the rest dim to 10% opacity. |
| `height` | `number` | `350` | Pixel height. |
| `width` | `number` | measured | Explicit width (SSR/tests). |
| `ariaDescription` | `string` | — | Accessible description (svg `role="img"`). |
| `clusterLabel` | `(n: number) => string` | `` n => `${n} changes` `` | Cluster label wording. |
| `timestampFormat` | `(ts: number) => string` | compact locale | Tooltip timestamp format. |

### TimeseriesChart Events

| Event | Payload | Description |
|------|------|------|
| `time-range-change` | `(from: number, to: number)` | Brush finished. Attaching it enables brushing. |
| `update:hiddenSeries` | `(names: string[])` | `v-model:hidden-series` write-back. |

## Markers & Thresholds

`markers` draw vertical dashed lines on the time axis; markers that crowd together cluster into one line based on the visible span, the label becomes "N changes" (`clusterLabel` overrides the wording), and hovering the line lists each marker's `label` and `description`. `thresholds` draw horizontal lines on the value axis, and the y domain extends automatically to cover them.

### Deploy markers + SLO line

:::TuffDemoWrapper{demo="TimeseriesChartMarkersDemo" code-lang="vue" title="Markers + Thresholds" description="The last two markers sit close together and cluster."}
---
code: |
  <script setup lang="ts">
  const markers = [
    { timestamp: t1, label: 'Deploy v2.4', description: 'Rollout to 50%.' },
    { timestamp: t2, label: 'Config change' },
    { timestamp: t2 + tiny, label: 'Alert fired' },
  ]
  const thresholds = [
    { value: 300, label: 'SLO 300ms', color: ChartPalette.semantic('Attention') },
  ]
  </script>

  <template>
    <TxTimeseriesChart :data="data" :markers="markers" :thresholds="thresholds" />
  </template>
---
:::

## Gradient & Incomplete Data

`gradient` adds a vertical fill under each line (series color at 40% → transparent). `incomplete` declares data outside `[before, after]` as incomplete: edge segments render dashed and overlap the solid segment by one sample so the line stays connected.

### Gradient + dashed edges

:::TuffDemoWrapper{demo="TimeseriesChartGradientDemo" code-lang="vue" title="Gradient + Incomplete" description="The first and last three hours are incomplete periods."}
---
code: |
  <template>
    <TxTimeseriesChart
      :data="data"
      gradient
      :incomplete="{ before: start + 3 * hour, after: start + 20 * hour }"
    />
  </template>
---
:::

## Stacked Bars

With `type="bar"` every series stacks automatically (kumo's `stack: 'total'`), and the y domain covers each timestamp's stacked total.

### Daily stacks

:::TuffDemoWrapper{demo="TimeseriesChartBarDemo" code-lang="vue" title="Stacked bars" description="Three series share one bar per day."}
---
code: |
  <template>
    <TxTimeseriesChart type="bar" :data="data" :x-axis-tick-format="formatDay" />
  </template>
---
:::

## Time Range Selection

Attaching a `time-range-change` listener enables horizontal brushing: drag on the plot to draw a selection; releasing emits `(from, to)` in milliseconds and clears the selection. Drags under 3px count as clicks and do nothing. The selection rectangle fills with `rgba(120, 140, 180, 0.3)` and strokes 1px `rgba(120, 140, 180, 0.8)`. While dragging, the plot outside the selection dims too: one luminance `<mask>` spanning the plot (a white plot rect plus two `#4D4D4D` strips) presses everything outside the selection to 30%, so each series stays mounted exactly once — equivalent to ECharts `outOfBrush.colorAlpha: 0.3`.

### Drag to select

:::TuffDemoWrapper{demo="TimeseriesChartRangeDemo" code-lang="vue" title="Brush selection" description="The readout below shows the selected range."}
---
code: |
  <template>
    <TxTimeseriesChart :data="data" @time-range-change="(from, to) => (range = [from, to])" />
  </template>
---
:::

## Legend Interplay

`v-model:hidden-series` two-way binds the hidden series names: hidden series neither render nor appear in tooltips. `highlighted-series` emphasizes one series and dims the rest to 10% opacity (ECharts blur's `fromState.opacity * 0.1`). Combine both with `TxChartLegendItem` for a clickable, hover-highlighting legend.

### Click to hide + hover to highlight

:::TuffDemoWrapper{demo="TimeseriesChartLegendDemo" code-lang="vue" title="Legend interplay" description="Click legend items to toggle; hover to highlight."}
---
code: |
  <template>
    <TxChartLegendItem
      v-for="(name, i) in names"
      :key="name"
      :name="name"
      :color="ChartPalette.categoricalVar(i)"
      value=""
      :inactive="hidden.includes(name)"
      @click="toggle(name)"
      @pointerenter="highlighted = name"
      @pointerleave="highlighted = null"
    />
    <TxTimeseriesChart v-model:hidden-series="hidden" :data="data" :highlighted-series="highlighted" />
  </template>
---
:::

## Loading Skeleton

`loading` swaps the chart for a harmonic-wave skeleton; the line and bar variants share one silhouette, and the shimmer respects `prefers-reduced-motion`.

### Skeleton loop

:::TuffDemoWrapper{demo="TimeseriesChartLoadingDemo" code-lang="vue" title="Loading skeleton" description="Loading toggles on a loop, alternating the line and bar variants."}
---
code: |
  <template>
    <TxTimeseriesChart :data="data" :type="type" :loading="loading" />
  </template>
---
:::

## Animation & Interaction

The chart reuses ECharts' default timings (kumo pins `echarts ^6.0.0`):

- First render: lines/areas reveal left to right through a clip, 1000ms `easings.linear`; bars grow out of the base axis (y lerps from `baseY` to the target, height 0 → target) and scatter symbols scale 0 → radius while fading in, all 1000ms cubicInOut.
- Data updates: geometry morphs over 500ms cubicInOut.
- State switches (emphasis / blur): 300ms cubicOut.
- `animationThreshold` 2000: a series with more points than that renders its final geometry with no animation.
- Tooltip: closing waits 100ms, then fades out over 200ms and removes the DOM, so brief pointer excursions do not flicker; the position travels via `transform: translate3d()` and lets the browser interpolate the 400ms move (`cubic-bezier(0.23, 1, 0.32, 1)`); pointer tracking is throttled to 50ms, taking one leading and one trailing sample.
- The plot outside the brush selection dims while dragging and restores on release — see the single `<mask>` above.
- Hovering the plot dims every series except the one nearest the cursor's value at that timestamp to 10% opacity; this is a documented approximation of ECharts `emphasis.focus: 'series'`, which only emphasizes over an actual item.
- Under `prefers-reduced-motion: reduce` every tween lands on its final frame immediately, and all CSS transitions sit inside `@media (prefers-reduced-motion: no-preference)`.

The `@talex-touch/tuffex/charts` entry also exports the animation primitives, so bespoke charts can reuse the same timings: `easings` (`linear | cubicIn | cubicOut | cubicInOut`), `cubicBezier(x1, y1, x2, y2)`, `tween({ duration, easing, delay, onUpdate, onComplete })`, `useEnterProgress(options)`, `useTweenedNumbers(source, options)`, `prefersReducedMotion()`, plus the constants `ENTER_DURATION` 1000, `UPDATE_DURATION` 500, `STATE_DURATION` 300 and `ANIMATION_THRESHOLD` 2000. The tooltip timing constants (`TOOLTIP_FADE_DURATION` 200, `TOOLTIP_MOVE_DURATION` 400, `TOOLTIP_HIDE_DELAY` 100, `TOOLTIP_TRACK_THROTTLE` 50, `tooltipMoveEasing`) live in the charts entry's `core/animate`.

## Differences from kumo

- `enableLegendSelection` + imperative echarts actions → declarative `v-model:hidden-series`; highlighting via `highlighted-series`.
- `tooltipBoundary` performs the real clipping-ancestors collision walk: it intersects clipping ancestors (offsetParents whose `overflow` is not `visible`) with the viewport, flips on overflow, and defaults to `'clipping-ancestors'`.
- The brush no longer just draws the rectangle during a drag: one luminance `<mask>` presses everything outside the selection to 30% while each series stays mounted exactly once.
- Geometry animation matches ECharts: the pie grows angularly via `animationType: 'expansion'` and highlights displace a slice by `scaleSize` 5px; sankey and maps have no geometry animation (`SankeyView` has no `initProps`/`updateProps`, maps set `animationDurationUpdate: 0`) — parity, not a gap.
- Series-level hover emphasis is an approximation: ECharts blurs the other series only over an actual item, while this picks the nearest series at the cursor's timestamp.
- Keep each series under ~5k points: SVG rendering degrades in the tens of thousands.
