Skip to content

Title and subtitle

title and subtitle are drawn around the plot. Both accept the same options: text, font, color, alignment, vertical placement and the padding around the text.

ts
import { getData } from './data';
import type { ChartOptions } from 'grafit-charts';

// Custom caption fonts, colors and padding.
export function createOptions(): ChartOptions {
  return {
    data: getData(),
    title: { text: 'Annual revenue', fontSize: 22, fontWeight: 800, color: '#0f766e', padding: { bottom: 4 } },
    subtitle: { text: 'by quarter, $K', fontSize: 14, color: '#94a3b8', padding: { bottom: 16 } },
    series: [{ type: 'bar', xField: 'quarter', yField: 'revenue', name: 'Revenue' }],
    legend: { enabled: false },
  };
}
ts
export function getData() {
  return [
    { quarter: 'Q1', revenue: 210 },
    { quarter: 'Q2', revenue: 265 },
    { quarter: 'Q3', revenue: 248 },
    { quarter: 'Q4', revenue: 312 },
  ];
}

Options

OptionTypeDefaultDescription
textstringcaption text
enabledbooleantrueshow the caption
textAlign'left' | 'center' | 'right''center'alignment within the chart width (snaps to the chart padding)
position'top' | 'bottom''top'above or below the plot
fontSizePixels17 / 13title / subtitle font size
fontWeightFontWeight'bold' / 'normal'font weight
fontFamilystringtheme fontfont family
colorColorValueforeground / mutedtext color
paddingPaddingValue8 on the plot-facing sidepadding around the text: 8, [8, 12], [8, 12, 4, 0] or { top, right, bottom, left }
spacingPixels8deprecated — the plot-facing side of padding
wrapbooleantruebreak long text onto several lines

Alignment and placement

textAlign positions the caption within the chart width, snapping to the chart padding; position moves it above or below the plot. The title and subtitle are configured independently — mix them freely, e.g. a left-aligned title on top with a footnote-style subtitle at the bottom right:

ts
import { getData } from './data';
import type { ChartOptions } from 'grafit-charts';

// Left-aligned title on top, a footnote-style subtitle at the bottom right.
export function createOptions(): ChartOptions {
  return {
    data: getData(),
    title: { text: 'Weekly sessions', textAlign: 'left', padding: { bottom: 12 } },
    subtitle: { text: 'updated hourly', textAlign: 'right', position: 'bottom' },
    series: [{ type: 'area', xField: 'day', yField: 'sessions', name: 'Sessions' }],
    legend: { enabled: false },
  };
}
ts
export function getData() {
  return [
    { day: 'Mon', sessions: 320 },
    { day: 'Tue', sessions: 356 },
    { day: 'Wed', sessions: 402 },
    { day: 'Thu', sessions: 384 },
    { day: 'Fri', sessions: 441 },
    { day: 'Sat', sessions: 268 },
    { day: 'Sun', sessions: 214 },
  ];
}

When both captions share a zone, the title always stays above the subtitle; the padding of the last one faces the plot.

Padding

padding takes the same CSS-like shorthand as everywhere else in the options — a single number, [vertical, horizontal], [top, right, bottom, left], or the named object:

js
title: { text: 'Site traffic', padding: 12 },              // all four sides
title: { text: 'Site traffic', padding: [4, 24] },         // vertical, horizontal
title: { text: 'Site traffic', padding: [4, 24, 12, 24] }, // top, right, bottom, left
title: { text: 'Site traffic', padding: { bottom: 12 } },  // one side, the rest default

By default only the side facing the plot is padded, by 8px: below the caption in the 'top' zone, above it in the 'bottom' zone. Any side left out of the object form keeps that default, so { bottom: 12 } on a top caption is just a wider gap towards the plot.

The vertical padding grows the caption block and shrinks the plot by the same amount. The horizontal padding narrows the width the text is measured against — a left-aligned caption starts padding.left in from the chart padding, and long text wraps earlier:

js
grafit.create({
  container: '#chart',
  title: { text: 'Site traffic', padding: { bottom: 4 } },
  subtitle: { text: 'visits per month, thousands', padding: { bottom: 28 } },
  // ...
});

With both captions stacked on top, title.padding.bottom separates the title from the subtitle, and subtitle.padding.bottom separates the subtitle from the plot — so widening the gap under the captions means padding whichever caption sits last. Without a subtitle, the title is the one facing the plot. To move both captions away from the chart edge instead, use the chart-level padding.top (or padding.bottom).

