Charts, sparklines and pictures

Four things a sheet draws beyond its cells: a chart over a range, which floats above the sheet and redraws when a number changes; a sparkline, which is a chart inside one cell; a picture from a file; and a cell that is a picture because its formula says so. Each is inserted from the ribbon, kept in the document, and carried in the .xlsx. Getting started shows the document.

The examples share a year of traffic by channel.

<script lang="ts">
  import { SvSheet, createWorkbook, createSheetDocument, objectId, sparklineId, type SheetObject, type SparklineGroup } from '@svgrid/enterprise'

  const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']
  const traffic: Record<string, number[]> = {
    Search: [4200, 4450, 4310, 4800, 5200, 5600],
    Social: [1800, 2100, 1950, 2400, 2200, 2600],
    Direct: [2400, 2350, 2500, 2450, 2600, 2550],
  }
  const cells: string[][] = [
    ['Channel', ...months, 'Trend'],
    ...Object.entries(traffic).map(([name, values]) => [name, ...values.map(String), '']),
    ['Total', ...months.map((_, i) => `=SUM(${String.fromCharCode(66 + i)}2:${String.fromCharCode(66 + i)}4)`), ''],
  ]

  const at = { rowIdAt: (i: number) => `r${i}`, columnIdAt: (i: number) => String.fromCharCode(65 + i) }

  function trafficDoc() {
    const d = createSheetDocument({ workbook: createWorkbook([{ name: 'Traffic', cells }]) })
    const s = d.get('Traffic')
    s.formats.set([[0, 0, 0, 7]], { bold: true, fill: '#e2e8f0', color: '#0f172a' }, at)
    s.formats.set([[4, 0, 4, 7]], { bold: true, border: { top: { width: 1 } } }, at)
    s.formats.set([[1, 1, 4, 6]], { numFmt: '#,##0' }, at)
    s.widths.A = 90
    for (const c of 'BCDEFG') s.widths[c] = 64
    s.widths.H = 110
    s.freeze = { rows: 1, cols: 1 }
    return d
  }
</script>

A chart over a range

Insert > Chart charts the selected block, or the region around a single cell, as Excel starts one: the first row and column are read as labels when they look like labels, each column is a series, and the chart is anchored just under the block. It reads the range, not a copy of the numbers, so editing a cell redraws it.

A chart is an entry in a sheet's objects: its range, its type (bar, line, area, pie or scatter), whether the first row and column are headers, whether the series run down the columns or along the rows, and an anchor: the cell it hangs from, an offset inside it, and a size.

<script lang="ts">
  const doc = trafficDoc()
  const charts: SheetObject[] = [
    {
      id: objectId(),
      kind: 'chart',
      anchor: { row: 6, col: 1, dx: 8, dy: 8, width: 380, height: 220 },
      range: [0, 0, 3, 6],
      type: 'line',
      headers: true,
      series: 'rows',
      title: 'Visits by channel',
    },
    {
      id: objectId(),
      kind: 'chart',
      anchor: { row: 6, col: 7, dx: 8, dy: 8, width: 300, height: 220 },
      range: [0, 0, 3, 6],
      type: 'bar',
      headers: true,
      series: 'columns',
      stacked: true,
      title: 'Months, stacked',
    },
  ]
  doc.get('Traffic').objects = charts
</script>

<SvSheet document={doc} height={520} rows={20} columns={14} />

Change a number in the block: both charts redraw. Drag a chart to move it, drag its corner to resize it, press Delete to remove it, and double-click it (or Insert > Setup) for the Chart dialog, where a trend (linear, or sma3 for a three-point moving average) goes over every series and one series can be moved to a secondary axis on the right, which is what makes a revenue-and-margin chart readable when the two are orders of magnitude apart. Each change is one undo.

The anchor is Excel's: inserting a row above the chart moves it, deleting that row takes it with it, and widening a column under it moves it without reshaping it. The drawing itself is the free <SvChart> from @svgrid/grid, so the palette, the tooltips and the types are the grid's.

Open the live example: Charts and pictures on the sheet (Spreadsheet)

A sparkline in a cell

A sparkline is a chart inside one cell, one per row of a block, drawn from the range so editing a number redraws it. Insert > Sparklines offers Line, Column and Win/Loss, each opening the Create Sparklines dialog on the selected block with the data range filled in and the location the column just past it.

They are kept per group, as Excel keeps them: a data range, a location range of the same shape (a row per row, or a column per column when the location is a row), the type, and the settings they share - color, negativeColor for the bars below zero, markers for a dot on a line's last point, sameScale to draw the whole group on one value axis.

<script lang="ts">
  const doc = trafficDoc()
  const groups: SparklineGroup[] = [
    { id: sparklineId(), data: [1, 1, 3, 6], location: [1, 7, 3, 7], type: 'line', markers: true, color: '#2563eb' },
    { id: sparklineId(), data: [4, 1, 4, 6], location: [4, 7, 4, 7], type: 'column', color: '#16a34a' },
  ]
  doc.get('Traffic').sparklines = groups
  doc.get('Traffic').heights.set(1, 30)
  doc.get('Traffic').heights.set(2, 30)
  doc.get('Traffic').heights.set(3, 30)
  doc.get('Traffic').heights.set(4, 30)
</script>

<SvSheet document={doc} rows={8} columns={9} />

The cell stays a cell: type a label over a sparkline and the label reads on top of it, click and the click reaches the cell, so it selects, drags and edits as any other. With a sparkline's cell selected, the three kind buttons change that group's kind; Insert > Sparklines > Edit reopens the dialog on it, and Clear removes the groups under the selection. An insert or a delete carries a group along, and deleting the cells it is drawn in, or the cells it reads, removes it.

