Привет, Хабр! Меня зовут Андрей, я разработчик интерфейсов в команде User Experience инфраструктурных сервисов Яндекса. Мы развиваем Gravity UI — опенсорсную дизайн‑систему и библиотеку React‑компонентов, которую используют десятки продуктов внутри компании и за её пределами. Сегодня расскажу, как мы столкнулись с задачей визуализации сложных графов, почему существующие решения нас не устроили, и как в итоге появилась @gravity‑ui/graph — библиотека, которую мы решили сделать открытой для сообщества.

Эта история началась с практической проблемы: нам нужно было рендерить графы на 10 000+ элементов с интерактивными компонентами. В Яндексе много проектов, где пользователи создают сложные пайплайны обработки данных — от простых ETL‑процессов до машинного обучения. Когда такие пайплайны создаются программно, количество блоков может достигать десятков тысяч.

Существующие решения нас не устраивали:

  • HTML/SVG‑библиотеки красиво выглядят и удобны в разработке, но начинают тормозить уже на сотнях элементов.

  • Canvas‑решения справляются с производительностью, но требуют огромного количества кода для создания сложных UI‑элементов.

Нарисовать кнопку с закруглёнными углами и градиентом в Canvas несложно. Однако проблемы появляются, когда нужно создать свои сложные контролы или разметку — потребуется писать десятки строк низкоуровневых команд рисования. Каждый элемент интерфейса приходится программировать с нуля — от обработки кликов до анимаций. А нам нужны были полноценные UI‑компоненты: кнопки, селекты, поля ввода, drag‑and‑drop.

Мы решили не выбирать между Canvas и HTML, а использовать всё лучшее из обеих технологий. Идея была проста: автоматически переключаться между режимами в зависимости от того, насколько близко пользователь смотрит на граф.

Попробуйте сами

Откуда взялась задача

Нирвана и её графы

У нас в Яндексе есть сервис Нирвана для создания и выполнения графов обработки данных (мы про неё писали аж в 2018 году). Сервис большой, популярный, существует уже давно.

Часть пользователей создаёт графы руками — водят мышкой, добавляют блоки, соединяют их. С такими графами проблем нет: блоков не много, и всё работает отлично. Но есть проекты, которые создают графы программно. И здесь начинаются сложности: они могут положить в один граф до 10 000 операций. И получается такое:

И такое

Такие графы обычная связка HTML + SVG просто не тянет. Браузер начинает тормозить, память утекает, пользователь страдает. Мы пытались решить проблему в лоб: оптимизировать рендеринг HTML, но рано или поздно упирались в физические ограничения — DOM просто не рассчитан на тысячи одновременно видимых плавающих интерактивных элементов.

Нужно другое решение, и в браузере у нас остался только Canvas. Только он сможет обеспечить необходимую производительность.

Первая мысль — найти готовое решение. На дворе был 2017–2018 год, и мы перелопатили популярные библиотеки для Canvas или рендеринга графов, но все решения упирались в одну и ту же проблему: либо используй Canvas и примитивные элементы, либо используй HTML/SVG и жертвуй производительностью.

А что если не выбирать?

Level of Details: вдохновение из GameDev

В GameDev и картографии есть классная концепция — Level of Details (LOD). Эта техника родилась из необходимости — как показать огромный мир, не убив производительность?

Суть простая: у одного объекта может быть несколько уровней детализации в зависимости от того, как близко на него смотрят. В играх это особенно заметно:

  • Вдалеке видны горы — это простые полигоны с базовой текстурой.

  • Подходите ближе — появляются детали: трава, камни, тени.

  • Ещё ближе — видны отдельные листья на деревьях.

Никто не рендерит миллионы полигонов травы, когда игрок стоит на вершине горы и смотрит вдаль.

В картах принцип тот же — у каждого уровня масштаба свой набор данных и своя детализация:

  • Масштаб континента — видны только страны.

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

  • Ещё ближе — номера домов, кафе, автобусные остановки.

Мы поняли: пользователю не нужны интерактивные кнопки на большом масштабе графа из 10 000 блоков — он их всё равно не увидит и не сможет с ними работать.