The older spacing option still works and sets the same plot-facing gap, but padding wins wherever both are given.

Long text

A caption that does not fit the available width is broken between words onto several lines, each one a fontSize × 1.25 step below the previous — the caption grows downwards (or upwards, in the 'bottom' zone) and the plot area shrinks accordingly. A '\n' in the text always starts a new line, so a fixed two-line caption needs no measuring:

js
title: { text: 'Site traffic by acquisition channel\ntwelve months to August' },

wrap: false keeps the text on a single line even when it overflows the chart — explicit '\n' breaks still apply.

Flowing around a floating legend

A floating legend overlays the whole chart area, captions included. By default the captions flow around it: every line that is level with the legend box is laid out in the wider gap beside it, and lines below it use the full width again. So a long title pinned to the left and a top-right legend share the top zone without overlapping:

ts
import { getData } from './data';
import type { ChartOptions } from 'grafit-charts';

// Long captions wrap onto several lines and flow around the floating legend
// instead of running underneath it.
export function createOptions(): ChartOptions {
  return {
    data: getData(),
    title: { text: 'Site traffic by acquisition channel, twelve months to August', textAlign: 'left', padding: { bottom: 4 } },
    subtitle: { text: 'visits per month, thousands; organic includes search and referrals', textAlign: 'left', padding: { bottom: 12 } },
    series: [
      { type: 'line', xField: 'month', yField: 'organic', name: 'Organic' },
      { type: 'line', xField: 'month', yField: 'ads', name: 'Ads' },
    ],
    legend: {
      position: 'top-right',
      floating: true,
      background: { fill: 'rgba(255, 255, 255, 0.85)', stroke: '#cbd5e1', cornerRadius: 6, padding: 10 },
    },
  };
}
ts
export function getData() {
  return [
    { month: 'Jan', organic: 42, ads: 18 },
    { month: 'Feb', organic: 48, ads: 22 },
    { month: 'Mar', organic: 55, ads: 21 },
    { month: 'Apr', organic: 61, ads: 27 },
    { month: 'May', organic: 58, ads: 33 },
    { month: 'Jun', organic: 67, ads: 30 },
    { month: 'Jul', organic: 74, ads: 36 },
    { month: 'Aug', organic: 71, ads: 41 },
  ];
}

Turn the behaviour off with legend.avoidCaptions: false — the captions then use the full chart width and the legend is drawn over them. When the legend leaves no usable gap on either side (a wide legend anchored in the middle), a line falls back to the full width as well.

A short left-aligned title pairs well with a floating legend pinned to the opposite corner on the same level:

ts
import { getData } from './data';
import type { ChartOptions } from 'grafit-charts';

// A floating legend anchored to the top-right corner of the whole chart —
// on the same level as the left-aligned title.
export function createOptions(): ChartOptions {
  return {
    data: getData(),
    title: { text: 'Site traffic', textAlign: 'left', padding: { bottom: 4 } },
    subtitle: { text: 'visits per month, thousands', textAlign: 'left', padding: { bottom: 12 } },
    series: [
      { type: 'line', xField: 'month', yField: 'organic', name: 'Organic' },
      { type: 'line', xField: 'month', yField: 'ads', name: 'Ads' },
    ],
    legend: {
      position: 'top-right',
      floating: true,
      background: {
        fill: 'rgba(255, 255, 255, 0.9)',
        stroke: '#cbd5e1',
        cornerRadius: 6,
        // CSS-like shorthand: [vertical, horizontal]
        padding: [8, 12],
        // lifts the panel off the plot it overlays
        shadow: { color: 'rgba(15, 23, 42, 0.18)', blur: 10, offsetY: 3 },
      },
    },
  };
}
ts
export function getData() {
  return [
    { month: 'Jan', organic: 42, ads: 18 },
    { month: 'Feb', organic: 48, ads: 22 },
    { month: 'Mar', organic: 55, ads: 21 },
    { month: 'Apr', organic: 61, ads: 27 },
    { month: 'May', organic: 58, ads: 33 },
    { month: 'Jun', organic: 67, ads: 30 },
    { month: 'Jul', organic: 74, ads: 36 },
    { month: 'Aug', organic: 71, ads: 41 },
  ];
}