Open the live example: Sparklines: a chart inside the cell (Spreadsheet)

A picture on the sheet

Insert > Picture puts an image from a file on the sheet, carried in the document as a data: URL, anchored and moved like a chart. From code it is an object of kind: 'image' with a src and an alt:

<script lang="ts">
  const doc = trafficDoc()
  // A small inline SVG stands in for a logo file.
  const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="160" height="60"><rect width="160" height="60" rx="8" fill="#f97316"/><text x="80" y="38" font-family="sans-serif" font-size="22" font-weight="700" fill="#fff" text-anchor="middle">svgrid</text></svg>`
  const logo: SheetObject = {
    id: objectId(),
    kind: 'image',
    anchor: { row: 6, col: 1, dx: 8, dy: 8, width: 160, height: 60 },
    src: `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`,
    alt: 'The company logo',
  }
  doc.get('Traffic').objects = [logo]
</script>

<SvSheet document={doc} rows={12} columns={9} />

A file the .xlsx cannot hold as it is (an SVG, say: Excel keeps only a few raster types in xl/media) is drawn to a PNG on the way in, so what is on the sheet is what the file will carry. A picture whose src is a web address is left out of the file, and Save As says so in the status bar: a file with a broken image in it is worse than one without.

A cell that is a picture

=IMAGE(source, [alt]) puts the picture in the cell itself instead of floating one above the sheet, and that changes what it can do: a thumbnail column sorts and filters with its rows, copies down as a formula does, and follows its cell through an insert or a delete with nothing to keep aligned. Only a web address or a data: URL is drawn; any other text stays text, so a bad source is a word in a cell rather than a broken image. The second argument is the alt text, evaluated like any argument, which is what a screen reader reads out; and because the function's value is the source, a formula that references the cell gets the address, not a picture it could not use.

<script lang="ts">
  const swatch = (fill: string) => `data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="48" height="24"><rect width="48" height="24" rx="4" fill="${fill}"/></svg>`)}`
  const wb = createWorkbook([{ name: 'Catalogue', cells: [
    ['SKU', 'Colour', 'Source', 'Swatch'],
    ['CH-01', 'Ember', swatch('#f97316'), '=IMAGE(C2, B2)'],
    ['CH-02', 'Slate', swatch('#475569'), '=IMAGE(C3, B3)'],
    ['CH-03', 'Moss', swatch('#16a34a'), '=IMAGE(C4, B4)'],
  ] }])
  const doc = createSheetDocument({ workbook: wb })
  doc.get('Catalogue').formats.set([[0, 0, 0, 3]], { bold: true, fill: '#e2e8f0', color: '#0f172a' }, at)
  doc.get('Catalogue').widths.C = 60
  doc.get('Catalogue').heights.set(1, 30)
  doc.get('Catalogue').heights.set(2, 30)
  doc.get('Catalogue').heights.set(3, 30)
</script>

<SvSheet document={doc} rows={8} columns={5} />

Sort the block by Colour: the swatches go with their rows. The file stores the cell as _xlfn.IMAGE, where Excel keeps it.

Open the live example: IMAGE: a picture inside the cell (Spreadsheet)

In the document and the file

Charts and pictures are per sheet, ride in getState() as objects and report { kind: 'objects' } on onChange; sparklines as sparklines with { kind: 'sparklines' }. Both print with the sheet, hung from their anchor cells, and the default print area grows to hold a chart anchored below the numbers. In the .xlsx a chart becomes a chart part of its own that carries the references its series read, so Excel redraws it from the cells rather than from a snapshot; a picture's bytes go into xl/media; a sparkline group goes into the worksheet's extension list where Excel keeps them. All of them come back from a file the same way. Charts, pictures and sparklines travel in the .xlsx only; an IMAGE cell is a formula and goes wherever formulas go.

The ribbon raises insert-chart, insert-picture, chart-setup and delete-object, and sparkline-line, sparkline-column, sparkline-winloss, sparkline-setup and clear-sparklines, so an application can put a chart builder of its own in their place.

See also

Live examples

  • Charts and pictures on the sheet - Charts anchored over the cells the way Excel anchors one: each reads a range rather than a copy of the numbers, so typing into a cell redraws it. Insert > Chart charts the selected block and reads its first row and column as the labels, Insert > Picture puts an image on the sheet, and a double-click opens the Chart dialog for the type, the title, series in columns or rows and stacking. Drag an object to move it, its corner to resize, Delete to remove. It hangs from a cell, so inserting a row above moves it, and it rides in getState() and in the .xlsx as a real drawing part.
  • Sparklines: a chart inside the cell - Excel's smallest chart, and not an object: a sparkline IS the cell. One per row of a block of numbers, drawn from the range rather than a copy, so editing a number redraws it. Insert > Sparklines offers Line, Column and Win/Loss, with Edit for the group's ranges, kind and colours and Clear for the groups the selection touches; selecting a cell that holds one turns the kind buttons into a change to that group. They are kept per group the way Excel keeps them, ride in getState() and in the .xlsx where Excel keeps them, and move with an insert or a delete.
  • IMAGE: a picture inside the cell - Excel's IMAGE puts a picture IN a cell rather than floating one over it, so it sorts with its row, filters with it, copies as a formula and moves when the cells move. A product list whose thumbnail column reads the swatch beside it: sort by price and the pictures follow their rows. The source is a web address or a data URL, anything else stays text rather than becoming a broken image, and the second argument is the alt text a screen reader reads.

Related articles

  • Sparkline Cells in a Svelte Data Grid - 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.