1c-crm-red
От экспертов «1С-Рарус»: Библиотеки визуализации Vis.js и Pixi.js — хитрости и советы по созданию форм 1С. Часть 2
30.06.2022

От экспертов «1С-Рарус»: Библиотеки визуализации Vis.js и Pixi.js — хитрости и советы по созданию форм 1С. Часть 2

Введение

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

Визуализация схем и отчётов в 1С

В нашей работе мы часто сталкиваемся с задачами визуализации тех или иных схем и отчетов.

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

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

Сетевой маршрут

Об этой задаче мы говорили в прошлой части цикла статей.

Кратко напомним, согласно постановке — необходимо визуализировать матрицу смежности затрат в виде сетевого маршрута, в котором узлами являются наборы аналитик вида Счет + Подразделение + Статья, а ребрами соответственно движения сумм от узла к узлу. Что важно — сеть может быть достаточно «запутанной», в ней могут быть петли, и мы не будем от них избавляться, а отобразим корректно.

Визуализация схем и отчётов в 1С

Неинтерактивные требования

Базовое требование — схема должна быть понятна пользователю. Для этого важно чтобы в механизме отображения были возможности:

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

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

  • Достраивать и удалять узлы/ребра — все ребра уже есть в базе, но отображать их сразу бессмысленно, для актуального анализа сеть должна строиться постепенно при интерактивной работе пользователя по разворачиванию входящих и исходящих узлов.
  • Динамически балансировать узлы и ребра — взаимно размещать их так, чтобы они занимали наиболее оптимальное расположение и не перекрывали друг друга.
  • Агрегировать узлы — при слишком большом количестве узлов отображать множество узлов как один.
  • Настраивать масштабирование схемы.

Также важно обеспечить производительность при отображении большого количества элементов

Интерактивные требования

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

Подробнее обо всех пунктах мы поговорим в разделе решения задачи.

ХАСПП дерево

В модуле для ERP «1С:Общепит» для подсистемы HACCP (Hazard Analysis and Critical Control Points) необходимо разработать механизм для интерактивного создания/изменения блок-схем технологических процессов. Блок схема должна представлять собой граф из узлов разных видов — продуктов/ингредиентов и операций, и соединительных стрелок между ними.

Пример схемы из техзадания:

ХАСПП дерево

Ключевой момент здесь — полное управление схемой со стороны пользователя. Необходимо дать возможность работать с элементами схемы вручную:

  • Добавлять и удалять узлы.
  • Перемещать узлы с сохранением местоположения:
    • По вертикали дискретно по уровням — для визуального удобства, чтобы каждый узел был на определенном «уровне вложенности», чтобы не было «ступенек» на схеме.
    • По горизонтали поступательно, свободным перетаскиванием мышкой.
  • Устанавливать связи между узлами в виде стрелок.

Помимо этого нужно иметь в арсенале несколько видов форм узлов, для визуального отличия номенклатуры и операций.

Оргдиаграмма

В конфигурации «Финансового менеджмента» в основном АРМе «блока целеполагания» необходимо вывести организационную структуру подразделений в виде иерархической блок-схемы, в которой узлами были бы подразделения организации, а линиями между ними отображены связи подчинения — управленческого и функционального.

Оргдиаграмма

Одно из ключевых требований — корректная и быстрая работа в браузере.

Сложное наполнение области узла подразделения

  • Секция индикаторов — блок целеполагания предназначен для отслеживания статуса выполнения целей, поэтому в узле подразделения должна быть секция с индикаторами целей и показателей.
  • В узле должна быть информация о руководителе.
  • Также должно отличаться оформление узлов в зависимости от вида подразделения — прибылеобразующего/
    затратного/сервисного.

Динамическое контекстное меню

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

Поиск узла дерева

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

В остальном требования схожи с предыдущими задачами:

  • Нужно уметь балансировать схему, чтобы при раскрытии/сворачивании ветвей — узлы и связи не перекрывали друг друга.
  • Должна быть возможность задавать разные стили линиям связей между узлами, в зависимости от вида подчинения.
    • Управленческого — согласно управленческой документации, административное подчинение.
    • Функционального — по сфере деятельности без прямого административного подчинения, это подчинение одной оргединицы (сотрудника или подразделения) другой оргединице в пределах реализации определенных функций.
  • Важно обеспечить производительность при разворачивании большого количества узлов.

Выбор инструментов

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

Выбор инструментов

Поле табличного документа

Вероятно самый часто используемый элемент для вывода различных отчетов и диаграмм. Что касается текущих задач — в одном из наших отчетов уже использовался элемент формы «Поле табличного документа» для построения оргдиаграммы.

Поле табличного документа

Поле табличного документа

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

Поле табличного документа

Поле табличного документа это вполне рабочий и мощный инструмент, но для наших задач не хватало гибкости и графических «наворотов».

Графическая схема

Элемент «Графическая схема» предназначен для отрисовки различных организационных, структурных и иных схем.

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

Подробнее об этом элементе можно почитать в одной из прошлых частей «Хитрости и советы по созданию форм 1С. Часть 1».

Ниже приведен пример реализации на основе графической схемы:

Графическая схема

Поле HTML документа

На основе элемента «Поле HTML документа» можно решить множество задач. Например, с помощью него можно создать удобную страницу со справкой:

Поле HTML документа

Или сделать симпатичный интерфейс для истории задач:

Поле HTML документа

То есть он позволяет получить расширение визуальных возможностей по отношению к имеющимся в 1С.

Вместе с тем, стоит учесть, что разработка на основе элемента «Поле HTML документа» может быть трудоемка для рядового программиста, если нет соответствующих знаний в смежных областях html и javascript. И как бы ни был гибок этот элемент, всё же когда мы говорим о динамике, то «голый» html перестает удовлетворять запросам. Вот тут на первый план выходит возможность подключения бесплатных библиотек визуализации на языке javascript с открытым кодом.

Библиотек на javascript великое множество, и надо быть готовым к тому, что необходимо будет потратить существенное время на поиск подходящей и исследование её функционала. Стоит заметить, что, как правило, документации к библиотекам на русском языке нет. То есть 1С-нику весьма желательно знать английский язык.

Рассмотрев несколько вариантов решений мы остановили свой выбор на библиотеках — Vis.js и Pixi.js. Сегодня мы разберем вопросы их подключения, часть их функционала которую мы применили, и другие нюансы.

