# Time-series chart (date axis)

xType: time spaces points by ACTUAL time - irregular date gaps render proportionally - and shows real date ticks. A referenceLines target/SLA line spans the plot; toggle 100% stacked to read each day as a share of its total. Line, stacked area, or stacked bar.

Uses `@svgrid/grid` (MIT). Category: Charts. Live demo: https://svgrid.com/demos/151-time-series-chart/

Install: `npm i @svgrid/grid` (Svelte 5).

## About this example

A time-series chart in Svelte 5 with a real date axis. xType time makes SvGridChart treat categories as dates: x positions are spaced by actual elapsed time so irregular gaps render proportionally and the axis shows real date ticks. A referenceLines entry draws a target or SLA line across the plot, and a toggle switches 100 percent stacked to read each day's traffic as a share of its total, as line, stacked area or stacked bar.

Set `xType: 'time'` and `SvGridChart` treats the categories as dates: x positions are spaced by ACTUAL time (irregular gaps render proportionally, not evenly) and the axis shows real date ticks. A `referenceLines` entry draws a horizontal target/SLA line across the plot. Toggle 100% stacked to see each day's traffic split as a share of its total.

## Imports, features and API used

- Imports: `@svgrid/grid`
- Table features registered: `rowSortingFeature`, `columnFilteringFeature`
- Columns: `date` (Date), `channel` (Channel), `sessions` (Sessions)
- SvGridApi methods called: `api.getDisplayedRows()`

## Frequently asked questions

### What happens without xType time?

Categories are placed at equal steps regardless of their dates, so a gap of a week looks the same as a gap of a day. time fixes the spacing and labels the axis with dates.

### How do I add a target line?

Add { value, label } to referenceLines in the spec; the chart draws a dashed horizontal line at that value with the label at the edge.

### What does 100 percent stacked change?

Each category's series are normalised to sum to 100, so the chart shows the share of each channel per day rather than absolute sessions.

## Related documentation

