Skip to content

Состояние, синхронизация, экспорт

Состояние чарта

Зум и скрытые легендой серии — сериализуемое состояние:

ts
const state = chart.getState();
// { zoom: { x: [0.25, 0.75] }, hiddenSeries: ['line-1', 'histogram-0#1'] }
localStorage.setItem('chart-state', JSON.stringify(state));

// восстановление — при создании или на живом инстансе
Charts.create({ ...options, initialState: JSON.parse(saved) });
await chart.setState(JSON.parse(saved));

В hiddenSeries попадают и элементы серии, которая кладёт в легенду несколько пунктов, — секторы pie/donut и группы гистограммы, в виде 'seriesId#index'. Они восстанавливаются вместе со всем остальным, и, поскольку серии пересобираются на каждом update(), именно это не даёт фильтру легенды слетать при любой правке опций.

Синхронизация чартов

Чарты с одним sync.groupId разделяют подсветку узлов и окно зума:

ts
Charts.create({ ...top, sync: { groupId: 'dashboard' } });
Charts.create({ ...bottom, sync: { groupId: 'dashboard' } });
ОпцияПо умолчаниюОписание
groupId'default'имя группы
nodeInteractiontrueсинхронизация подсветки (по индексу данных)
zoomtrueсинхронизация окна зума

Модульная сборка

В сборке через grafit-charts/core синхронизация — отдельный модуль: register(syncModule).

Контекстное меню

contextMenu: { enabled: true } — правый клик открывает меню: «Скачать PNG», «Сбросить зум» (когда есть зум) и extraItems: [{ label, action }]. В сборке через grafit-charts/core — отдельный модуль: register(contextMenuModule).

Экспорт

ts
chart.download({ fileName: 'report.png' });
const dataUrl = chart.getImageDataURL();

Анимация

Появление серий анимируется по умолчанию (600 мс, ease-out). При update/updateDelta новые данные перетекают на место, а не подменяют нарисованное: строки сопоставляются, их числовые поля едут к новым значениям, оси едут вместе с ними. Открытая в этот момент подсказка остаётся на своём узле, и числа в ней не отстают.

Строки сопоставляются по позиции, поэтому изменение их количества рисуется сразу. Укажите key — поле, по которому строка остаётся той же строкой, — и оставшиеся строки продолжат перетекать, сколько бы ни пришло и ни ушло: приходящая вырастает от нуля своих полей значения и раскрывает собственную полосу, уходящая опускается и закрывает свою за собой — соседние категории расширяются плавно, а не рывком.

Ось значений едет к новым границам вместе с данными, а не пересчитывается по строкам в движении: её деления остаются круглыми числами конечной шкалы, и ничего на графике не прыгает в момент смены градации.

ts
chart.update({ ...options, data: next, animation: { key: 'month', updateDuration: 300 } });

Кнопка выдаёт новое показание — столбики едут к новым высотам, а выбывший сервис опускается, пока приходящий на его место вырастает:

ts
import { getData, newServices, newValues, type Reading } from './data';
import type { ChartInstance, ChartOptions } from 'grafit-charts';

// Data replaced on demand: the bars walk to their new heights and the axis
// travels with them. `key` says what makes a bar the same bar between readings,
// so a service that drops out sinks away while the one taking its place grows in.
export function createOptions(): ChartOptions {
  return {
    data: getData(),
    title: { text: 'Requests per minute' },
    series: [{ type: 'bar', xField: 'service', yField: 'requests', name: 'Requests', cornerRadius: 4 }],
    legend: { enabled: false },
    tooltip: {},
    animation: { key: 'service', updateDuration: 900 },
  };
}

// Buttons under the demo. The data of the chart is the state of the demo:
// each reading is built from the one on screen, so the bars drift rather than jump.
export const actions = [
  {
    label: 'New values',
    run: (chart: ChartInstance) => update(chart, newValues),
  },
  {
    label: 'New services',
    run: (chart: ChartInstance) => update(chart, newServices),
  },
];

function update(chart: ChartInstance, next: (previous: Reading[]) => Reading[]): void {
  const options = chart.getOptions();
  void chart.update({ ...options, data: next((options.data as Reading[]) ?? getData()) });
}
ts
/** Services the dashboard watches; a reading shows a few of them at a time. */
const SERVICES = ['auth', 'search', 'checkout', 'catalog', 'billing', 'media', 'notify'];

/** A type rather than an interface: a chart takes rows as `Datum`, and only a type alias reads as one. */
export type Reading = {
  service: string;
  requests: number;
};

/** The starting frame, fixed — a demo that opens differently every time is hard to read. */
export function getData(): Reading[] {
  return [
    { service: 'auth', requests: 820 },
    { service: 'search', requests: 1340 },
    { service: 'checkout', requests: 460 },
    { service: 'catalog', requests: 1180 },
    { service: 'billing', requests: 240 },
  ];
}

/** The same services, read again: every bar walks to a new height. */
export function newValues(previous: Reading[]): Reading[] {
  return previous.map((row) => ({ service: row.service, requests: drift(row.requests) }));
}

/**
 * A different handful of services. The ones that stayed keep the load they had
 * and carry on from it; the rest arrive and leave — which is what `key` is for.
 */
export function newServices(previous: Reading[]): Reading[] {
  return pickServices(previous.length).map((service) => ({
    service,
    requests: drift(previous.find((row) => row.service === service)?.requests ?? 600),
  }));
}

/** A random handful, in a stable order — the axis should not reshuffle on its own. */
function pickServices(count: number): string[] {
  const pool = [...SERVICES];
  const picked: string[] = [];
  while (picked.length < count && pool.length > 0) {
    picked.push(...pool.splice(Math.floor(Math.random() * pool.length), 1));
  }
  return picked.sort((a, b) => SERVICES.indexOf(a) - SERVICES.indexOf(b));
}

/** Within ±35% of the previous value, clamped to a plausible range. */
function drift(value: number): number {
  return Math.round(Math.min(1600, Math.max(120, value * (0.65 + Math.random() * 0.7))));
}

animation: { enabled: false } выключает обе анимации. updateEnabled отвечает только за обновление и главнее там, где задан: график может появляться сразу, а дальше двигаться. update() разрешается, когда переход доехал, — как и waitForUpdate().

Опции

ОпцияТипПо умолчаниюОписание
animation.enabledbooleantrueанимация входа и обновлений
animation.durationnumber600длительность появления, мс
animation.updateEnabledbooleananimation.enabledпереход при обновлении отдельно
animation.updateDurationnumberduration, иначе 450длительность перехода, мс
animation.keystring | (datum, index) => unknownпо чему строка — та же строка
contextMenu.enabledbooleantrueменю по правому клику
contextMenu.extraItems{ label, action }[]свои пункты после стандартных
download(options){ fileName?, fileFormat? }chart.pngэкспорт PNG/JPEG
initialStateChartStateстартовый зум и скрытые серии