Более того, попытка отрендерить 10 000 HTML‑элементов одновременно приведёт к фризу браузера. Но когда он зумится на конкретную область, количество видимых блоков резко падает — с 10 000 до, скажем, 50. Вот тут‑то и освобождаются ресурсы для HTML‑компонентов с богатой интерактивностью.

Три уровня нашей схемы Level of Details

Minimalistic (масштаб 0,1–0,3) — Canvas с простыми примитивами

В этом режиме пользователь видит общую архитектуру системы: где расположены основные группы блоков, как они соединены между собой. Каждый блок — это простой прямоугольник с базовой цветовой кодировкой. Никаких текстов, кнопок, детализированных иконок. Зато можно комфортно рендерить тысячи элементов. На этом уровне пользователь выбирает область для детального изучения.

Schematic (масштаб 0,3–0,7) — Canvas с деталями

Появляются названия блоков, иконки состояний, якоря для соединений. Текст рендерится средствами Canvas API — это быстро, но возможности стилизации ограничены. Связи между блоками становятся более информативными: можно показать направление потока данных, статус соединения. Это переходный режим, где производительность Canvas сочетается с базовой информативностью.

Detailed (масштаб 0,7+) — HTML с полной интерактивностью

Здесь блоки превращаются в полноценные компоненты интерфейса: с кнопками управления, полями для параметров, прогресс‑барами, селектами. Можно использовать любые возможности HTML/CSS, подключать UI‑библиотеки. В этом режиме в viewport обычно помещается не больше 20–50 блоков, что комфортно для детальной работы.

А что если считать FPS для выбора уровня детализации?

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

Как мы пришли к решению

Хорошо, LOD — это круто. Но реализация потребует Canvas для производительности, а это новая головная боль. Рисовать на Canvas это не очень сложно — проблемы появляются когда нужно сделать интерактивность.

Проблема: как понять, куда кликнул пользователь?

В HTML всё просто: кликнул на кнопку — получил событие сразу на элементе. В Canvas сложнее: кликнул на холст — и что дальше? Нужно самим выяснить, на какой элемент кликнул пользователь.

Базово существуют три подхода:

  • Pixel Testing (color picking),

  • Geometric approach (простой перебор всех элементов),

  • Spatial Indexing (пространственный индекс).

Pixel Testing (color picking)

Идея проста: создаём второй невидимый canvas, копируем туда сцену, но каждый элемент заливаем уникальным цветом, который будет считаться ID объекта. При клике считываем цвет пикселя под указателем мыши через getImageData и так получаем ID элемента.

Плюсы

Минусы

Реализуется за пару десятков строк

Не требует дополнительных структур данных

Сглаживание Canvas смешивает цвета — клик на границе фигуры может дать «невалидный» ID

Выключение anti‑aliasing в 2D‑Canvas недоступно

Второй холст дублирует память и удваивает рендер‑проход

Для небольших сцен метод годится, но при 10 000+ элементах процент ошибок становится неприемлемым — откладываем Pixel Testing.

Geometric approach (простой перебор всех элементов)

Идея проста: перебираем все элементы и проверяем, находится ли точка клика внутри элемента.

Плюсы

Минусы

Реализуется за пару десятков строк

Не требует дополнительных структур данных.

Очень медленно работает при большом количестве элементов

Не подходит для больших сцен

Spatial Indexing

Развитие геометрического подхода. В геометрическом подходе мы упирались в количество элементов. Алгоритмы пространственного индекса стараются как‑то сгруппировать рядом стоящие элементы, используя в основном деревья, что позволяет снизить сложность до log n.
Алгоритмов пространственного индекса довольно много, мы выбрали структуру данных R‑Tree в виде библиотеки rbush.

R‑Tree — это, как понятно из названия, дерево, где каждый объект помещается в прямоугольник минимального размера (MBR), а затем эти прямоугольники группируются в более крупные прямоугольники. И так получается дерево, где каждый прямоугольник содержит в себе другие прямоугольники.