- [Charts: getting started](https://svgrid.com/docs/help/charts/start.md)

## Related articles

- [Throttling Live Grid Updates to Animation Frames](https://svgrid.com/blog/throttle-live-updates-animation-frames/) - High-frequency WebSocket feeds will wreck grid performance if you apply every message immediately. One rAF buffer and a Map coalescer cuts thousands of renders down to 60 per second.
- [Sparkline Cells in a Svelte Data Grid](https://svgrid.com/blog/sparkline-cells/) - Show inline trend sparklines inside grid cells using SvGrid's built-in sparkline column property - no charting library needed, just a field that holds a number array.
- [Real-Time Grids - Live WebSocket Updates in SvGrid](https://svgrid.com/blog/realtime-websocket-updates/) - How to wire a high-frequency WebSocket feed into SvGrid without frame drops - covering stable row identity, RAF batching, and flash feedback done right.

## Source code (151-time-series-chart.svelte)

```svelte
<!-- Documented in: docs/help/charts/start.md -->
<script lang="ts">
  /**
   * 151. Time-series chart (date axis + target line)
   * ------------------------------------------------
   * Set `xType: 'time'` and `SvGridChart` treats the categories as dates: x
   * positions are spaced by ACTUAL time (irregular gaps render proportionally,
   * not evenly) and the axis shows real date ticks. A `referenceLines` entry
   * draws a horizontal target/SLA line across the plot. Toggle 100% stacked to
   * see each day's traffic split as a share of its total.
   */
  import {
    SvGrid,
    SvGridChart,
    rowsToChartSpec,
    tableFeatures,
    rowSortingFeature,
    columnFilteringFeature,
    type GridColumns,
    type SvGridApi,
    type ChartSpec,
    type ChartType,
  } from '@svgrid/grid'

  const features = tableFeatures({ rowSortingFeature, columnFilteringFeature })

  type Row = { id: number; date: string; channel: string; sessions: number }
  // Irregular dates (note the jump after the 5th) so the time axis earns its keep.
  const DATES = ['2024-01-01', '2024-01-02', '2024-01-03', '2024-01-04', '2024-01-05', '2024-01-12', '2024-01-19', '2024-01-26', '2024-02-02']
  const CHANNELS = ['Organic', 'Paid', 'Social']
  let seed = 0xbeef11
  const rnd = () => ((seed = (seed * 1103515245 + 12345) >>> 0) / 0xffffffff)
  const BASE: Record<string, number> = { Organic: 1200, Paid: 700, Social: 450 }
  let nid = 0
  const rows: Row[] = DATES.flatMap((date, di) =>
    CHANNELS.map((channel) => ({
      id: nid++,
      date,
      channel,
      sessions: Math.round(BASE[channel]! * (0.8 + di * 0.06 + rnd() * 0.5)),
    })),
  )

  const columns: GridColumns<Row> = [
    { field: 'date', header: 'Date', width: 130 },
    { field: 'channel', header: 'Channel', width: 130 },
    { field: 'sessions', header: 'Sessions', width: 130, align: 'right', format: { type: 'number', options: { maximumFractionDigits: 0 } } },
  ]

  let api = $state<SvGridApi<typeof features, Row> | null>(null)
  let displayed = $state<Row[]>(rows)
  let chartType = $state<ChartType>('line')
  let stacked100 = $state(false)
  let target = $state(2500)
  let showTarget = $state(true)

  function sync() {
    displayed = (api?.getDisplayedRows() as Row[]) ?? rows
  }

  const compactNum = (v: number) => (Math.abs(v) >= 1e3 ? (v / 1e3).toFixed(v % 1e3 ? 1 : 0) + 'k' : String(Math.round(v)))

  const spec = $derived.by<ChartSpec>(() => {
    const s = rowsToChartSpec(displayed, {
      type: chartType,
      category: 'date',
      value: 'sessions',
      series: 'channel',
      reduce: 'sum',
      stacked: chartType !== 'line',
      stacked100: stacked100 && chartType !== 'line',
      width: 540,
      height: 300,
    })
    s.xType = 'time'
    s.xAxisTitle = 'Date'
    s.yAxisTitle = stacked100 && chartType !== 'line' ? 'Share' : 'Sessions'
    if (showTarget && !(stacked100 && chartType !== 'line')) {
      s.referenceLines = [{ value: target, label: `Target ${compactNum(target)}` }]
    }
    return s
  })

  const fmtVal = (v: number) => (stacked100 && chartType !== 'line' ? `${Math.round(v)}` : compactNum(v))
  /** Pane size, so the chart fills its card rather than a fixed viewBox. */
  let paneW = $state(0)
  let paneH = $state(0)
</script>

<section class="flex flex-col flex-1 min-h-0 gap-3">
  <div class="shrink-0 rounded-lg border px-4 py-3" style="border-color: var(--sg-border); background: var(--sg-header-bg);">
    <p class="text-sm font-semibold" style="color: var(--sg-fg);">
      Time-series with a real date axis (irregular gaps) + a target line
    </p>
    <p class="mt-0.5 text-xs" style="color: var(--sg-muted);">
      The gap after Jan 5 is wider because the dates jump a week - positions track actual time, not row order.
    </p>
    <div class="mt-2 flex flex-wrap items-center gap-2 text-xs">
      <select bind:value={chartType} class="ic-sel">
        <option value="line">Line</option>
        <option value="area">Area (stacked)</option>
        <option value="bar">Bar (stacked)</option>
      </select>
      {#if chartType !== 'line'}
        <label class="ic-chk"><input type="checkbox" bind:checked={stacked100} /> 100% stacked</label>
      {/if}
      <label class="ic-chk"><input type="checkbox" bind:checked={showTarget} /> Target line</label>
      <label class="ic-chk">
        Target
        <input type="range" min="1000" max="4000" step="100" bind:value={target} />
        <span style="color: var(--sg-fg); font-variant-numeric: tabular-nums;">{compactNum(target)}</span>
      </label>
    </div>
  </div>

  <div class="flex flex-1 min-h-0 gap-3">
    <div class="flex-1 min-w-0 min-h-0">
      <SvGrid responsive={true}
      columnResize
        data={rows}
        columns={columns}
        features={features}
        sortable
        filterable
        selectionMode="none"
        rowHeight={32}
        containerHeight="100%"
        fitColumns={true}
        onApiReady={(a) => { api = a; sync() }}
        onFiltersChange={sync}
        onSortingChange={sync}
      />
    </div>
    <div class="rounded-lg border p-3" style="flex: 0 1 580px; min-width: 0; min-height: 0; border-color: var(--sg-border); background: var(--sg-bg);">
      <!-- Measured box, not the card: its height comes from the parent, so the
           chart cannot push the thing it is sized against. -->
      <div style="width: 100%; height: 100%; min-height: 0;" bind:clientWidth={paneW} bind:clientHeight={paneH}>
        {#if paneW > 40 && paneH > 40}
          <SvGridChart {spec} formatValue={fmtVal} width={paneW} height={paneH} />
        {/if}
      </div>
    </div>
  </div>
</section>

<style>
  .ic-sel {
    border: 1px solid var(--sg-input-border, var(--sg-border));
    background: var(--sg-input-bg, var(--sg-bg));
    color: var(--sg-fg);
    border-radius: 6px;
    padding: 4px 8px;
    font-size: 12px;
  }
  .ic-chk {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    color: var(--sg-fg);
  }
  .ic-chk input[type='checkbox'] { accent-color: var(--sg-accent); }
</style>
```

[View this example on GitHub](https://github.com/sv-grid/sv-grid/blob/main/examples/src/demos/151-time-series-chart.svelte)
