Area charts

Sessions by channel over a year as areas: stacked, in two independent stack groups on one chart, normalised to a share, or as a stream on a centred baseline with the wiggle and silhouette offsets, with gradient fills. Below it a range area: the daily temperature band between the low and the high with the mean as a line and the freezing point shaded.

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

About this example

Area charts from one spec: stacked piles the channels, stackOffset wiggle and silhouette turn the pile into a streamgraph on a centred baseline, stacked100 normalises each month to a share, stack groups two independent piles on one chart so their totals can be compared, gradient fades each fill towards the baseline, and a range area draws the band between lowValues and values with the mean as a line over it. Free, in @svgrid/grid.

Traffic by channel over a year, as areas:

  • stacked piles the channels; stackOffset: 'wiggle' and 'silhouette' turn the pile into a stream centred on its own baseline; stacked100 normalises each month to a share.
  • stack groups two piles on one chart: the three web channels stack on each other while the app installs stack on their own, so the two totals can be compared without adding them.
  • gradient fades each fill towards the baseline.
  • The second chart is a range area: the daily temperature band between the low (lowValues) and the high (values), with the mean as a line over it and the freezing point as a reference band.

Free, in @svgrid/grid.

Imports, features and API used

Imports: @svgrid/grid, ../shared/ChartDataGrid.svelte, ../shared/ViewSwitch.svelte

Frequently asked questions

How do I draw a streamgraph?

Set stacked: true and stackOffset: wiggle (or silhouette) on the spec. The pile is centred on its own baseline instead of sitting on zero; hide the value axis labels, since the offsets have no zero.

How do I draw a band between a low and a high?

Use type range-area with values as the high end and lowValues as the low end. Add a line series over it for the mean.

Can two stacks share one chart?

Yes. Give each series a stack name; series with the same name pile on each other, and each pile is drawn from the baseline on its own.

Related documentation

Related articles

Source code (440-chart-area.svelte)

<!-- Documented in: docs/help/charts/gallery.md -->
<script lang="ts">
  /**
   * 440. Area charts
   * ----------------
   * Traffic by channel over a year, as areas:
   *
   * - `stacked` piles the channels; `stackOffset: 'wiggle'` and `'silhouette'`
   *   turn the pile into a stream centred on its own baseline; `stacked100`
   *   normalises each month to a share.
   * - `stack` groups two piles on one chart: the three web channels stack on
   *   each other while the app installs stack on their own, so the two totals
   *   can be compared without adding them.
   * - `gradient` fades each fill towards the baseline.
   * - The second chart is a range area: the daily temperature band between
   *   the low (`lowValues`) and the high (`values`), with the mean as a line
   *   over it and the freezing point as a reference band.
   *
   * Free, in @svgrid/grid.
   */
  import { SvChart, type ChartSpec } from '@svgrid/grid'
  import ChartDataGrid from '../shared/ChartDataGrid.svelte'
  import ViewSwitch from '../shared/ViewSwitch.svelte'

  let seed = 11
  const rnd = () => ((seed = (seed * 1103515245 + 12345) % 2147483648) / 2147483648)
  const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
  const wave = (base: number, amp: number, phase: number, noise: number) =>
    MONTHS.map((_, i) => Math.round(base + amp * Math.sin((i / 12) * Math.PI * 2 + phase) + (rnd() - 0.5) * noise))
  const channels = [
    { label: 'Search', values: wave(420, 90, 0.4, 60), stack: 'web' },
    { label: 'Social', values: wave(260, 120, 2.1, 70), stack: 'web' },
    { label: 'Referral', values: wave(140, 40, 1.2, 30), stack: 'web' },
    { label: 'App installs', values: wave(300, 110, 3.6, 50), stack: 'app' },
  ]

  let mode = $state<'stacked' | 'groups' | 'wiggle' | 'silhouette' | 'percent'>('stacked')
  let gradient = $state(true)

  const traffic = $derived<ChartSpec>({
    type: 'area',
    categories: MONTHS,
    series: channels.map((c) => ({
      label: c.label,
      values: c.values,
      gradient,
      smooth: mode === 'wiggle' || mode === 'silhouette',
      stack: mode === 'groups' ? c.stack : undefined,
    })),
    stacked: mode !== 'groups',
    stacked100: mode === 'percent',
    stackOffset: mode === 'wiggle' ? 'wiggle' : mode === 'silhouette' ? 'silhouette' : 'zero',
    valueFormat: 'compact',
    title: 'Sessions by channel',
    subtitle:
      mode === 'groups' ? 'Web channels stacked, app installs on their own pile' :
      mode === 'percent' ? 'Share of each month' :
      mode === 'stacked' ? 'Thousands, stacked' : 'Thousands, as a stream',
    yAxis: { title: mode === 'percent' ? 'Share' : 'Sessions (k)', gridLines: mode === 'stacked' || mode === 'groups' || mode === 'percent', labels: mode !== 'wiggle' && mode !== 'silhouette' },
    height: 320,
  })

  const days: string[] = []
  for (let i = 0; i < 90; i += 1) days.push(new Date(Date.UTC(2026, 0, 1 + i)).toISOString().slice(0, 10))
  const mean = days.map((_, i) => 2 + 9 * Math.sin(((i - 20) / 90) * Math.PI) + (rnd() - 0.5) * 4)
  const band = $derived<ChartSpec>({
    type: 'range-area',
    xType: 'time',
    categories: days,
    series: [
      { label: 'Daily range', values: mean.map((m) => Math.round((m + 4 + rnd() * 3) * 10) / 10), lowValues: mean.map((m) => Math.round((m - 4 - rnd() * 3) * 10) / 10), color: '#0ea5e9', marker: 'none' },
      { label: 'Mean', type: 'line', values: mean.map((m) => Math.round(m * 10) / 10), color: '#0369a1', smooth: true, marker: 'none' },
    ],
    title: 'Temperature, first quarter',
    subtitle: 'Daily low to high with the mean',
    yAxis: { title: '°C', gridLines: true },
    referenceBands: [{ from: -10, to: 0, label: 'Freezing', color: '#60a5fa', opacity: 0.12 }],
    height: 240,
  })

  // Chart | Grid: the same spec as the chart or as the rows behind it.
  let view = $state<'chart' | 'grid'>('chart')
