Skip to content

Аннотации

Декларативные пометки в координатах данных — рисуются поверх серий и переживают зум/ресайз. Интерактивное рисование — в будущих фазах.

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

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

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

export function createOptions(): ChartOptions {
  return {
    data: getData(),
    title: { text: 'Annotations' },
    series: [{ type: 'line', xField: 'month', yField: 'price', name: 'Price' }],
    annotations: [
      { type: 'horizontal-line', value: 180, stroke: '#e5484d', label: { text: 'resistance 180' } },
      { type: 'range', axis: 'x', range: ['Mar', 'Apr'], label: { text: 'correction' } },
      { type: 'line', start: { x: 'Jan', y: 140 }, end: { x: 'Aug', y: 196 }, stroke: '#21a06c', lineDash: [6, 4] },
      { type: 'text', x: 'Jun', y: 192, text: 'peak' },
    ],
    legend: { enabled: false },
  };
}
ts
export function getData() {
  return [
    { month: 'Jan', price: 142 },
    { month: 'Feb', price: 149 },
    { month: 'Mar', price: 161 },
    { month: 'Apr', price: 155 },
    { month: 'May', price: 171 },
    { month: 'Jun', price: 188 },
    { month: 'Jul', price: 179 },
    { month: 'Aug', price: 195 },
  ];
}

Горизонтальные и вертикальные линии можно перетаскивать мышью (всегда включено).

Типы

ТипПоляОписание
horizontal-linevalue, stroke?, lineDash?, label?горизонтальный уровень
vertical-linevalue (категория/дата), …вертикальная отметка
linestart: {x, y}, end: {x, y}произвольный отрезок (трендовая)
textx, y, text, color?, fontSize?подпись в точке данных
rangeaxis: 'x' | 'y', range: [a, b], fill?, label?закрашенный диапазон

Значения, которые решают данные

Линия «на среднем» должна двигаться вместе с данными, поэтому value принимает не только ответ, но и вопрос — { stat, field }, пересчитываемый при каждом обновлении:

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

// Reference lines that follow the data: the median and the p95 of the same field
// the histogram bins, recomputed on every update.
export function createOptions(): ChartOptions {
  return {
    data: getData(),
    title: { text: 'Response time' },
    subtitle: { text: 'median and p95 as computed annotations' },
    series: [{ type: 'histogram', xField: 'response', name: 'Requests', binWidth: 25, fillOpacity: 0.7 }],
    annotations: [
      {
        type: 'vertical-line',
        value: { stat: 'median', field: 'response' },
        stroke: '#21a06c',
        label: { formatter: (value) => `median ${Math.round(value)} ms` },
      },
      {
        type: 'vertical-line',
        value: { stat: 'percentile', percentile: 95, field: 'response' },
        stroke: '#e5484d',
        label: { formatter: (value) => `p95 ${Math.round(value)} ms` },
      },
    ],
    legend: { enabled: false },
  };
}
ts
export function getData() {
  const values: Array<{ response: number }> = [];
  for (let i = 0; i < 400; i++) {
    const u = ((i * 9301 + 49297) % 233280) / 233280;
    const v = ((i * 7621 + 1) % 233280) / 233280;
    values.push({ response: Math.round(120 + 55 * (Math.sqrt(-2 * Math.log(u + 1e-6)) * Math.cos(2 * Math.PI * v))) });
  }
  return values.filter((d) => d.response > 0 && d.response < 320);
}
ts
annotations: [
  { type: 'vertical-line', value: { stat: 'median', field: 'response' } },
  {
    type: 'vertical-line',
    value: { stat: 'percentile', percentile: 95, field: 'response' },
    label: { formatter: (value) => `p95 ${Math.round(value)} мс` },
  },
];

stat — одно из 'mean', 'median', 'min', 'max', 'sum', 'percentile'percentile: 0…100).

weightField называет колонку, в которой лежит, за сколько записей отвечает строка. Предагрегированные данные — одна строка на корзину со счётчиком рядом — иначе дают статистику корзин, а не записей: без весов медиана [{ms: 10, n: 900}, {ms: 900, n: 1}] равна 455, с весами — 10. Взвешенное среднее — это Σ w·x / Σ w; взвешенная медиана и перцентиль — значение, на котором накопленный вес переходит отметку, без интерполяции между соседями, так что ответом всегда оказывается реально встречавшееся значение. Строки с отсутствующим, нулевым или отрицательным весом отбрасываются. Оба конца range принимают тот же дескриптор — так пишется полоса между двумя перцентилями:

ts
{
  type: 'range',
  axis: 'y',
  range: [
    { stat: 'percentile', percentile: 25, field: 'price' },
    { stat: 'percentile', percentile: 75, field: 'price' },
  ],
}

label.formatter получает число, на котором оказалась линия, — ради этого вычисляемый уровень и нужен: подпись говорит p95 208 ms, и никто не вписывает 208 руками. Вычисляемую линию нельзя перетащить (она вернулась бы на место на следующем кадре), а статистика, за которой нет ни одного числа, оставляет свою аннотацию ненарисованной.

Полный список опций

ОпцияТипПо умолчаниюОписание
strokeWidthлинии1толщина линии аннотации
fillOpacityrange0.12прозрачность заливки диапазона
label.textstringподпись линии (horizontal/vertical-line)
label.formatter(value: number) => stringподпись из значения линии
label.fontSizePixels11шрифт подписи
label.colorColorValueцвет линиицвет подписи

Координаты задаются значениями данных: категории/даты для X, числа для Y.

horizontal-line и vertical-line можно перетаскивать мышью — значение обновляется по шкале (категориальные линии прилипают к ближайшей категории).