Skip to content

Funnel и Pyramid

Стадийные серии без осей: плоские данные stageField/valueField.

Funnel

Стадии сверху вниз, ширина пропорциональна значению. funnel — прямоугольные стадии, cone-funnel — трапеции до следующей стадии.

ts
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 },
  };
}
ts
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 },
  ];
}

Отступы и подписи снаружи

itemSpacing — зазор между сегментами; label.placement: 'outside' выносит подписи вправо. Геометрия фигуры не зависит от подписей — ширину задаёт widthRatio:

ts
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 },
  };
}
ts
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-воронка с подписями снаружи

Трапециевидные стадии; линия идёт от наклонной грани. Внутренние подписи получают обводку цветом фона (читаются на любом сегменте):

ts
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 },
  };
}
ts
export function getData() {
  return [
    { stage: 'Leads', count: 8200 },
    { stage: 'Qualified', count: 4900 },
    { stage: 'Demo', count: 2300 },
    { stage: 'Contract', count: 1100 },
    { stage: 'Payment', count: 750 },
  ];
}

Из чего состоит подпись

Подпись стадии — это имя стадии и её значение, нарисованные одним блоком, чтобы читались вместе: та же подпись, что и у сектора круговой. У каждой половины свой шрифт, layout решает, идёт ли значение следом за именем ('inline' — по умолчанию, через separator) или на своей строке ('stacked'), а value.type: 'percent' превращает число в долю от всей воронки:

js
label: {
  placement: 'outside',
  layout: 'stacked',
  category: { fontWeight: 'bold' },
  value: { type: 'percent', fontSize: 11, color: '#8a8f98' },
},
ts
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 },
  };
}
ts
export function getData() {
  return [
    { stage: 'Site visits', value: 62000 },
    { stage: 'Product views', value: 24500 },
    { stage: 'Add to cart', value: 8100 },
    { stage: 'Orders', value: 1420 },
  ];
}

Имя стадии приходит из поля данных, а у поля формат такой же, как у любого другого — дата со своей гранулярностью, код со своими словами. stageName задаёт, как это значение превращается в текст, один раз на всю серию: легенда, заголовок тултипа и половина подписи с именем читаются одинаково. Принимаются обе половины контракта — format, сериализуемая строка, переживающая сохранение конфига, и formatter — для того, что строкой не выразить:

js
stageName: { formatter: ({ datum, value }) => formatWeek(value) },

Там, где подписи нужно что-то короче легенды, у label.category есть свой формат, и он перекрывает stageName — ради этого он и существует. За имя он отвечает ровно так же, как value.format/value.formatter — за число, а его форматтер получает те же { datum, stage, value, share }:

js
label: { category: { format: '%d.%m.%Y' }, value: { type: 'percent' } },

Каждая половина может остаться одна: category: { enabled: false } оставит голое число, value: { enabled: false } — только имя. label.formatter по-прежнему говорит за всю подпись, когда нужен один текст, и имеет приоритет над category/value.

Длинный хвост тонких стадий

Подпись получает каждая стадия, какой бы тонкой она ни была, — в тесноте подписи просто накладываются. Прореживают их те же две опции, что и у круговых, и отвечают они на разные вопросы.

label.minShare решает, какие стадии вообще достойны подписи: ниже этой доли от суммы стадия рисуется, но остаётся без подписи — то, к чему сужается воронка, сохраняет выноску, а хвост остаётся в фигуре и в тултипе.

ts
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 },
  };
}
ts
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 решает, есть ли для подписи место: первыми просят самые крупные стадии, поэтому зажатая по высоте воронка теряет подписи тонких стадий, а не последних.

Опции складываются: minShare отбирает стадии, достойные подписи, avoidOverlap гарантирует, что оставшиеся не столкнутся.

Тултип

Тултип по умолчанию показывает значение стадии с её долей от всей воронки. tooltip.renderer серии получает datum целиком — в тултип можно выводить любые поля:

js
tooltip: {
  renderer: ({ datum, stage, value, color }) => ({
    heading: stage,
    rows: [{ label: 'Пользователи', value: `${value} из ${datum.target}`, color }],
  }),
}

Pyramid