R-tree
Картинка из википедии R-tree

Для поиска в RTree нам нужно спускаться по дереву (вглубь прямоугольника), пока мы не попадём в конкретный элемент. Путь выбирается проверкой пересечения поискового прямоугольника с MBR. Все ветви, чьи bounding‑box даже не задевают поисковый прямоугольник, отбрасываются сразу — именно поэтому глубина обхода обычно ограничивается 3–5 уровнями, а сам поиск занимает микросекунды даже на десятках тысяч элементов.

Этот вариант работает хоть и медленнее (O(log n) в лучшем случае и O(n) в худшем), чем pixel testing, но он точнее и менее требователен к памяти.

Событийная модель

На основе RTree мы теперь можем построить нашу событийную модель. Когда пользователь кликает, запускается процедура хит‑тест: формируем прямоугольник размером 1×1 пиксель в координатах курсора и ищем его пересечение в R‑Tree. Получив элемент, в который попадает этот прямоугольник, мы делегируем событие этому элементу. Если элемент не остановил событие, то это событие передаётся его родителю и так до рута. Поведение этой модели похоже на поведение привычной нам событийной модели в браузере. События можно перехватить, запревентить или остановить всплытие.

Как я уже упомянул, при хит‑тесте мы формируем прямоугольник размером 1×1 пиксель, а это значит, что мы можем сформировать прямоугольник любого размера. И это нам поможет сделать еще одну очень важную оптимизацию — Spatial Culling.

Spatial Culling

Spatial Culling — это техника оптимизации рендеринга, которая нацелена на то, чтобы не рисовать то, что не видно. Например, чтобы не рисовать объекты, которые находятся вне пространства камеры или которые загорожены другими элементами сцены. Так как наш граф рисуется в 2D‑пространстве, то нам достаточно не рисовать лишь те объекты, которые находятся вне области видимости камеры (viewport).

Как это работает:

  • при каждом перемещении или зуме камеры мы формируем прямоугольник, равный текущему viewport;

  • ищем его пересечение в R‑Tree;

  • результатом становится список элементов, которые действительно видны;

  • мы рендерим только их, всё остальное пропускается.

Такой приём делает производительность почти независимой от общего количества элементов: если в кадре помещается 40 блоков — библиотека нарисует ровно 40, а не десятки тысяч, скрытых за пределами экрана. На дальних масштабах в viewport попадает большое количество элементов, поэтому мы рисуем лёгкие Canvas‑примитивы, а при приближении камеры количество элементов снижается и высвобождённые ресурсы позволяют переключиться на HTML‑режим с полной детализацией.

Сводя всё воедино, получается простая схема:

  • Canvas отвечает за скорость,

  • HTML — за интерактивность,

  • R‑Tree и Spatial Culling незаметно объединяют их в единую систему, позволяя быстро понимать какие элементы можно нарисовать на html-слое.

Пока камера двигается, маленький viewport спрашивает у R‑Tree лишь те объекты, которые реально находятся в кадре. Такой подход позволяет нам рисовать действительно большие графы, или по крайней мере иметь запас производительности до тех пор, пока пользователь не ограничит viewport.

Итого в своём ядре библиотека содержит:

  • Canvas‑режим с простыми примитивами;

  • HTML‑режим с полной детализацией;

  • R‑Tree и Spatial Culling для оптимизации производительности;

  • привычную событийную модель.

Но для продакшена этого недостаточно, нужно иметь возможность расширять библиотеку и кастомизировать её под свои нужды.

Кастомизация

Библиотека предлагает два взаимодополняющих способа расширения и изменения поведения:

  • Переопределение базовых компонентов. Меняем логику стандартных Block, Anchor, Connection.

  • Расширение через слои (Layers). Добавляем принципиально новую функциональность поверх/под существующей сцены.

Переопределение компонентов

Когда нужно модифицировать внешний вид или поведение уже существующих элементов, наследуемся от базового класса и переопределяем ключевые методы. Затем регистрируем компонент под собственным именем.

Кастомизация блоков

