Skip to content

Группы

Группа описывает строку timeline: ресурс, исполнителя, категорию, машину, рейс, смену или любой другой контейнер, в котором раскладываются задачи. Задача попадает в строку группы через task.groupId.

Пользовательский тип группы всегда должен расширять TimelineGroupInput. Базовый контракт нужен runtime для связи задач с группами, фильтрации, layout и API-обновлений. Все дополнительные поля остаются вашими доменными данными: их можно использовать в options.groups.columns, options.groups.order, options.groups.filter, templates и обработчиках событий.

ts
import type { TimelineGroupInput } from '@engine2d/timeline-chart'

interface Group extends TimelineGroupInput {
  title: string
  owner: string
  shift: 'day' | 'night'
}

const groups: Group[] = [
  { id: 'group-1', title: 'Группа 1', owner: 'Ops', shift: 'day' },
]

Стандартные поля

ПолеТипОбязательноеОписание
idstringдаСтабильный идентификатор группы. На него ссылается task.groupId.
parentIdstring | nullнетId родительской группы для вложенной структуры. Если родитель не найден, группа считается root-level.
expandedbooleanнетНачальное состояние раскрытия группы с подгруппами. Если не задано, используется groups.tree.defaultExpanded.
visiblebooleanнетФлаг видимости, который удобно использовать в options.groups.filter.
allowOverlapbooleanнетРазрешает пересечение range-задач внутри этой группы, если сценарий допускает overlap.

Пользовательские поля

Timeline не ограничивает модель группы только стандартными полями. Например, title можно вывести в колонке, owner показать в tooltip или header template, а shift использовать для сортировки и цветовой схемы.

Исполняемый примерTimeline: optionsИзменение layout и visual options.
import { Nova, NovaNode, type NovaSchema } from '@endge/nova'

// Документационный сценарий: timeline-chart-options
class ExampleNode extends NovaNode<Record<string, Event>> {
  render(): void {
    const schema: NovaSchema = [
      {
        type: 'rect',
        x: 24,
        y: 24,
        width: this.width - 48,
        height: this.height - 48,
        styles: { background: '#0d2745' },
      },
      {
        type: 'text',
        x: 44,
        y: 44,
        width: this.width - 88,
        height: 32,
        text: 'Timeline: options',
        styles: { color: '#e8f5ff' },
      },
    ]

    this.renderer.schema(schema)
    this.setRenderBoundsFromSchema(schema)
  }
}

export function mountExample(canvas: HTMLCanvasElement) {
  const app = Nova.createApp({
    target: canvas,
    size: { width: canvas.clientWidth, height: canvas.clientHeight, maxDpr: 2 },
    scheduler: { loop: false },
  })
  const surface = app.surface({ id: 'resizable-workspace' })
  surface.createNode(ExampleNode)

  return () => app.destroy()
}

Добавление и обновление групп

ts
import type { TimelineChartRef, TimelineGroupInput, TimelineTaskInput } from '@engine2d/timeline-chart'

interface Group extends TimelineGroupInput {
  title: string
  owner: string
}

interface Task extends TimelineTaskInput {
  title: string
}

function addGroup(timeline: TimelineChartRef<Group, Task>): void {
  timeline.add({
    groups: [{ id: 'group-2', title: 'Группа 2', owner: 'Support' }],
  })
}

function updateGroup(timeline: TimelineChartRef<Group, Task>): void {
  timeline.update({
    groups: [{ id: 'group-1', title: 'Группа 1 / обновлена', owner: 'Ops' }],
  })
}

function hideGroup(timeline: TimelineChartRef<Group, Task>): void {
  timeline.update({
    groups: [{ id: 'group-1', visible: false }],
  })
}

function removeGroup(timeline: TimelineChartRef<Group, Task>): void {
  timeline.remove({
    groups: ['group-2'],
  })
}

Панель групп

Левая панель групп показывает строки timeline и помогает пользователю сопоставлять задачи с ресурсами. Ее можно скрыть через groups.visible, но в операционных интерфейсах панель обычно оставляют включенной: без нее плотный timeline быстро теряет контекст.

