Color-blind-safe pattern fills

patternFallback: true layers a texture (stripe / crosshatch / dots / diagonal) on every series so two series with similar hues still read as distinct in grayscale or for readers with color-vision deficiency. Works on bars and area stacks.

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

About this example

Colour-blind-safe pattern fills on Svelte 5 charts. Colour alone is a fragile encoding for readers with red-green or blue-yellow colour vision deficiency, so SvGrid layers SVG pattern fills, stripe, crosshatch, dots and diagonal, over the series colour. Opt in per series with pattern or set patternFallback true on the spec to cycle four patterns across every unflagged series, on bars and area stacks alike, so similar hues still read as distinct in grayscale.

Color alone is a fragile encoding for readers with red/green or blue/yellow color vision deficiency. SvGrid lets you layer SVG pattern fills (stripe, crosshatch, dots, diagonal) over the series color so two series with similar hues still read as distinct.

Two ways to opt in:

  • series[i].pattern: 'stripe' per series, OR
  • spec.patternFallback: true cycles four patterns across every unflagged series. One flag, instant accessibility upgrade.

Imports, features and API used

Imports: @svgrid/grid

Table features registered: rowSortingFeature, columnFilteringFeature

Columns: quarter (Quarter), region (Region), revenue (Revenue)

SvGridApi methods called: api.getDisplayedRows()

Frequently asked questions

How do I turn patterns on for a whole chart?

Set patternFallback: true on the spec. Every series without its own pattern gets one of the four textures in rotation, layered over its colour.

Can I pick a pattern per series?

Yes. series[i].pattern accepts stripe, crosshatch, dots or diagonal and wins over the fallback cycle.

Do patterns print?

Yes. They are SVG pattern fills, so they survive grayscale printing and the SVG export, which is the point.

Related documentation

Related articles

Source code (156-chart-patterns.svelte)

<!-- Documented in: docs/help/charts/axes-and-styling.md -->
<script lang="ts">
  /**
   * 156. Chart pattern fills (colorblind-safe)
   * ------------------------------------------
   * Color alone is a fragile encoding for readers with red/green or
   * blue/yellow color vision deficiency. SvGrid lets you layer SVG
   * pattern fills (`stripe`, `crosshatch`, `dots`, `diagonal`) over the
   * series color so two series with similar hues still read as distinct.
   *
   * Two ways to opt in:
   *  - `series[i].pattern: 'stripe'` per series, OR
   *  - `spec.patternFallback: true` cycles four patterns across every
   *    unflagged series. One flag, instant accessibility upgrade.
   */
  import {
    SvGrid,
    SvGridChart,
    rowsToChartSpec,
    tableFeatures,
    rowSortingFeature,
    columnFilteringFeature,
    type GridColumns,
    type SvGridApi,
    type ChartSpec,
  } from '@svgrid/grid'

  const features = tableFeatures({ rowSortingFeature, columnFilteringFeature })

  type Row = { id: number; quarter: string; region: string; revenue: number }
  const QUARTERS = ['Q1', 'Q2', 'Q3', 'Q4']
  const REGIONS = ['Americas', 'EMEA', 'APAC', 'LATAM']
  let seed = 0xfeedbeef
  const rnd = () => ((seed = (seed * 1103515245 + 12345) >>> 0) / 0xffffffff)
  let nid = 0
  const rows: Row[] = QUARTERS.flatMap((q, qi) =>
    REGIONS.map((region) => ({
      id: nid++,
      quarter: q,
      region,
      revenue: Math.round(80_000 + qi * 22_000 + rnd() * 80_000),
    })),
  )

  const columns: GridColumns<Row> = [
    { field: 'quarter', header: 'Quarter', width: 100 },
    { field: 'region', header: 'Region', width: 130 },
    { field: 'revenue', header: 'Revenue', width: 140, align: 'right',
      format: { type: 'currency', currency: 'USD', options: { maximumFractionDigits: 0 } } },
  ]

  let api = $state<SvGridApi<typeof features, Row> | null>(null)
  let displayed = $state<Row[]>(rows)
  let patternFallback = $state(true)
  let stacked = $state(false)
  function sync() { displayed = (api?.getDisplayedRows() as Row[]) ?? rows }

  const compact = (v: number) => (Math.abs(v) >= 1e6 ? (v / 1e6).toFixed(1) + 'M' : 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: 'bar',
      category: 'quarter',
      value: 'revenue',
      series: 'region',
      reduce: 'sum',
      stacked,
      width: 560,
      height: 320,
    })
    s.patternFallback = patternFallback
    s.yAxisTitle = 'Revenue (USD)'
    return s
  })
  /** 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);">
      Pattern fills - color-blind-safe series
    </p>
    <p class="mt-0.5 text-xs" style="color: var(--sg-muted);">
      Toggle the pattern fallback. Each series gets a distinct texture (stripe / crosshatch /
      dots / diagonal) layered over its color so it stays distinguishable in grayscale or for
      readers with deuteranopia / protanopia. Try it stacked - the textures preserve series
      identity even when the colors touch.
    </p>
    <div class="mt-2 flex flex-wrap items-center gap-2 text-xs">
      <label class="ic-chk"><input type="checkbox" bind:checked={patternFallback} /> Pattern fallback</label>
      <label class="ic-chk"><input type="checkbox" bind:checked={stacked} /> Stacked</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={28}
        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 600px; 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={compact} width={paneW} height={paneH} />
        {/if}
      </div>
    </div>
  </div>
</section>

<style>
  .ic-chk { display: inline-flex; align-items: center; gap: 4px; color: var(--sg-fg); font-size: 12px; }
  .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.