Например, если вам нужно создать граф с прогресс‑барами на блоках — скажем, для отображения статуса выполнения задач в пайплайне — вы можете легко кастомизировать стандартные блоки:

import { CanvasBlock } from "@gravity-ui/graph";

class ProgressBlock extends CanvasBlock {
  // Базовая форма блока с закругленными углами
  public override renderBody(ctx: CanvasRenderingContext2D): void {
    ctx.fillStyle = "#ddd";
    ctx.beginPath();
    ctx.roundRect(this.state.x, this.state.y, this.state.width, this.state.height, 12);
    ctx.fill();
    ctx.closePath();
  }

  public renderSchematicView(ctx: CanvasRenderingContext2D): void {
    const progress = this.state.meta?.progress || 0;

    // Рисуем основу блока
    this.renderBody(ctx);

    // Прогресс-бар с цветовой индикацией
    const progressWidth = (this.state.width - 20) * (progress / 100);
    ctx.fillStyle = progress < 50 ? "#ff6b6b" : progress < 80 ? "#feca57" : "#48cae4";
    ctx.fillRect(this.state.x + 10, this.state.y + this.state.height - 15, progressWidth, 8);

    // Рамка прогресс-бара
    ctx.strokeStyle = "#ddd";
    ctx.lineWidth = 1;
    ctx.strokeRect(this.state.x + 10, this.state.y + this.state.height - 15, this.state.width - 20, 8);

    // Текст с процентами и названием
    ctx.fillStyle = "#2d3436";
    ctx.font = "12px Arial";
    ctx.textAlign = "center";
    ctx.fillText(`${Math.round(progress)}%`, this.state.x + this.state.width / 2, this.state.y + 20);
    ctx.fillText(this.state.name, this.state.x + this.state.width / 2, this.state.y + 40);
  }
}

Кастомизация соединений

Аналогично, если вам нужно изменить поведение и внешний вид связей, — например, показать интенсивность потока данных между блоками — вы можете создать кастомное соединение:

import { BlockConnection } from "@gravity-ui/graph";

class DataFlowConnection extends BlockConnection {
  public override style(ctx: CanvasRenderingContext2D) {
    // Получаем данные о потоке из связанных блоков
    const sourceBlock = this.sourceBlock;
    const targetBlock = this.targetBlock;

    const sourceProgress = sourceBlock?.state.meta?.progress || 0;
    const targetProgress = targetBlock?.state.meta?.progress || 0;

    // Вычисляем интенсивность потока на основе прогресса блоков
    const flowRate = Math.min(sourceProgress, targetProgress);
    const isActive = flowRate > 10; // Поток активен при прогрессе > 10%

    if (isActive) {
      // Активный поток -- толстая зеленая линия
      ctx.strokeStyle = "#00b894";
      ctx.lineWidth = Math.max(2, Math.min(6, flowRate / 20));
    } else {
      // Неактивный поток -- пунктирная серая линия
      ctx.strokeStyle = "#ddd";
      ctx.lineWidth = this.context.camera.getCameraScale();
      ctx.setLineDash([5, 5]);
    }

    return { type: "stroke" };
  }
}

Использование кастомных компонентов

Регистрируем созданные компоненты в настройках графа:

const customGraph = new Graph({
  blocks: [
    {
      id: "task1",
      is: "ProgressBlock",
      x: 100,
      y: 100,
      width: 200,
      height: 80,
      name: "Data Processing",
      meta: { progress: 75 },
    },
    {
      id: "task2",
      is: "ProgressBlock",
      x: 400,
      y: 100,
      width: 200,
      height: 80,
      name: "Analysis",
      meta: { progress: 30 },
    },
    {
      id: "task3",
      is: "ProgressBlock",
      x: 700,
      y: 100,
      width: 200,
      height: 80,
      name: "Output",
      meta: { progress: 5 },
    },
  ],
  connections: [
    { sourceBlockId: "task1", targetBlockId: "task2" },
    { sourceBlockId: "task2", targetBlockId: "task3" },
  ],
  settings: {
    // Регистрируем кастомные блоки
    blockComponents: {
      'progress': ProgressBlock,
    },
    // Регистрируем кастомное соединение для всех связей
    connection: DataFlowConnection,
    useBezierConnections: true,
  },
});