Высота слоя пропорциональна значению; reverse переворачивает остриё вниз.

ts
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 },
  };
}
ts
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 },
  ];
}

Отступы и подписи внутри

itemSpacing разрезает пирамиду на слои; label.placement: 'inside' — подписи в сегментах с автоконтрастным цветом:

ts
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 },
  };
}
ts
export function getData() {
  return [
    { level: 'CEO', people: 2 },
    { level: 'Directors', people: 9 },
    { level: 'Managers', people: 34 },
    { level: 'Engineers', people: 120 },
    { level: 'Interns', people: 45 },
  ];
}

Из чего состоит подпись

Слой пирамиды получает такую же блочную подпись, как стадия воронки: имя и значение, у каждого свой шрифт, layout: 'stacked' уводит значение на отдельную строку, а value: { type: 'percent' } читает его как долю от суммы.

js
label: { placement: 'inside', layout: 'stacked', value: { type: 'percent' } },

Подписи ближе к вершине

К вершине слои утончаются, и их подписям первым не хватает места. label.minShare оставляет самые тонкие слои без подписи, label.avoidOverlap раздаёт оставшееся место сначала самым толстым слоям:

ts
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 },
  };
}
ts
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 },
  ];
}

Выделение

Стадии и слои выбираются кликами так же, как секторы круговых: выбранный сегмент получает обводку, остальные приглушаются, пока выбор активен. listeners.nodeClick и listeners.selectionChange работают как везде, выбором можно управлять из кода (chart.setSelection, chart.clickNode) — см. Выделение.

js
selection: { enabled: true, mode: 'multiple' },
listeners: { selectionChange: ({ items }) => console.log(items) },

Опции

Общие опции всех серий (name, showInLegend, tooltip.renderer, …) — в разделе Общие опции серий.

ОпцияСерииПо умолчаниюОписание
stageFieldвсеимя и значение стадии
stageName.formatstringкак поле имени стадии становится текстом: легенда, заголовок тултипа, подпись
stageName.formatter({ datum, value }) => stringто же, когда строкой формата не выразить
valueFieldвсеимя и значение стадии
fillsвсепалитрацвета стадий
itemSpacingвсеfunnel 4, pyramid 0зазор между сегментами
widthRatioвсе0.62доля ширины области под фигуру (не зависит от подписей)
reversepyramidfalseостриё снизу
label.enabledbooleantrueподписи стадий
label.placement'inside' | 'outside'funnel 'inside'; pyramid 'outside'позиция (общая для всех сегментов)
label.formatter({ datum, stage, value }) => stringвся подпись сразу; приоритет над category/value
label.layout'inline' | 'stacked''inline'значение через разделитель или на своей строке
label.separatorstring' · 'разделитель половин inline-подписи
label.category.enabledbooleantrueимя стадии как часть подписи
label.category.formatstringстрока формата для поля имени ('%d.%m.%Y', ',.0f')
label.category.formatter({ datum, stage, value, share }) => …текст половины с именем
label.category.formatstringстрока формата для поля имени ('%d.%m.%Y', ',.0f')
label.category.formatter({ datum, stage, value, share }) => …текст половины с именем
label.category.*FontOptionsшрифт подписишрифт имени
label.value.enabledbooleantrueзначение как часть подписи
label.value.type'value' | 'percent''value'само значение или его доля от суммы
label.value.formatstringстрока формата (',.0f', '.1%')
label.value.formatter({ datum, stage, value, share }) => …текст половины со значением
label.value.*FontOptionsшрифт подписишрифт значения
label.fontSizePixels12шрифт
label.fontWeightstring | numbernormalнасыщенность
label.colorColorValueinside — автоконтраст; outside — foregroundцвет
label.minShareFraction0доля от суммы, начиная с которой стадия достойна подписи
label.avoidOverlapbooleanfalseскрывать подписи, которым не хватило места
calloutLine.enabledbooleantrue при outsideлиния к внешней подписи
calloutLine.lengthPixels14длина линии
calloutLine.strokeColorValueцвет сегментацвет линии
calloutLine.strokeWidthPixels1толщина
tooltip.renderer({ datum, stage, value, color }) => …кастомный тултип