Vis.js

Vis.js — это динамическая библиотека визуализации графов и сетей. Библиотека достаточно проста в использовании, может обрабатывать большие объемы данных и поддерживает интерактивное взаимодействие с отображаемыми объектами.

Пример простого графа отображенного с помощью vis.js:

Пример простого графа отображенного с помощью vis.js

Более сложные примеры:

Пример более сложного графа отображенного с помощью vis.js

Пример более сложного графа отображенного с помощью vis.js

Помимо сетей, согласно документации, библиотека позволяет также строить диаграммы/графики в том числе в 3D, но данная функциональность нами не исследовалась.

Подробнее на visjs.org.

Pixi.js

PixiJS — это движок рендеринга с высокой производительностью, который можно использовать для создания игр, визуализации данных и других проектов наполненных графикой. У библиотеки удобный API, имеющий множество полезных функций.

При подходе к задачам мы сравнивали возможность реализации с помощью PixiJS и с прямым написанием графики на html. По оценкам PixiJS оказалось намного проще, т. к. библиотека предоставляет набор графических абстракций, комбинируя которые можно удовлетворить всем требованиям, предъявляемым к нашим задачам. Помимо этого, PixiJS подкупил объектным подходом — графические абстракции можно соединять подобно конструктору «лего», и использовать далее как один объект.

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

Выделить конкретный пример визуализации проблематично, их очень много.

Подробнее можно посмотреть на сайте pixijs.com.

Pixi.js

Технология разработки

Подключение библиотек

Чтобы иметь возможность работать с методами библиотеки необходимо подключить её, для этого нужно в тексте html документа указать путь к ней внутри тега script src:

<head>
<script src='Путь_к_библиотеке'></script>
</head> 

Есть несколько подходов, выбор зависит от изолированности системы:

  • Интернет-ссылка.
    Самое простое — можно указать ссылку на текст библиотеки с ресурса в интернете, например с официального сайта:
    • https://unpkg.com/vis-network/standalone/umd/vis-network.min.js.
    • https://cdn.jsdelivr.net/npm/pixi.js-legacy@5.3.7/dist/pixi-legacy.min.js.
  • Ссылка веб-сервера.
    Чтобы быть независимыми от доступности конкретных интернет ресурсов, можно скопировать библиотеку на свой веб-сервер и создать виртуальную директорию. Тогда в тексте html можно указать ссылку на файл библиотеки с веб-сервера:
    • https://<веб-сервер>/visjs/vis-network/standalone/umd/vis-network.js.
  • Локальное хранилище.
    Этот подход подразумевает полную автономность. Как вариант можно получить двоичные данные файла библиотеки и загрузить их в макет в своей конфигурации. Далее в коде 1С перед началом работы нужно сохранить макет во временные файлы ОС, и в тексте html указать путь к этому файлу.
    Подключение библиотек

Объекты библиотеки Vis.js

Документация библиотеки visjs.github.io/vis-network/docs/network/.

Библиотека состоит из компонентов DataSet, Timeline, Network, Graph2d и Graph3d.

DataSet — объект, который позволяет хранить и управлять неструктурированными данными. Элементы структуры можно добавлять, обновлять и удалять из набора данных, а также подписываться на изменения в наборе данных. Данные в DataSet можно фильтровать и упорядочивать, а поля (например, даты) можно преобразовывать в определенный тип. Данные также могут быть нормализованы при добавлении их в DataSet.

Конструктор:

var data = new vis.DataSet([data] [, options])

Пример создания:

// Подготовим список узлов
var nodes = new vis.DataSet([
{ id: 1, label: "Узел 1" },
{ id: 2, label: "Узел 2" },
{ id: 3, label: "Узел 3" }
]);

// Подготовим таблицу ребер
var edges = new vis.DataSet([
{from: 1, to: 3, arrows: "to"},
{from: 1, to: 2, arrows: "from"}
]);

Network — ключевой объект который мы использовали. При его создании задаются начальные значения графа — узлы и ребра, и параметры отображения. Взаимодействие с графом осуществляется через методы объекта Network. Содержит в себе множество методов и настроек, как то — работа с физической моделью, настройка манипуляцией элементов и т. д.

Конструктор:

// Объект HTML документа помещаем в переменную
var container = document.getElementById("canvasConvainer");
// Коллекция данных для визуализации
var data = {
nodes: nodes,
edges: edges,
};
// Коллекция параметров визуализации
var options = {};
// Инициализация объекта приложения
var network = new vis.Network(container, data, options);

В библиотеке также есть компоненты:

  • Timeline — используется для создания настраиваемых, интерактивных таймлайнов.
  • Graph2d/Graph3d — используются для построения графиков в 2D и 3D.

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

Объекты библиотеки Pixi.js

Документация библиотеки pixijs.download/release/docs/index.html. 

Application

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

Конструктор:

const app = new PIXI.Application()

Container

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

Конструктор:

const rootContainer = new PIXI.Container();

Graphics

Класс Graphics в основном используется для рендеринга примитивных фигур, таких как линии, круги и прямоугольники для их окрашивания и наполнения. Также можно использовать объект Graphics для создания списка примитивов и использования в качестве маски.

При работе с объектом следует учесть, что дочерние методы drawRect(), drawPolygon() и т. д. не рисуют на экране напрямую, а сохраняют примитивы для последующего использования. Точно так же функция clear() не изменяет экран, она просто сбрасывает список примитивов.

Конструктор:

const Graphics = new PIXI.Graphics();

Sprite

Объект Sprite является основой для всех текстурированных объектов, отображаемых на экране.

Спрайт может быть создан непосредственно из изображения, либо из объекта Graphics:

const Texture =  app.renderer.generateTexture(Graphics);
var Sprite = new PIXI.Sprite(Texture);

Реализация задач

Сетевой маршрут

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

  • Счет 1 + Подразделение 1 + Статья 1 → Счет 2 + Подразделение 2  + Статья 2 — Сумма.
  • Счет 2 + Подразделение 2 + Статья 2 → Счет 3 + Подразделение 3 + Статья 3 — Сумма.
  • ...

Изначально отчет выводится в виде дерева с динамическим построением дочерних ребер:

Сетевой маршрут