customGraph.setEntities({
  blocks: [
    {
     is: 'progress',
     id: '1',
     name: "progress block',
     x: 10, 
     y: 10, 
     width: 10, 
     height: 10,
     anchors: [],
     selected: false,
    }
  ]
})

customGraph.start();

Результат

В результате получается граф, где:

  • блоки показывают текущий прогресс с цветовой индикацией;

  • соединения визуализируют поток данных: активные потоки — зелёные и толстые, неактивные — серые и пунктирные;

  • при зуме блоки автоматически переключаются на HTML‑режим с полной интерактивностью.

Расширение слоями

Слои — это дополнительные Canvas или HTML‑элементы, которые вставляются в «пространство» графа. По сути, каждый слой — это отдельный канал рендеринга, который может содержать собственный canvas для быстрой графики или HTML‑контейнер для сложных интерактивных элементов.

Кстати, именно через слои работает React‑интеграция нашей библиотеки: React‑компоненты рендерятся в HTML‑слой через React Portal.

Архитектура слоёв

Слои — это еще одно решение ключевое решение дилеммы Canvas vs HTML. Слои синхронизируют позиции Canvas и HTML‑элементов, обеспечивая правильное наложение их друг на друга. Это позволяет бесшовно переключать Canvas и HTML оставаясь в едином пространстве. Граф состоит из независимых слоёв, наложенных друг на друга:

Слои могут работать в двух системах координат:

  • Привязанные к графу (transformByCameraPosition: true):

    • элементы движутся вместе с камерой,

    • блоки, соединения, элементы графа.

  • Зафиксированные на экране (transformByCameraPosition: false):

    • остаются на месте при панорамировании,

    • тулбары, легенды, контролы UI.

Как устроена React-интеграция

Слой с React‑интеграцией достаточно показателен для демонстрации, что такое слои. Для начала давайте посмотрим на компонент, выделяющий список блоков, которые находятся в области видимости камеры. Для этого нам нужно подписаться на изменения камеры и после каждого изменения делать проверку пересечения viewport камеры c hitbox элементов.

import { Graph } from "@gravity-ui/graph";

const BlocksList = ({ graph, renderBlock }: { graph: Graph, renderBlock: (graph: Graph, block: TBlock) => React.JSX.Element }) => {
  const [blocks, setBlocks] = useState([]);

  const updateVisibleList = useCallback(() => {
    const cameraState = graph.cameraService.getCameraState();
     const CAMERA_VIEWPORT_TRESHOLD = 0.5;
     const x = -cameraState.relativeX - cameraState.relativeWidth * CAMERA_VIEWPORT_TRESHOLD;
     const y = -cameraState.relativeY - cameraState.relativeHeight * CAMERA_VIEWPORT_TRESHOLD;
     const width = -cameraState.relativeX + cameraState.relativeWidth * (1 + CAMERA_VIEWPORT_TRESHOLD) - x;
     const height = -cameraState.relativeY + cameraState.relativeHeight * (1 + CAMERA_VIEWPORT_TRESHOLD) - y;
    
    const blocks = graph
      .getElementsOverRect(
        {
          x,
          y,
          width,
          height,
        }, // определяет области в которой будет искаться список блоков
        [CanvasBlock] // определяет типы элементов, которые будут искаться в области видимости камеры
      ).map((component) => component.connectedState); // Получаем список моделей блоков

      setBlocks(blocks);
  });

    useGraphEvent(graph, "camera-change", ({ scale }) => {
      if (scale >= 0.7) {
        // Если масштаб больше 0.7, то обновляем список блоков
        updateVisibleList()
        return;
      }
      setBlocks([]);
    });

    return blocks.map(block => <React.Fragment key={block.id}>{renderBlock(graphObject, block)}</React.Fragment>)
}

Теперь давайте посмотрим на описание самого слоя, который будет использовать этот компонент.

import { Layer } from '@gravity-ui/graph';