Ширина панели складывается из ширин колонок. Если колонок несколько, держите первую колонку стабильной и читаемой: обычно это название ресурса, группы или исполнителя.

ts
const options = {
  groups: {
    visible: true,
    columns: [
      { id: 'title', title: 'Группа', width: 160, data: (group: Group) => group.title },
      { id: 'owner', title: 'Owner', width: 100, data: (group: Group) => group.owner },
    ],
    paddingY: 6,
  },
}

Вложенные группы

Вложенность задается плоским списком через parentId. Runtime не рендерит дерево рекурсивно: перед отрисовкой он строит плоскую visible-проекцию раскрытых строк, поэтому виртуализация, grid, hit-test и задачи продолжают работать как для обычного списка групп.

ts
const groups: Group[] = [
  { id: 'project', title: 'Запуск продукта', expanded: true },
  { id: 'research', parentId: 'project', title: 'Исследование', expanded: true },
  { id: 'engineering', parentId: 'project', title: 'Разработка', expanded: true },
  { id: 'frontend', parentId: 'engineering', title: 'Фронтенд', expanded: true },
  { id: 'backend', parentId: 'engineering', title: 'Бэкенд', expanded: true },
]

Если хотя бы одна группа содержит parentId, tree mode включается автоматически. Для явной настройки используйте options.groups.tree:

ts
const options = {
  groups: {
    visible: true,
    columns: [
      { id: 'title', title: 'Группа', width: 260, data: (group: Group) => group.title },
    ],
    tree: {
      enabled: true,
      defaultExpanded: true,
      indent: 18,
      disclosureColumnId: 'title',
    },
  },
}

disclosureColumnId указывает колонку, в которой рисуется marker раскрытия. В template колонки доступен runtime group context: group.depth, group.hasChildren, group.expanded, group.childrenCount, group.parentId.

ts
const options = {
  groups: {
    columns: [
      {
        id: 'title',
        title: 'Группа',
        width: 260,
        data: group => group.title,
        template: ({ group, data, x, y, width, height }) => [
          {
            type: 'text',
            text: `${group.hasChildren ? (group.expanded ? '-' : '+') : ''} ${data}`,
            x: x + group.depth * 18,
            y,
            width: width - group.depth * 18,
            height,
            styles: { ellipsis: true },
          },
        ],
      },
    ],
  },
}

API раскрытия

Через TimelineChartRef можно управлять одной группой или всей веткой:

ts
timeline.value?.collapseGroup('engineering')
timeline.value?.expandGroup('engineering')
timeline.value?.toggleGroup('engineering')
timeline.value?.setGroupExpanded('engineering', false)

timeline.value?.collapseGroupBranch('project')
timeline.value?.expandGroupBranch('project')

collapseGroup(id) скрывает потомков, но сохраняет их внутреннее состояние. collapseGroupBranch(id) дополнительно сворачивает всех потомков, поэтому после раскрытия родителя вложенные уровни останутся свернутыми.

Колонки

Колонки панели групп задаются в options.groups.columns. Функция data получает исходный объект группы.

ts
const options = {
  groups: {
    visible: true,
    columns: [
      { id: 'title', title: 'Группа', width: 160, data: (group: Group) => group.title },
      { id: 'owner', title: 'Owner', width: 100, data: (group: Group) => group.owner },
    ],
    order: (a: Group, b: Group) => a.title.localeCompare(b.title),
    filter: (group: Group) => group.visible !== false,
  },
}

Layout строк

Группа не задает свою высоту напрямую. Высота строки рассчитывается из задач внутри группы, options.tasks.height, options.tasks.rowGap, options.groups.paddingY и overlap-правил. Если allowOverlap: false, runtime старается развести пересекающиеся range-задачи по строкам внутри группы.

:::callout title="Стабильные id" group.id должен быть постоянным между render/update циклами. Не используйте индекс массива и не генерируйте новый id при каждом refetch: update, remove, связь task.groupId и сортировка строк опираются на этот ключ. :::