За основу решения мы взяли библиотеку Vis.js.

Инициализация сети

При формировании отчета нам нужно задать начальный текст html, в котором мы инициализируем начальный набор элементов сети — первые N уровней иерархии.

Сначала мы формируем две таблицы — узлов и ребер.

Узлы

Инициализация сети

Ребра

Инициализация сети

В тексте элемента html по таблице создаем объект DataSet, который будет являться списком узлов:

var nodes = new vis.DataSet([
{ "id": "1", "label": "90.02.1\nПодразделение1\nСтатья1"},
{ "id": "2", "label": "26.22\nПодразделение1\nСтатья1"},
{ "id": "3", "label": "26.01\nПодразделение2\nСтатья1"}
]);

Свойства узла:

  • id — идентификатор узла, является обязательным атрибутом и должен быть уникальным;
  • label — заголовок узла, отображаемый внутри или снаружи, в зависимости от формы.

Аналогично создаем объект DataSet для ребер:

var edges =  new vis.DataSet([
{"id": "1", "from": "2",  "to": "1",  "arrows": "to"},
{"id": "2", "from": "3",  "to": "2",  "arrows": "to"}
]); 

Свойства ребра:

  • id — идентификатор ребра, является необязательным атрибутом. В нашем случае в исходных данных он уже есть, и для наглядности мы тоже его указываем.
  • from/to — поля в которых необходимо указать идентификаторы исходящего и входящего узла соответственно.
  • arrows — свойство определяет отрисовку стрелки на ребре. В простом варианте можно указать значения to, from, middle с разделителем в любом сочетании. Для более сложных случаев можно задать в виде структурированного объекта с дополнительными свойствами, см. документацию (visjs.github.io/vis-network/docs/network/edges.html).

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

Инициализация сети

Базовый готовый пример

<html>
<head>
  <script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
  <link href="../../dist/vis.css" rel="stylesheet" type="text/css" />
  <meta http-equiv='X-UA-Compatible' content='IE=edge'>
</head>

<body>
<div id="mynetwork"></div>
<script type="text/javascript">
// create an array with nodes
var nodes = new vis.DataSet([
{ "id": "1", "label": "90.02.1\nПодразделение1\nСтатья1"},
{ "id": "2", "label": "26.22\nПодразделение1\nСтатья1"},
{ "id": "3", "label": "26.01\nПодразделение2\nСтатья1"}
]);

// create an array with edges
var edges = new vis.DataSet([
{"id": "1", "from": "2",  "to": "1",  "arrows": "to"},
{"id": "2", "from": "3",  "to": "2",  "arrows": "to"}
]);

  // create a network
var container = document.getElementById('mynetwork');
var data = {
nodes: nodes,
edges: edges
};
var network = new vis.Network(container, data, {});

</script>
</body>

</html> 

Стили ребер/узлов

Согласно требованиям задачи нам нужно задавать:

  • Толщину линий ребер — в зависимости от веса ребра, то есть суммы движения.
  • Вид узлов — выделять начальные и конечные узлы.

Для этого при формировании массива узлов воспользуемся дополнительными свойствами:

{
"id": "1",
"label": "90.02.1\nПодразделение1\nСтатья1",
"shadow": true,
"shape": "box",
"font": {"color": "#000000", "size": 20},
"color": {"background": "red",
"border": "#000000",
"highlight": {"border": "BLUE"} }

  • shadow — определяет наличие тени у узла, может задаваться в виде булево или в виде объекта для более сложных настроек.
  • shape — определяет форму узла. Некоторые формы поддерживают надписи внутри узла, другие во вне. При желании можно задать пользовательскую форму. Мы воспользуемся видами:
    • box — для узлов с флагом «финрез» по нашей таблице;
    • circle — для остальных.
  • font — объект определяющий свойства надписи узла. Цвет можно задать шестнадцатеричным значением либо литералом вида red, white и т. п. Зададим цвет надписи черным и увеличим размер шрифта для узлов с пометкой «финрез».
  • color — цветовые свойства узла. Для всех узлов задаем синюю подсветку при выделении и для узлов финреза красный фон.

Аналогично для ребер зададим новые свойства:

{
"id": "1",
"from": "2",
"to": "1",
"arrows": "to",
"length": 250,
"color": {"color": "#000000", "highlight": "BLUE"},
"width": 5,
"label": "100",
"font": {"align": "bottom", "size": 20}

  • length — длина ребра в физической симуляции и при отображении. Эмпирическим путем мы выявили, что для нашей задачи значение в 250 оказалось оптимальным
  • color — зададим базовый цвет ребер — черный, и цвет подсветки — синий
  • width — толщина ребра должна зависеть от веса (суммы). Будем задавать толщину по формуле: Толщина = Окр (Log10 (Сумма)) * 2 + 1, логарифм в формуле позволяет увеличивать толщину постепенно — по мере роста порядка суммы. Расчет производится на стороне 1С, и после передается уже в виде числа в структуру настроек ребра.
  • label — подпись ребра, то есть сумма движения в нашем случае.
  • font — свойства шрифта подписи, задаем выравнивание по нижней границе ребра и размер шрифта.

С этим настройками получился такой граф:

Инициализация сети

Физическая модель, автобалансировка

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

Параметры физики задаются при создании объекта сети, в третьем параметре.

var network =  new vis.Network(container, data, options);

Пример настроек, которые мы используем:

var options = {
    physics: {
    enabled: true,
    solver: "barnesHut",
    barnesHut: {
          theta: 0.5,
          gravitationalConstant: -25000,
          centralGravity: 0.3,
          springLength: 195,
          springConstant: 0.04,
          damping: 0.09,
          avoidOverlap: 0
      },
    stabilization: {
          enabled: true,
          iterations: 1000,
          updateInterval: 50,
          onlyDynamicEdges: false,
          fit: false
       },
    },
    smoothCurves: true,
    freezeForStabilization: false
};

Ключевые параметры здесь:

  • enabled — определяет сам факт использования физической модели. По-умолчанию, равно true. Если в вашей задаче автобалансировка не нужна и вы хотите управлять расположением элементов сами, то необходимо установить значение в false.
  • solver — алгоритм физической модели, vis.js поддерживает несколько видов, для конкретной задачи нужно подбирать подходящий. В нашей реализации используется barnesHut. При этом в опциях можно настроить параметры каждой модели дополнительно, в разделе с именем этой модели.
  • stabilization — параметры стабилизации, количество итераций, интервал обновления и т. д.

Опций у физической модели достаточно много, подробнее можно ознакомиться в разделе руководства physics (visjs.github.io/vis-network/docs/network/physics.html).

Демонстрация балансировки физической модели:

Демонстрация балансировки физической модели

Динамическое добавление и удаление элементов графа

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

Тут стоит пояснить, что граф распределения затрат может быть очень ветвистым (как на картинке выше и даже больше) и вывести его сразу целиком — практически невозможно.

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

Для этого мы добавили команды контекстного меню:

  • «Входящие развернуть».
  • «Исходящие развернуть».

Динамическое добавление и удаление элементов графа

Добавляем функцию, которая получит id выделенного узла. Для этого достаточно вызвать метод network.getSelectedNodes(), который вернет массив выделенных узлов, и взять первый элемент.

В нашем случае функция немного усложнилась, т. к. помимо id узла возвращается сопутствующая информация.

function selectedNodeData() {
    var result = "";
    var selectedNodes = network.getSelectedNodes();
    if (selectedNodes.length > 0) {
        var index;
        for (index = 0; index < nodesExtraData.length; ++index) {
            if (nodesExtraData[index].id == selectedNodes[0]) {
                result = JSON.stringify(nodesExtraData[index]);
                break;
            }
        }
    }
    return result;
}

Далее по команде из контекстного меню вызываем этот метод html документа:

JSНажатогоУзла    = Элементы.ТекстПоляHTML.Документ.DefaultView.selectedNodeData();
ДанныеУзла        = ДанныеИзJSONСтроки(JSНажатогоУзла);
ИдентификаторУзла = ДанныеУзла.id; 

По идентификатору узла из данных базы получаем дочерние ребра, примерно так же как делали в начале. И теперь нам нужно их добавить в существующий объект графа, без перезаполнения текста html. Для этого добавляем функцию js addNode:

function addNode(node_object_sting, node_extra_object_sting, edge_object_sting) {            
    var node_object = JSON.parse(node_object_sting);
    var edge_object = JSON.parse(edge_object_sting);
    var selectedNodes = network.getSelectedNodes();
   
    selectedNode = nodes.get(selectedNodes[0]);
    node_object.y = selectedNode.y;
    node_object.x = selectedNode.x;
    nodes.add(node_object);
    edges.add(edge_object);
   
    node_extra_object = JSON.parse(node_extra_object_sting);
    nodesExtraData.push(node_extra_object);
};

Пояснения:

  • Параметры node_object_sting и edge_object_sting это строки, описывающие параметры объекта узла и ребра соответственно, аналогичные тем, что мы задавали вначале:
    {
    "id": "126405",
    "label": "26.01\nМСК.АД\nСодержание ОР",
    "shadow": true,
    "shape": "circle",
    "font": {"color": "#000000",    "size": 14},
    "color": {"background": "#FFFFFF",
          "border": "#000000",
          "highlight": { "border": "BLUE" }
          }
     }
  • nodes.add(node_object) edges.add(edge_object) — добавляем новые узел/ребро в существующие массивы (типа DataSet).
  • node_extra_object_sting — дополнительная информация узла, которую помещаем в отдельный массив, в алгоритме она не участвует.

И вызываем функцию addNode из 1С:

Элементы.ТекстПоляHTML.Документ.DefaultView.addNode(СтрокаУзлаJS,  СтрокаУзлаJSДоп, СтрокаРебраJS);

При разворачивании меняем иконку и имя команды на «Входящие свернуть –».

«Входящие свернуть –»

Теперь нам нужно сделать так, чтобы при нажатии на неё удалялись все узлы с движениями в выбранный узел. Для этого добавляем новый метод js — collapseSelectedNode:

function collapseSelectedNode(Incoming) {
    var selectedNodes = network.getSelectedNodes();
    if (selectedNodes.length == 1)
    {
      selectedNode = nodes.get(selectedNodes[0]);
     collapseNode(selectedNode.id, Incoming);
      }
};

И рекурсивную функцию collapseNode, которая по цепочке вызывая саму себя, удалит все входящие узлы и ребра начиная от выделенного узла:

function collapseNode(NodeId, Incoming) {
      if (Incoming=="true")
            { SwitchIncomingNodesExpanded(selectedNode.id,false); }
      else
            { SwitchOutgoingNodesExpanded(selectedNode.id,false); }
      var ConnectedEdges = network.getConnectedEdges(NodeId);
           
      ConnectedEdges.forEach(edgeGUID => {
      var edge = edges.get(edgeGUID);
     
     if (Incoming=="true" && edge.to == NodeId)
      {
           collapseNode(edge.from, Incoming);
           nodes.remove(edge.from);
          edges.remove(edge);
          spliceNodeExtraData(edge.from)
           }
     
           if (Incoming=="false" && edge.from == NodeId)
      {
           collapseNode(edge.to, Incoming);
          nodes.remove(edge.to);
          edges.remove(edge);
          spliceNodeExtraData(edge.to)
           }
         });
};

И вызываем из 1С по команде контекстного меню:

Элементы.ТекстПоляHTML.Документ.DefaultView.collapseSelectedNode(Входящие);

Пояснения:

  • SwitchIncomingNodesExpanded/SwitchOutgoingNodesExpanded — нам понадобилось сделать дополнительные флаги у каждого узла, которые определяли бы факт развернутости входящих и исходящих ребер, для переключения флагов вызываются эти функции.

Кластеризация

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

По счастью возможности кластеризации уже встроены в библиотеку Vis.js. Варианты исполнения можно посмотреть на странице примеров (visjs.github.io/vis-network/examples/) библиотеки.

Мы реализовали это в виде функции вызываемой из 1С после добавления узлов:

function clusterNodes(parent, order, incoming,  nodelabel, fontsize, edgelabel, edgewidth) {
  var clusterOptionsByData = {
      joinCondition: function (childOptions) {
      return (childOptions.parent == parent && childOptions.order > order && childOptions.incomingnode == incoming);
      },
  clusterNodeProperties: {
    id: parent + incoming, label:nodelabel,
    borderWidth: 3,
    shape: "database",
    font: { size: fontsize },
  },
   clusterEdgeProperties: {
        label: edgelabel,
        width: edgewidth,
        length: 350
    }
};
      network.cluster(clusterOptionsByData);
      network.stabilize();  
};

При раскрытии узла, после добавления массива узлов в сеть, если узлов слишком много (определяется настройкой), вызываем метод:

Элементы.ТекстПоляHTML.Документ.DefaultView.clusterNodes(ДанныеУзла.id, КоличествоУзловДляКластеризации, Входящие,ЗаголовокКластера,РазмерШрифта, ЗаголовокРебра, ТолщинаРебра);

В секции clusterOptionsByData описывается функция отбора узлов для кластеризации. Мы добавили дополнительные поля в узлы — parent и order, определяющие id родительского узла и номер узла в списке (служебное поле рассчитанное программно). В таблице узлы сортируются по сумме ребер поэтому order > 3 означает, что мы поместим в кластер все узлы кроме первых трех.

Блоки clusterNodeProperties и clusterEdgeProperties определяют свойства узла и ребра кластера соответственно. Размер шрифта рассчитывается заранее в зависимости от количества входящих в кластер узлов. Толщина ребра рассчитывается исходя из общей суммы всех ребер кластера, как мы описывали ранее.

Кластеризация

Последние штрихи

Напоследок мы заглянем в раздел interaction блока options (visjs.github.io/vis-network/docs/network/interaction.html). Основные функции для удобства навигации расположены здесь, рычажков которые можно подвигать там достаточно.

Мы воспользовались следующей настройкой:

var options = {
      interaction: {
      navigationButtons: true,
      keyboard: true,
      dragNodes: true
}}
  • navigationButtons — при включении этого свойства, в поле схемы добавляются встроенные навигационные кнопки, которыми можно двигать схему, приближать/удалять и приводить к общему масштабу:

    Реализация задач

  • keyboard — включает возможность использования клавиш клавиатуры, например клавиши «+» и «−» позволяют приближать удалять схему, а стрелки — перемещать.
  • dragNodes — настройка, которая определяет возможность двигать узлы мышью. По умолчанию включена

Как итог получилась такая схема:

Реализация задач

ХАССП дерево

В отличии от предыдущей задачи, где расчет узлов/ребер производился автоматически и нужна была автобалансировка, для схемы ХАССП необходимо было сделать по сути конструктор, в котором пользователь мог бы сам добавлять элементы, соединять узлы друг с другом и вручную настраивать их расположение.

Блоки технологического процесса имеют два вида:

  1. Номенклатура — это ингредиенты блюд, объекты над которыми производятся операции.
  2. Операция — этап работы который производится над ингредиентами/блюдами, например — чистка овощей, поджаривание, отбивка мяса, подача блюда и т. д.

За основу решения этой задачи мы также взяли библиотеку Vis.js.

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

Дискретность перемещения

Так как здесь нам не нужна автобалансировка, первое что было сделано это отключение работы физической симуляции в блоке options:

var options = {
     physics: { enabled: false }
};

Далее, согласно требованиям, нужно было реализовать дискретное перемещение по вертикали.

Для этого мы воспользовались дополнительными настройками в структуре узла:

{"id": "2839652b-cc13-432c-811f-daa2dd544588",
"label": "Мята",
"fixed": { "x": false, "y": true},
"x": 174,
"y": -200,
"font": {"color": "#000000", "size": 14},
"shape": "ellipse",
"color": {"background": "#FFFFFF", "border": "#000000",
"highlight": {"background": "#FFFFFF", "border": "#000000"}}},

  • fixed — блок настроек определяет фиксирование движения по осям координат вручную и в рамках физической симуляции. При этом изменить координаты программно по прежнему возможно. В нашем случае мы заблокировали ось Y.
  • x, y — поля определяющие координаты узла на схеме.

Мы условно разделили схему по строкам, с расстоянием между ними в 100 ед. Добавили два метода для дискретного движения по вертикали:

function levelUp() {
  var selectedNodes = network.getSelectedNodes();
  var index;
    for (index = 0; index < selectedNodes.length; ++index) {
        selectedNode = nodes.get(selectedNodes[index]);
        nodes.update({"id": selectedNodes[index], "y": selectedNode.y + 100});
  }
};

function levelDown() {
var selectedNodes = network.getSelectedNodes();
var index;
for (index = 0; index < selectedNodes.length; ++index) {
selectedNode = nodes.get(selectedNodes[index]);
nodes.update({"id": selectedNodes[index], "y": selectedNode.y - 100});
}
};

В этих функциях с помощью метода update объекта DataSet мы заменяем значение координаты «y» увеличивая или уменьшая её на 100.

И добавили две соответствующие команды в меню «Увеличить уровень» и «Уменьшить уровень», которые вызывают эти методы:

Элементы.Поле1.Документ.DefaultView.levelUp();
Элементы.Поле1.Документ.DefaultView.levelDown();

Дискретность перемещения

Дискретность перемещения

Добавление и удаление узлов

Как и в задаче сетевого маршрута, в конструкторе плана технологического процесса необходимо уметь добавлять узлы. В нашем случае соответствием для узлов являются элементы справочников «Номенклатура» и «Операции».

Мы добавили в меню команды выбора элементов этих справочников:

Добавление и удаление узлов

При выборе элемента формируем на основе его данных строку с параметрами узла:

// Получение текста шаблона для нового узла.
СтрокаJS = ТекстШаблонаУзел(УИД, ТекУзел, СтруктураНастроекХАССП);
СтрокаJSДоп = ТекстШаблонаУзелДопИнформация(УИД, ТекУзел); 

Пример строки со структурой свойств узла:

{"id": "3acec18a-0562-401b-9419-4b11b6f03f57",
"label": "Подготовка овощей и фрукт\nов",
"fixed": { "x": false, "y": true},
"borderWidth": 1,
"font": {"color": "#000000", "size": 14},
"shape": "box",
"color": {"background": "#FFFFFF",
"border": "#000000",
"highlight": {"background": "#FFFFFF", "border": "#000000"}}}

И добавляем узел в сеть:

Элементы.Поле1.Документ.DefaultView.addNode(СтрокаJS, СтрокаJSДоп, ТипСвязи);
Элементы.Поле1.Документ.network.storePositions();
Элементы.Поле1.Документ.network.redraw();

Методы storePositions и redraw нужны, чтобы отрисовать узлы по указанным координатам.

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

function addNode(node_object_sting, node_extra_object_sting, edge_type) {
    var node_object = JSON.parse(node_object_sting);
    var selectedNodes = network.getSelectedNodes();
    if (selectedNodes.length == 1) {
        selectedNode = nodes.get(selectedNodes[0]);
        node_object.y = selectedNode.y + 100;
        nodes.add(node_object);
        if (edge_type == 'Прямая') {
            edges.add({"from": selectedNode.id, "to": node_object.id, "arrows": "to"});
        } else {
            edges.add({"from": node_object.id, "to": selectedNode.id, "arrows": "to"});
        }
    }
    else {
        node_object.y = -100;
        nodes.add(node_object);
    };
    node_extra_object = JSON.parse(node_extra_object_sting);
    nodesExtraData.push(node_extra_object);
};

Установка связей

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

По счастью, в Vis.js есть встроенный режим добавления связей. Чтобы им воспользоваться, по команде пользователя вызовем метод addEdgeMode объекта сети. В этом случае сеть переключается в режим прокладки связи:

Установка связей

&НаКлиенте
Процедура ДобавитьСвязь(Команда)
            Элементы.Поле1.Документ.network.addEdgeMode();
            Модифицированность = Истина;
КонецПроцедуры 

Далее пользователю достаточно зажав левую клавишу мыши протянуть от одного узла к другому:

Установка связей

Установка связей

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

var options = {
    manipulation: {
        addEdge: function(edgeData, callback) {
          if (edgeData.from !== edgeData.to) {
            edgeData.arrows = "to";
            callback(edgeData);
          }
        }
      }
}

Первым параметром в него передаются свойства ребра edgeData, в которых мы установим поле arrows равным to, в результате для ребра прорисуется стрелка на конце.

Сохранение схемы

В довершение всего нам осталось реализовать сохранение созданной пользователем схемы. Перед записью блок-схемы получим данные узлов и ребер из нашей сети:

СтрокаДанныеУзлов    =  Элементы.Поле1.Документ.DefaultView.getNodesData();
СтрокаДанныеУзловДоп = Элементы.Поле1.Документ.DefaultView.getNodesDataExtra();
СтрокаДанныеСвязей   = Элементы.Поле1.Документ.DefaultView.getEdgesData();

Функции в js которые возвращают сериализованные данные элементов:

function getNodesData(){
return JSON.stringify(nodes.get());
};

function getEdgesData(){
return JSON.stringify(edges.get());
};

function getNodesDataExtra(){
return JSON.stringify(nodesExtraData);
};

По полученным данным на стороне 1С для каждого узла создадим элемент справочника «Узлы блок-схемы», в которой установим связь с источником узла — номенклатурой или операцией, и сохраним координаты на схеме:

Сохранение схемы

Отдельно в регистре связей сохраним ребра:

Сохранение схемы

Теперь при открытии блок-схемы мы можем получить сохраненные элементы из данных базы и восстановить все узлы и ребра с учетом сохраненных координат:

ТаблицаУзлов = ТаблицаУзловБлокСхемы(Объект.Ссылка);
ТаблицаСвязей = ТаблицаСвязейБлокСхемы(Объект.Ссылка);

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

Результат работ 

Результат работ 

Оргдиаграмма

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

Оргдиаграмма

Увы, при тестах табличное поле не удовлетворяло требованиям — скудные возможности дизайна, и помимо этого, при работе в браузере отображение было некорректным. Поэтому было принято решение о поиске другого инструмента, и в итоге за основу была взята библиотека Pixi.js.

Библиотека Pixi.js представляет довольно гибкий функционал по выводу изображения, позволяя работать с отдельными его элементами-примитивами и обрабатывать события связанные с ними. В то же время эти элементы можно объединить в контейнеры, их также можно поместить в контейнеры более высокого уровня и т. д. То есть структура элементов представляет из себя некую «матрешку».

Инициализация исходных данных

Чтобы сформировать блоки на схеме и вывести на них нужную информацию, необходимо передать данные о подразделениях внутрь js. Для этого мы в коде 1С получаем запросом все нужные сведения и создаем строку json типа массив структур, со свойствами конкретного подразделения.

Например:

[{"UUID": "d4561350-fbc9-11ea-8101-005056960659",
"description": "Административный департамент",
"employee": "Морозов Антон",
"position": "Администр. директор",
"color": "0xFF0000"
}
…]

В тексте скрипта инициализируем переменную ODItems как массив:

var ODItems = [];

И далее из 1С вызываем метод initODItems, который преобразует нашу строку json в массив:

МассивПодразделенийJSON = ПолучитьМассивПодразделенийJSON();
Форма.Документ.DefaultView.initODItems(МассивПодразделенийJSON);

function initODItems(metaStructArray){
ODItems = JSON.parse(metaStructArray);
}

Отрисовка боксика

Поговорим о базовой отрисовке объектов.

Основу вывода графики в библиотеке Pixi составляют т. н. контейнеры — это объекты класса container и sprite. Сначала инициализируем корневой контейнер, после его можно использовать как матрешку, добавляя в него новые элементы того же типа.

После этого нужно отрисовать простые объекты, используя методы класса Graphics. На основе него с отрендеренными примитивами можно сгенерировать текстуру. Её нужно передать в объект класса Sprite, который является наследуемым классом от Container. И этот спрайт можно поместить в корневой контейнер.

Пример отрисовки прямоугольника:

const app = new PIXI.Application();
const rootContainer = new PIXI.Container();
app.stage.addChild(rootContainer);
const gr = new PIXI.Graphics();

//рисуем зеленый прямоугольник с черной границей
gr.lineStyle(1, 0x000000, 1);
gr.beginFill(0x00ff00, 1);
gr.drawRect(0, 0, 50, 50);

const txtr = app.renderer.generateTexture(gr);
var node = new PIXI.Sprite(txtr);
node.position = new PIXI.Point(x, y);
container.addChild(node);

Пояснения:

  • lineStyle/beginFill — настройки стиля линии и заполнения для использования в последующих вызовах объекта Graphics;
  • drawRect — отрисовка прямоугольника;
  • addChild — метод добавляет элемент в контейнер.

Готовый пример:

<html lang="ru">
<head>
<meta charset="utf-8" />
<script src="https://cdn.jsdelivr.net/npm/pixi.js-legacy@5.3.7/dist/pixi-legacy.min.js"></script>
</head>
<body>
<div id="canvasConvainer" style="width: 100%; height: 100%;"></div>
<script type="text/javascript">
           
//::: Параметры боксика
var pntPars = {   width:      150, heigth: 120 };
           
//::: объект HTML документа с Id = "canvasConvainer" помещается в одноименную переменную
const canvasParent = document.getElementById("canvasConvainer");
const gr = new PIXI.Graphics();               
const style = new PIXI.TextStyle({
fontFamily: 'Arial',
fontSize: 12,
wordWrap: true,
wordWrapWidth: pntPars.width - 20,
breakWords: true,
});
const app = new PIXI.Application({transparent: true});
canvasParent.appendChild(app.view);
           
const rootContainer = new PIXI.Container();
app.stage.addChild(rootContainer);
           
//Загрузка тестовых данных
ODItems = getTestODItems();
drawOrgDiagramm();
           
function drawOrgDiagramm(){
PosY = 0; PosX = 0;
for (var itemInd = 0 ; itemInd <= ODItems.length - 1; itemInd++) {
Item = ODItems[itemInd];
PosY += 120;     
drawNode(PosX, PosY, rootContainer, Item, false);   
}
}// drawOrgDiagramm()

//////////////////////////////////////////////////
//::: Отрисовка блока
//::: container - родительский контейнер, в который нужно поместить спрайт узла
//::: meta - контент\тематические данные узла
function drawNode(x, y, container, meta) {

//::: цвет окантовки блока
brdColor = meta.color;

//::: цвет фона блока
let bgColor = 0xffffff;

//отрисовка раскрашенного прямоугольника со скругленными углами
gr.clear();
gr.lineStyle(1, brdColor, 1);
gr.beginFill(brdColor, 1);
gr.drawRoundedRect(0, 0, pntPars.width, pntPars.heigth, 5);
gr.beginFill(bgColor, 1);
gr.drawRect(0, 5, pntPars.width, pntPars.heigth - 10);
gr.endFill();

const txtr = app.renderer.generateTexture(gr);
var node = new PIXI.Sprite(txtr);
node.position = new PIXI.Point(x, y);

//вывод наименования подразделения и руководителя
var newStyle = style.clone();
newStyle.fontWeight = 'bold';
//отрисовка раскрашенного прямоугольника со скругленными углами
var txtTitle = new PIXI.Text(meta.description, newStyle);
txtTitle.position = new PIXI.Point(5, 10);
node.addChild(txtTitle);

var txtAcc = new PIXI.Text(meta.employee, style.clone());
txtAcc.position = new PIXI.Point(5, 60);
node.addChild(txtAcc);

container.addChild(node);

return node;

}// drawNode()

     
function getTestODItems(){

return [
{
"UUID": "fc6d1f81-f9b2-11ea-8101-005056960659",
"description": "Департамент Межотраслевых Решений",
"employee": "Шатунов Алексей",
"position": "Директор дирекции",
"color": "0x009646"
},
{
"UUID": "3aeefc85-f9b3-11ea-8101-005056960659",
"description": "Генеральный директор",
"employee": "Петросян Вячеслав",
"position": "Генеральный директор",
"color": "0x009646"
},
{
"UUID": "d4561350-fbc9-11ea-8101-005056960659",
"description": "Административный департамент",
"employee": "Морозов Антон",
"position": "Администр. директор",
"color": "0xFF0000"
},
{
"UUID": "09159a3c-0a29-11eb-8101-005056960659",
"description": "Группа продаж и аккаунтинга",
"employee": "Ягодина Оксана",
"position": "Начальник отдела",
"color": "0x009646"
}    
];
}

</script>
</body>
</html> 

Результат вывода примера выше:

Отрисовка боксика

По тому же принципу можно отрисовать любые необходимые элементы — линии, индикаторы, кнопки и т. д.

Пример — горизонтальная линия:

function drawHorLine(x, y, length, container){
  const line = new PIXI.Graphics();
  line.lineStyle(1, 0x949799, 1);
  line.moveTo(0, 0);
  line.lineTo(length, 0);
  line.position = new PIXI.Point(x, y);
  container.addChild(line);
}// drawHorLine()

Пример — отрисовка кружка с плюсом и назначение событие нажатия

При создании нового спрайта можно указать для него свою функцию для обработки событий:

gr.lineStyle(1, 0x949799, 1);
gr.beginFill(0xffffff, 1);
gr.drawCircle(0, 0, 6);
gr.endFill();

const txtrCirc2 = app.renderer.generateTexture(gr);
var tCircle = new PIXI.Sprite(txtrCirc2);
tCircle.position = new PIXI.Point(2, pntPars.heigth - 1);

var plus = drawPlus();
plus.position = new PIXI.Point(6,4);
tCircle.addChild(plus);

node.addChild(tCircle);

tCircle.interactive = interactive;
tCircle.on('pointerdown', onClickExpandBranch);

 

function drawPlus(){
var btn = new PIXI.Graphics();
btn.lineStyle(1, 0x949799);
btn.moveTo(-3, 2);
btn.lineTo(3, 2);
btn.moveTo(0, -1);
btn.lineTo(0, 5);
return btn;
}

function onClickExpandBranch() {
curNode = ODItems.find(element => element.sprite == this.parent);
expandedItemsUUID.push(curNode.UUID);

}

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

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

Одно из требований к оргдиаграмме — возможность вывода контекстного меню с произвольным набором команд, в зависимости от нажатого объекта. Мы реализовали такое меню тоже средствами Pixi.

Сначала при формировании спрайта боксика формируем «спрайт-кнопку», для которой назначаем событие нажатия левой кнопки мыши pointerdown:

var  nodeOptions = new PIXI.Text("☰", style.clone());
nodeOptions.position = new PIXI.Point(pntPars.width - 15, pntPars.heigth - 25);
node.addChild(nodeOptions);
nodeOptions.interactive = interactive;
nodeOptions.on('pointerdown', onClickOptions);

Далее при обработке события формируем прямоугольник со списком команд, и назначаем обработчик события для них:

function onClickOptions() {
  contextMenuClick = true;
  curNode = ODItems.find(element => element.sprite == this.parent);
  if (curNode != undefined)
       selectedMenuData = {UUID: curNode.UUID, UnitUUID: curNode.UnitUUID, emplUUID: curNode.emplUUID, isEmployee: curNode.isEmployee};
     
  //Список команд контекстного меню
  comandsArr = [];
  comandsArr.push("Открыть корпоративную книгу");
  comandsArr.push("Открыть книгу подразделения");
  comandsArr.push("Перейти к поквартальному просмотру");
  comandsArr.push("Открыть АРМ подразделения");
  comandsArr.push("Открыть подразделение");           
     
  curRect = this.parent;
  contextX = curRect.x + curRect.width - 5;
  contextY = curRect.y + curRect.height - 8;
     
  //Отрисовываем фон контекстного меню
  gr.clear();
  gr.lineStyle(1, 0x000000, 1);
  gr.beginFill(0xffffff, 1);
  gr.drawRect(0, 0, pntPars.contextMenuWidth, 21 * comandsArr.length + 10);
  gr.endFill();

  txtr = app.renderer.generateTexture(gr);
contextmenu = new PIXI.Sprite(txtr);
contextmenu.position = new PIXI.Point(pntPars.width -10, pntPars.heigth - 18);

//::: разделитель
const line = new PIXI.Graphics();
line.lineStyle(1);
line.moveTo(0, 0);
line.lineTo(pntPars.contextMenuWidth - 20, 0);
//:::
line.position = new PIXI.Point(10,  22 * (comandsArr.length - 1));
contextmenu.addChild(line);

  commandY = 10;

//Добавляем пункты в меню
for (command of comandsArr){
newStyle = style.clone();
newStyle.wordWrapWidth = pntPars.contextMenuWidth - 10;
txtObj = new PIXI.Text(command, newStyle);
txtObj.position = new PIXI.Point(10, commandY);
contextmenu.addChild(txtObj);
txtObj.interactive = interactive;
txtObj.on('pointerdown', onClickContextMenu);
commandY += 20;
}

//::: добавление меню к Sprite выбранного box-ика
this.parent.addChild(contextmenu);

app.ticker.update(performance.now());
contextMenuIsOpen = true;
clickProcessed = true;
}

 

function onClickContextMenu(){

curNode = ODItems.find(element => element.sprite == this.parent.parent);

//После выбора пункта - уничтожаем спрайт контекстного меню
contextmenu.destroy(true);
contextMenuIsOpen = false;

app.ticker.update(performance.now());

selectedMenuData = {UUID: curNode.UUID, UnitUUID: curNode.UnitUUID, emplUUID: curNode.emplUUID, menuItem: this._text, isEmployee: curNode.isEmployee};

  //Вызываем событие “ПриНажатии” элемента HTMLДокумент для отлавливания в 1С
let evt = new Event("onclick");
canvasParent.dispatchEvent(evt);

}

Примечание. selectedMenuData — глобальная переменная, в которую записывается служебная информация. Она будет считана в коде 1С в событии HTMLДокументПриНажатии.

В результате получилось такое меню:

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

Поиск узла дерева

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

Для этой цели мы добавили функцию в js findText, которая вызывается с формы 1С при поиске в специальном поле:

function findText(textStr) {
textStr = textStr.toLowerCase();
//Поиск по наименованию подразделения и руководителя
searchArr = ODItems.filter(element => element.description.toLowerCase().includes(textStr) || element.employee.toLowerCase().includes(textStr));
     
if (searchArr.length > 0){
lastNodeMetaGlobal = ODItems.find(element => element.sprite == curNodeSpriteGlobal);
highlightNode(0);
}
return searchArr.length;
}

function highlightNode(nodeIndex) {

if (nodeIndex >= searchArr.length){
return;
}

let curNode = searchArr[nodeIndex];
curParent = ODItems.find(element => element.code == curNode.parentCode);
isExpanded = !curParent || expandedItemsUUID.indexOf(curParent.UUID) != -1;
if (!isExpanded){
isLeafNode = true;
while (isLeafNode){
if (rootUUID == "" || curParent.parUUID != ""){
//Добавляем элемент в массив развернутых
setODItemAsExpanded(curParent.UUID);
}
parentIndex = ODItems.findIndex(element => element.code == curParent.parentCode);
isLeafNode = parentIndex != -1;
if (isLeafNode){
curParent = ODItems[parentIndex];
}
}
//Перерисовываем орг диаграмму
drawOrgDiagramm();
}

//Отрисовываем подсвеченый боксик
updateHighlightNode(curNode.sprite, true);

}

Пару слов о тикере

Тикер — объект приложения PIXI, это таймер на основе которого происходит рендеринг изображения.

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

Поэтому мы пришли к варианту с ручным управлением тикером.

В самом начале при инициализации глобальной переменной приложения мы останавливаем тикер:

const  canvasParent = document.getElementById("canvasConvainer");
const app = new PIXI.Application();
canvasParent.appendChild(app.view);
const rootContainer = new PIXI.Container();
app.stage.addChild(rootContainer);
app.ticker.stop();

И далее во всех моментах перерисовки схемы вызываем метод update тикера:

app.ticker.update();

В результате получилась такая оргдиаграмма:

Оргдиаграмма

Заключение

В заключении можно сказать несколько важных вещей:

  • 1С по-настоящему крутая среда разработки и если вдруг не хватило текущих средств визуализации, то 1С как невероятно дружелюбная и открытая среда разработки может приютить в своих недрах и библиотеки JavaScript.
  • Если вы хотите использовать широкие возможности 1С, то следует изучать смежные языки и технологии, ну и английский язык не будет лишним.
  • Внимательно относитесь к каждому пункту требований и аккуратно декомпозируйте их, чтобы не попасть в ситуацию неверно выбранного инструмента и необходимости переделки.

Авторы статьи

Борисенко Станислав
Борисенко Станислав
Андрей Черанев
Андрей Черанев
Есть вопросы по статье? Задайте их нам!
info-big
Рассылка «Новости компании»: узнавайте о новых продуктах, услугах и спецпредложениях
Отправляя эту форму, Вы соглашаетесь с Политикой конфиденциальности и даете согласие на обработку персональных данных компанией «1С-Рарус»

Остались вопросы?
Нужна консультация?
Свяжитесь с нами!