class ReactLayer extends Layer {
  constructor(props: TReactLayerProps) {
    super({
      html: {
        zIndex: 3, // поднимаем слой над остальными слоями
        classNames: ["no-user-select"], // добавляем класс для отключения выделения текста
        transformByCameraPosition: true, // слой привязан к камере - теперь слой будет двигаться вместе с камерой
      },
      ...props,
    });
  }

  public renderPortal(renderBlock: <T extends TBlock>(block: T) => React.JSX.Element) {
    if (!this.getHTML()) {
      return null;
    }

    const htmlLayer = this.getHTML() as HTMLDivElement;

    return createPortal(
      React.createElement(BlocksList, {
        graphObject: this.context.graph,
        renderBlock: renderBlock,
      }),
      htmlLayer,
    );
  }
}

Теперь мы можем использовать этот слой в нашем приложении.

import { Flex } from "@gravity-ui/uikit";

const graph = useMemo(() => new Graph());
const containerRef = useRef<HTMLDivElement>();

useEffect(() => {
    if (containerRef.current) {
      graph.attach(containerRef.current);
    }

    return () => {
      graph.detach();
    };
  }, [graph, containerRef]);


const reactLayer = useLayer(graph, ReactLayer, {});

const renderBlock = useCallback((graph, block) => <Block graph={graph} block={block}>{block.name}</Block>)

  return (
    <div>
      <div style={{ position: "absolute", overflow: "hidden", width: "100%", height: "100%" }} ref={containerRef}>
        {graph && reactLayer && reactLayer.renderPortal(renderBlock)}
      </div>
    </div>
  );

В целом всё довольно просто. Ничего из того, что было описано выше, не нужно писать самому — всё уже написано и готово к использованию.

Наша библиотека графов: в чем плюсы и как пользоваться

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

Преимущества

Производительность + удобство

@gravity‑ui/graph автоматически переключается между Canvas и HTML в зависимости от масштаба. Это означает, что вы получаете:

  • Стабильные 60 FPS на графах из тысяч элементов.

  • Возможность использовать полноценные HTML‑компоненты с богатой интерактивностью при детальном просмотре.

  • Единую событийную модель независимо от способа рендеринга — click, mouseenter работают одинаково на Canvas и в HTML.

Совместимость с UI-библиотеками

Одно из главных преимуществ — совместимость с любыми UI‑библиотеками. Если ваша команда использует:

  • Gravity UI,

  • Material‑UI,

  • Ant Design,

  • кастомные компоненты.

…, то вам не нужно от них отказываться! При увеличении масштаба граф автоматически переключается в HTML‑режим, где привычные Button, Select, DatePicker в нужной вам цветовой теме работают точно так же, как в обычном React‑приложении.

Framework agnostic

Хотя мы и реализовали базовый HTML‑renderer используя React, мы постарались разрабатывать библиотеку так, чтобы библиотека оставалась framework‑agnostic. Это значит при необходимости вы можете довольно просто реализовать слой с интеграцией вашего любимого фреймворка.

А есть ли аналоги? 

На рынке сейчас довольно много решений для отрисовки графов, от платных решений вроде yFiles, JointJS, до опенсорс‑решений Foblex Flow, baklavajs, jsPlumb. Но мы для сравнения рассматриваем @antv/g6 и React Flow как наиболее популярные инструменты. Каждый из них обладает своими особенностями.

React Flow — хорошая библиотека, заточенная на построение node‑based интерфейсов. У неё есть очень большие возможности, но из‑за использования svg и html — довольно скромная производительность. Библиотека хороша, когда есть уверенность, что графы не будут превышать 100–200 блоков.

В свою очередь у @antv/g6 есть куча возможностей, она поддерживает Canvas и в частности WebGL. Напрямую @antv/g6 и @gravity‑ui/graph, наверное, сравнивать нельзя: ребята больше ориентируются на построения графов и диаграмм, — но node‑based UI тоже поддерживается. Так что antv/g6 подойдёт, если вам важно не только node‑based интерфейс, но и нарисовать графики.