</script>

<section class="wrap">
  <header class="chrome">
    <ViewSwitch bind:value={view} options={[['chart', 'Chart'], ['grid', 'Grid']]} />
    <label class="ctl">
      Layout
      <select bind:value={mode}>
        <option value="stacked">Stacked</option>
        <option value="groups">Two stack groups</option>
        <option value="percent">100% stacked</option>
        <option value="wiggle">Stream (wiggle)</option>
        <option value="silhouette">Stream (silhouette)</option>
      </select>
    </label>
    <label class="chk"><input type="checkbox" bind:checked={gradient} /> Gradient fills</label>
    <span class="note">
      Stacked areas, two independent piles on one chart, a normalised share, streams on a centred
      baseline, gradient fills, and a low-to-high band with its mean.
    </span>
  </header>

  <div class="pane">
    {#if view === 'chart'}
      <SvChart spec={traffic} legend="bottom" autosize />
    {:else}
      <ChartDataGrid spec={traffic} />
    {/if}
  </div>
  <div class="pane">
    {#if view === 'chart'}
      <SvChart spec={band} legend="bottom" zoomable crosshairLabels autosize />
    {:else}
      <ChartDataGrid spec={band} />
    {/if}
  </div>
</section>

<style>
  .wrap {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-height: 0;
    gap: 10px;
    overflow: auto;
  }
  .chrome {
    display: flex;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
    flex: none;
  }
  .note {
    font-size: 12px;
    color: var(--sg-muted, #64748b);
  }
  .chk,
  .ctl {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    font-size: 12px;
    color: var(--sg-fg, #0f172a);
    white-space: nowrap;
  }
  .ctl select {
    font: inherit;
    border: 1px solid var(--sg-border, #e2e8f0);
    border-radius: 6px;
    background: var(--sg-bg, #fff);
    color: inherit;
    padding: 2px 6px;
  }
  .pane {
    flex: none;
    border: 1px solid var(--sg-border, #e2e8f0);
    border-radius: 10px;
    background: var(--sg-bg, #fff);
    padding: 8px 10px;
    min-width: 0;
  }
</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.