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.

A live, editable Svelte 5 data grid example from the SvGrid gallery (Charts). See the SvGrid documentation for the full API.

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

Related articles

Source code (151-time-series-chart.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

More Charts examples

  • Chart a selection (context menu) - With integrated charting enabled, the right-click menu gains a Chart selected range item: select a block of cells, right-click, and the chart panel opens scoped to that range - the Excel chart-this gesture, built in. The item appears only when charting is on and is appended to the default context menu automatically.
  • Chart view of the grid - The `chart` prop turns the same <SvGrid> into a chart, driven by the grid’s filtered + sorted rows (search + sort flow through). A view of the grid like board and scheduler, but the renderer is free: the grid lazy-loads a built-in view wrapping the standalone SvChart via rowsToChartSpec. Flip Table <-> Chart (bar / line / area) over one source of truth.
  • Candlestick / OHLC - Candlestick and OHLC price marks with an ordinal date axis. ChartSeries.ohlc carries the four prices while values keeps the closes, so the CSV export, the screen-reader table and a 10-day moving average all work with no candle-specific code. Toggle the axis: a real time axis opens a gap over every weekend, an ordinal one spaces sessions evenly and still labels them by date. The strip under the plot is the chart brush: drag its window to pan, drag an edge to resize.
  • Box plot + error bars - Distribution rather than average: box plots with the 1.5 IQR whisker rule and individual outliers, next to the same data as a bar chart of the means with error bars. two regions with nearly identical means sit side by side in the bars and look nothing alike in the boxes. boxStats() summarises a raw sample, rowsToBoxSpec() does it per group, and ChartSeries.errors annotates any existing mark without changing its type.
  • Axes, titles and styling - The chart hands the decisions back: a numeric x axis that spreads payload sizes the way the numbers do, pinned domains with a fixed tick interval and a formatter per axis, grid lines on or off, a shaded budget band, a vertical reference line, per-point markers and colours, a stepped dashed series on the right axis, a title / subtitle / caption, a legend on any side, and a tooltip snippet in single-series mode. Below it, 50,000 monitoring readings decimated to one point per pixel with LTTB or min / max - toggle it off to see the cost.