Funnel and Pyramid
Stage-based series without axes: flat data with stageField/valueField.
Funnel
Stages run top to bottom, width is proportional to the value. funnel — rectangular stages, cone-funnel — trapezoids tapering to the next stage.
import { getData } from './data';
import type { ChartOptions } from 'grafit-charts';
export function createOptions(): ChartOptions {
return {
data: getData(),
title: { text: 'Activation funnel' },
series: [{ type: 'cone-funnel', stageField: 'stage', valueField: 'value', name: 'Users' }],
legend: { enabled: false },
};
}export function getData() {
return [
{ stage: 'Visits', value: 12400 },
{ stage: 'Sign-ups', value: 5300 },
{ stage: 'Activations', value: 2900 },
{ stage: 'Subscriptions', value: 1150 },
{ stage: 'Renewals', value: 780 },
];
}Spacing and outside labels
itemSpacing — the gap between segments; label.placement: 'outside' moves labels out to the right. The shape geometry does not depend on labels — the width is set by widthRatio:
import { getData } from './data';
import type { ChartOptions } from 'grafit-charts';
export function createOptions(): ChartOptions {
return {
data: getData(),
title: { text: 'Funnel: spacing and outside labels' },
series: [
{
type: 'funnel',
stageField: 'stage',
valueField: 'count',
itemSpacing: 10,
label: {
placement: 'outside',
formatter: ({ stage, value }) => `${stage} — ${value.toLocaleString('en-US')}`,
},
},
],
legend: { enabled: false },
};
}export function getData() {
return [
{ stage: 'Visits', count: 12500 },
{ stage: 'Sign-ups', count: 6400 },
{ stage: 'Activations', count: 3100 },
{ stage: 'Subscriptions', count: 1400 },
{ stage: 'Renewals', count: 900 },
];
}Cone funnel with outside labels
Trapezoidal stages; the callout line starts at the slanted edge. Inside labels get an outline in the background color (readable on any segment):
import { getData } from './data';
import type { ChartOptions } from 'grafit-charts';
export function createOptions(): ChartOptions {
return {
data: getData(),
title: { text: 'Cone funnel: outside labels' },
series: [
{
type: 'cone-funnel',
stageField: 'stage',
valueField: 'count',
itemSpacing: 2,
label: {
placement: 'outside',
formatter: ({ stage, value }) => `${stage} — ${value.toLocaleString('en-US')}`,
},
},
],
legend: { enabled: false },
};
}export function getData() {
return [
{ stage: 'Leads', count: 8200 },
{ stage: 'Qualified', count: 4900 },
{ stage: 'Demo', count: 2300 },
{ stage: 'Contract', count: 1100 },
{ stage: 'Payment', count: 750 },
];
}What the label is made of
A stage label is the name of the stage and its value, drawn as one block so the two always read together — the same label a pie sector gets. Each half carries its own font, layout decides whether the value follows the name ('inline', the default, behind separator) or sits on a line of its own ('stacked'), and value.type: 'percent' turns the number into the share of the whole funnel:
label: {
placement: 'outside',
layout: 'stacked',
category: { fontWeight: 'bold' },
value: { type: 'percent', fontSize: 11, color: '#8a8f98' },
},import { getData } from './data';
import type { ChartOptions } from 'grafit-charts';
export function createOptions(): ChartOptions {
return {
data: getData(),
title: { text: 'Commerce funnel' },
subtitle: { text: 'the name and the share of the total, each with its own font' },
series: [
{
type: 'cone-funnel',
stageField: 'stage',
valueField: 'value',
name: 'Users',
label: {
placement: 'outside',
layout: 'stacked',
category: { fontWeight: 'bold' },
value: { type: 'percent', fontSize: 11, color: '#8a8f98' },
},
},
],
legend: { enabled: false },
};
}export function getData() {
return [
{ stage: 'Site visits', value: 62000 },
{ stage: 'Product views', value: 24500 },
{ stage: 'Add to cart', value: 8100 },
{ stage: 'Orders', value: 1420 },
];
}The stage name comes out of a data field, and that field has a format like any other — a date with its granularity, a code with its wording. stageName says how that value becomes text, once for the whole series: the legend, the tooltip heading and the name half of the label all read the same. Both halves of the contract are accepted — format, a serializable string that survives a saved config, and formatter, for what a string cannot express:
stageName: { formatter: ({ datum, value }) => formatWeek(value) },Where a label wants something shorter than the legend, label.category has a format of its own and overrides stageName — that is what it is for. It answers for the name exactly as value.format/value.formatter answer for the number, and its formatter receives the same { datum, stage, value, share }:
label: { category: { format: '%d.%m.%Y' }, value: { type: 'percent' } },Either half can go on its own — category: { enabled: false } leaves the bare number, value: { enabled: false } the bare name. label.formatter still speaks for the whole label when one text is all you want; it wins over category/value.
A long tail of thin stages
Every stage is labelled whatever its size, and the crowded ones simply overlap. The same two options a pie has thin them out, and they answer different questions.
label.minShare decides which stages are worth a label at all: below that share of the total a stage is drawn but left unlabelled — what the funnel narrows down to keeps its callout, the tail stays in the shape and in the tooltip.
import { getData } from './data';
import type { ChartOptions } from 'grafit-charts';
export function createOptions(): ChartOptions {
return {
data: getData(),
title: { text: 'Commerce funnel' },
subtitle: { text: 'minShare leaves the tail of the funnel unlabelled' },
series: [
{
type: 'cone-funnel',
stageField: 'stage',
valueField: 'value',
name: 'Users',
label: {
placement: 'outside',
minShare: 0.02,
formatter: ({ stage, value }) => `${stage} — ${value.toLocaleString('en-US')}`,
},
},
],
legend: { enabled: false },
};
}export function getData() {
return [
{ stage: 'Ad impressions', value: 480000 },
{ stage: 'Site visits', value: 62000 },
{ stage: 'Product views', value: 24500 },
{ stage: 'Add to cart', value: 8100 },
{ stage: 'Checkout started', value: 3400 },
{ stage: 'Payment entered', value: 1900 },
{ stage: 'Orders', value: 1420 },
{ stage: 'Repeat orders', value: 460 },
];
}label.avoidOverlap decides whether there is room for a label: the largest stages ask first, so a funnel squeezed for height loses the labels of its thinnest stages rather than of its last ones.
The two combine: minShare picks the stages worth labelling, avoidOverlap guarantees that what is left never collides.
The tooltip
The default tooltip reads the stage value with its share of the whole funnel. The series tooltip.renderer receives the whole datum, so it can display any fields:
tooltip: {
renderer: ({ datum, stage, value, color }) => ({
heading: stage,
rows: [{ label: 'Users', value: `${value} of ${datum.target}`, color }],
}),
}Pyramid
Layer height is proportional to the value; reverse flips the apex downward.
import { getData } from './data';
import type { ChartOptions } from 'grafit-charts';
export function createOptions(): ChartOptions {
return {
data: getData(),
title: { text: 'Company structure' },
series: [{ type: 'pyramid', stageField: 'level', valueField: 'count', name: 'People' }],
legend: { enabled: false },
};
}export function getData() {
return [
{ level: 'C-level', count: 6 },
{ level: 'Managers', count: 28 },
{ level: 'Team leads', count: 90 },
{ level: 'Engineers', count: 420 },
{ level: 'Interns', count: 160 },
];
}Spacing and inside labels
itemSpacing slices the pyramid into layers; label.placement: 'inside' — labels in the segments with an auto-contrast color:
import { getData } from './data';
import type { ChartOptions } from 'grafit-charts';
export function createOptions(): ChartOptions {
return {
data: getData(),
title: { text: 'Pyramid: spacing and inside labels' },
series: [
{
type: 'pyramid',
stageField: 'level',
valueField: 'people',
itemSpacing: 6,
label: { placement: 'inside', fontWeight: 'bold' },
},
],
legend: { enabled: false },
};
}export function getData() {
return [
{ level: 'CEO', people: 2 },
{ level: 'Directors', people: 9 },
{ level: 'Managers', people: 34 },
{ level: 'Engineers', people: 120 },
{ level: 'Interns', people: 45 },
];
}What the label is made of
A pyramid layer gets the same block label as a funnel stage: the name and the value, each with its own font, layout: 'stacked' putting the value on its own line and value: { type: 'percent' } reading it as the share of the total.
label: { placement: 'inside', layout: 'stacked', value: { type: 'percent' } },Labels toward the apex
The layers thin out toward the apex, and their labels are the first to run out of room. label.minShare leaves the thinnest layers unlabelled; label.avoidOverlap hands out what is left to the thickest layers first:
import { getData } from './data';
import type { ChartOptions } from 'grafit-charts';
export function createOptions(): ChartOptions {
return {
data: getData(),
title: { text: 'Org pyramid' },
subtitle: { text: 'minShare and avoidOverlap thin out the labels toward the apex' },
series: [
{
type: 'pyramid',
stageField: 'level',
valueField: 'people',
itemSpacing: 2,
label: { placement: 'inside', minShare: 0.1, avoidOverlap: true, fontWeight: 'bold' },
},
],
legend: { enabled: false },
};
}export function getData() {
return [
{ level: 'Board', people: 4 },
{ level: 'C-level', people: 9 },
{ level: 'VPs', people: 14 },
{ level: 'Directors', people: 26 },
{ level: 'Managers', people: 48 },
{ level: 'Leads', people: 74 },
{ level: 'Engineers', people: 210 },
{ level: 'Interns', people: 62 },
];
}Selection
Stages and layers are picked out by clicks, the way pie sectors are: a selected segment is outlined and the rest fade back while the selection is active. listeners.nodeClick and listeners.selectionChange fire as they do elsewhere, and the selection is drivable from code (chart.setSelection, chart.clickNode) — see Selection.
selection: { enabled: true, mode: 'multiple' },
listeners: { selectionChange: ({ items }) => console.log(items) },Options
Options common to all series (name, showInLegend, tooltip.renderer, …) are covered in Common series options.
| Option | Series | Default | Description |
|---|---|---|---|
stageField | all | — | stage name and value |
stageName.format | string | — | how the stage-name field becomes text: legend, tooltip heading, label |
stageName.formatter | ({ datum, value }) => string | — | the same, when a format string cannot express it |
valueField | all | — | stage name and value |
fills | all | palette | stage colors |
itemSpacing | all | funnel 4, pyramid 0 | gap between segments |
widthRatio | all | 0.62 | fraction of the area width given to the shape (independent of labels) |
reverse | pyramid | false | apex at the bottom |
label.enabled | boolean | true | stage labels |
label.placement | 'inside' | 'outside' | funnel 'inside'; pyramid 'outside' | position (shared by all segments) |
label.formatter | ({ datum, stage, value }) => string | — | the whole label at once; wins over category/value |
label.layout | 'inline' | 'stacked' | 'inline' | the value behind a separator or on its own line |
label.separator | string | ' · ' | what separates the halves of an inline label |
label.category.enabled | boolean | true | the stage name as part of the label |
label.category.format | string | — | format string for the name field ('%d.%m.%Y', ',.0f') |
label.category.formatter | ({ datum, stage, value, share }) => … | — | text of the name half |
label.category.format | string | — | format string for the name field ('%d.%m.%Y', ',.0f') |
label.category.formatter | ({ datum, stage, value, share }) => … | — | text of the name half |
label.category.* | FontOptions | the label font | font of the name |
label.value.enabled | boolean | true | the value as part of the label |
label.value.type | 'value' | 'percent' | 'value' | the value itself or its share of the total |
label.value.format | string | — | format string (',.0f', '.1%') |
label.value.formatter | ({ datum, stage, value, share }) => … | — | text of the value half |
label.value.* | FontOptions | the label font | font of the value |
label.fontSize | Pixels | 12 | font |
label.fontWeight | string | number | normal | font weight |
label.color | ColorValue | inside — auto-contrast; outside — foreground | color |
label.minShare | Fraction | 0 | share of the total a stage needs before it is worth a label |
label.avoidOverlap | boolean | false | drop the labels there is no room for instead of letting them overlap |
calloutLine.enabled | boolean | true when outside | line to the outside label |
calloutLine.length | Pixels | 14 | line length |
calloutLine.stroke | ColorValue | segment color | line color |
calloutLine.strokeWidth | Pixels | 1 | line width |
tooltip.renderer | ({ datum, stage, value, color }) => … | — | custom tooltip |