Хотя библиотека @antv/g6 умеет как в canvas/webgl, так и в html/svg, управление правилами переключения придётся делать руками, и нужно это сделать правильно. По производительности она сильно быстрее, чем React Flow, но к библиотеке всё же есть вопросы. Хотя заявлено, что есть поддержка WebGL, если посмотреть на их стресс‑тест, то заметно, что на 60к нодах библиотека не способна обеспечить динамики — на MacBook M3 отрисовка одного кадра заняла 4 секунды. Для сравнения наш стресс‑тест на 111к нод и 109к связей на том же Macbook M3: рендеринг сцены всего графа занимает ~60ms что даёт ~15–20FPS. Это не очень много, но с учетом Spatial Culling есть возможность ограничить viewport и таким образом улучшить отзывчивость. Хотя мейнтейнеры заявляли, что хотят добиться рендеринга 100к нод в 30 FPS, судя по всему, добиться этого им пока не удалось.

Ещё один пункт, по которому @gravity‑ui/graph выигрывает, — это размер бандла. 

Bundle size Minified

Bundle size Minified + Gzipped

@antv/g6 bundlephobia

1.1 MB

324.5kB

react flow bundlephobia

181.2kB

56.4kB

@gravity-ui/graph bundlephobia

2.2kB

672B

Хотя обе библиотеки довольно мощные по производительности или по удобству интеграции, @gravity‑ui/graph обладает рядом преимуществ — библиотека способна обеспечить производительность на действительно больших графах и сохранить UI/UX для пользователя и упростить разработку.

Планы на будущее

Уже сейчас библиотека имеет достаточный запас производительности для большинства задач, поэтому в ближайшее время мы больше внимания будем уделять развитию экосистемы вокруг библиотеки — разрабатывать слои (плагины), интеграции для других библиотек и фреймворков (Angular/Vue/Svelte, …etc), добавим поддержку touch‑девайсов, адаптацию для мобильных браузеров и в целом улучшим UX/DX.

Попробуйте и присоединяйтесь

В репозитории вы найдёте полностью рабочую библиотеку:

  • Ядро на Canvas + R-Tree (≈ 30K строк кода),

  • React-интеграцию,

  • Storybook с примерами.

Установить библиотеку можно в одну строку:

npm install @gravity-ui/graph


Довольно долго библиотека, которая сейчас зовётся @gravity‑ui/graph, была внутренним инструментом внутри Нирваны, и выбранный подход хорошо себя зарекомендовал. Сейчас нам хочется поделиться нашими разработками и помочь разработчикам снаружи рисовать свои графы проще, быстрее и производительнее.

Мы хотим стандартизировать подходы к отображению сложных графов в опенсорс‑сообществе — слишком много команд изобретают велосипед или мучаются с неподходящими инструментами.

Поэтому нам очень важно собрать ваш фидбек — разные проекты приносят разные edge‑случаи, которые позволяют развивать библиотеку. Это поможет нам доработать библиотеку и быстрее растить экосистему Gravity UI.

Комментарии (4)


  1. VitaminND
    07.08.2025 07:38

    Это просто праздник какой то! Спасибо!!!

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

    Подскажите, а без React библиотека будет работать? У меня что-то с React не задалось, не могу подключить в webview в VSCode.


    1. draedful Автор
      07.08.2025 07:38

      Приветствую! Да, библиотека может работать без реакта и html слоя в целом(только канвас). Если потребуется использовать другую библиотеку для рендера html, то библиотека к этому тоже готова, но потребуется написать слой который перенесет твои кубики в пространство графа.


      1. VitaminND
        07.08.2025 07:38

        Подскажите, а какой-то пример минимальный есть для html?

        Я скомпилировал umd версию, получил gravity-ui-graph.min.css и gravity-ui-graph.min.js, после чего подключил к html - и там ошибка, почему-то внутри js оказался еще react.

        Что-то я не так делаю, похоже...


  1. Deimos1337
    07.08.2025 07:38

    Хорошая статья

    Я был одним из первых людей, что попробовали применить библиотеку. Рад, что проект продолжает развиваться.