Описанная ниже инструкция по созданию плагина представлена также в виде видео — .

Подготовка окружения и запуск проекта

Перейдите на репозиторий GitHub, в нем загружена ветка reference-universal — стартовая точка для разработки с нуля (дефолтные версии всех модулей).

Создайте клон репозитория через интерфейс редактора.

После загрузки проекта запустите npm install.

Перед запуском dev-сервера надо убедиться, что в папке prebuild располагается сборка ядра портала.

Примечание — сборку ядра портала можно скачать в виде zip-архива в настройках портала, нажав на кнопку «Скачать Frontend». Далее этот архив нужно разархивировать в папку prebuild.

Перейдите в package.json. Запустите dev-сервер при помощи скрипта start:dev.

Примечание — версия node в вашей системе должна соответствовать указанной в package.json, иначе возможны ошибки при запуске скриптов.

Примечание — цикл разработки плагина осуществляется при помощи скриптов. Основные:

  • start:dev — запускает портал в режиме разработчика, в этом режиме код можно отлаживать;
  • build:prod — собирает плагин для продакшена;
  • build:plugin — берет сборку плагина и заполненный файл манифеста, создает дистрибутив плагина, готовый для загрузки в портал.

Откройте пустой отчет на localhost:7000, перейдите в редактор компонента отчета.

В списке визуализаций появится плитка DEV — это ваш будущий плагин. Перетащите ее в область «Компонент отображения не задан».

Визуализации пока нет, так как она ещё без функционала. В боковой части, в настройках компонента, по умолчанию добавлены разделы «Общие настройки», «Данные», «Инфо», «Рамка компонента». Эти разделы характерны для всех визуализаций.

Добавленный в отчет компонент DEV необходимо сохранить, а затем сохранить и сам отчет. Все изменения, вносимые в плагин, будут видны после обновления localhost:7000.

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

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

Раскройте src/modules и зайдите в модуль CustomChart. Там подключён CommandChartAdapter, в который вынесена вся логика по запросу данных. Сам рендеринг вынесен в Content.js, куда после dataAdaptor приходят все нужные пропсы.

Для проверки в Content.js попробуйте, например, вернуть «HELLO WORLD» через return:

  return (
    <div>HELLO WORLD</div>
  );
}

Обновите localhost:7000 — произойдет пересборка проекта плагина. В области предпросмотра компонента отобразится «HELLO WORLD».

Добавление пользовательских настроек и кастомизация интерфейса

Для изменения заголовка компонента и названия типа компонента перейдите в defaultConfig.json и измените значения в title и chartType:

{
  "title": "Example Chart",
  "subtitle": "",
  "chartType": "exampleChart",
  "showtitle": true,
  "noexport": false,
  "nocopy": false,
  "filterMode": false,
  "bigNumberClasses": [],
  "margin": {
    "l": 15,
    "r": 15,
    "t": 15,
    "b": 0,
    "pad": 0
  },

Обновите localhost:7000 — в разделе «Общие настройки» отобразится введенный заголовок.

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

Перейдите в Content.js, из приложенных заготовок в самом файле перенесите представленный кусок кода внутрь return (раньше там был «HELLO WORLD»):

  return (

    <div style={{
      background: config.backgroundColor,
      height: '100%',
      overflow: 'auto'
    }}>
      <pre>{JSON.stringify(plotData, 0, 2)}</pre>
    </div>

  );
}

Обновите localhost:7000.

Пояснение к коду: этот фрагмент заменяет статический текст на отображение сырых данных (plotData) в виде JSON. Теперь при изменении полей в полках компонента будут меняться и отображаемые данные, как показано на рисунках ниже.

Далее попробуйте изменить CustomReducers и CustomSettings (они работают в связке) для добавления настройки изменения цвета фона компонента.

Перейдите в CustomSettings/MainOptions.js. Добавьте следующий код из заготовок файла MainOptions.js:

   <SettingsItem type='inline' title='Цвет фона'>
            <SettingsColorPicker
              float='right'
              color={config.backgroundColor === 'transparent' ? undefined : config.backgroundColor}
              onChange={(value) => changeChart('setBackgroundColor', { value })}
            />
          </SettingsItem>

Со стороны CustomReducers необходимо добавить обработчик команды setBackgroundColor. Для этого перейдите в CustomReducers/changeCustomChartReducer.js и добавьте следующий код из заготовок самого файла:

if (action.command === 'setBackgroundColor') {
             configNew.backgroundColor = action.settings.value;
            }

Пояснение к коду: этот код позволяет менять цвет фона компонента. Когда вы выбираете цвет в палитре, срабатывает обработчик onChange, который отправляет команду setBackgroundColor. Затем обработчик (редьюсер react) сохраняет выбранный цвет в настройках, и после обновления компонента фон меняется на новый цвет.

Обновите localhost:7000 — в разделе «Общие настройки» появится добавленное поле «Цвет фона».

Далее попробуйте настроить панели полок компонента через CustomAxes. Через него вы можете подключать новые полки, настраивать состав пилюль на полках, а также их иконки. Попробуйте настроить иконку полки «Серии». Перейдите в CustomAxes/index.js, найдите рендер, связанный с icons. На основе копирования categories добавьте series, не забудьте изменить className и title для серий:

export function renderAxeIcon(props) {
  const { axe, field, config, componentType, axisNames, HsMuiFontIcon, HsMuiSvgIcon } = props;

  const icons = {
    categories: (
      <HsMuiFontIcon className='fa fa-bars' title={axisNames.categories} style={{ transform: 'rotate(90deg)' }} />
    ),
    series: (
      <HsMuiFontIcon className='fa fa-square' title={axisNames.series} style={{ transform: 'rotate(90deg)' }} />
    ),
    values: <HsMuiFontIcon className='fa fa-bars' title={axisNames.values} />,
  };

  return icons[axe.type];
}

Обновите localhost:7000. Напротив полки «Серии» отобразится иконка квадрата, а при наведении — заголовок полки.

Построение собственной визуализации (на примере гистограммы)

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

Для этого в dataAdaptor.js внутри CustomChart необходимо заменить this.plotData = structuredClone(this.aggregated); на следующее:

const data = this.aggregated?.[0];
const total = data?.length || 0;
if (!total) return [];

const letterCount = (item) => {
  return (item?.values + '').length;
};

const countsByLen = new Map();
for (const item of data) {
  const len = letterCount(item);
  countsByLen.set(len, (countsByLen.get(len) || 0) + 1);
}

this.plotData = Array.from(countsByLen.entries())
  .sort((a, b) => a[0] - b[0])
  .map(([len, count]) => ({
    len,
    count,
    percent: (count / total) * 100,
  }));

Представленный выше код уже есть в файле dataAdaptor.js в качестве альтернативного варианта (закомментирован).

Пояснение к коду: этот код преобразует исходные данные aggregated в массив, где для каждой длины строки (количество символов в значении) подсчитывается количество таких значений и их процент от общего числа. Полученный массив содержит поля len, count и percent, которые далее используются для построения гистограммы.

Далее в Content.js необходимо изменить часть, относящуюся к рендерингу. Будет отрисована горизонтальная гистограмма на основе данных, которые приходят через plotData из dataAdaptor.js. Нужный для этого код уже есть в файле Content.js как заготовка – вы можете просто скопировать его оттуда, заменив предыдущий return:

  return (
        <div
      style={{
        background: config.backgroundColor,
        height: '100%',
        overflow: 'auto',
        padding: 10,
      }}
    >
      <div style={{ display: 'grid', gap: 10 }}>
        {plotData.map((r) => (
          <div
            key={r.len}
            style={{
              display: 'grid',
              gridTemplateColumns: '20px 1fr 72px',
              gap: 10,
              alignItems: 'center',
            }}
          >
            <div style={{ whiteSpace: 'nowrap' }}>{r.len}</div>

            <div
              style={{
                height: 12,
                background: '#ffffff',
                borderRadius: 12,
                overflow: 'hidden',
              }}
            >
              <div
                style={{
                  height: '100%',
                  width: `${Math.round(r.percent * 10) / 10}%`,
                  background: '#4c84ff',
                  borderRadius: 12,
                }}
              />
            </div>

            <div style={{ textAlign: 'right', fontVariantNumeric: 'tabular-nums' }}>
              {`${(Math.round(r.percent * 10) / 10).toFixed(1)}%`}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

Обновите localhost:7000. Получится следующая визуализация:

При изменении в настройках визуализации полей внутри полок происходит изменение самой визуализации, как показано на рисунках ниже.

Рассмотрены все этапы, которые могут возникнуть при создании плагина. Плагин готов, далее необходимо собрать дистрибутив.

Сборка дистрибутива и установка плагина в портал

Перейдите в файл package.json и запустите build:prod.

После окончания процесса появится папка build, внутри которой будет располагаться файл plugin.js. Наименование файла перед сборкой дистрибутива изменять не нужно.

Также необходимо добавить манифест. Добавьте в папку build файл манифеста manifest.json. Скопируйте в него содержимое файла manifest.example.js (это заранее созданный пример манифеста). В скопированном тексте манифеста необходимо изменить следующее:

  • name — название, которое будет выводиться в интерфейсе;
  • apiVersion — актуальная версия API;
  • description — описание.

Теперь можно запускать сборку дистрибутива: перейдите в файл package.json и запустите build:plugin.

Появится example.tar.gz — необходимый архив плагина.

Примечание — загрузка плагина на бэкенд происходит через админ-интерфейс в виде архива .tar.gz. После загрузки сервер распаковывает архив и выполняет проверку: наличие всех компонентов, корректность манифеста, целостность данных, а также отсутствие конфликтов имён с уже установленными визуализациями. Если все проверки пройдены успешно, ассеты плагина перемещаются в отдельное пространство для статических ресурсов. Здесь они обслуживаются независимо от основного приложения. Каждому плагину присваивается версия для контроля изменений, а доступ к управлению регулируется системой прав: разные пользователи могут иметь права только на загрузку плагинов или только на их активацию в продакшн-среде. Только после полного прохождения этого процесса плагин считается установленным и готовым к использованию.

Схематично процесс выглядит так:

Теперь необходимо добавить данный архив в портал, подробнее про добавление пользовательских компонентов в разделе «Компоненты Аналитического портала».

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

Пример создания плагина прогресс-бара

Пример создания плагина прогресс-бара

Что понадобится:

Запуск проекта

Начните с шаблона плагина. Скачайте ZIP‑архив из ветки main репозитория Modus BI, распакуйте его и откройте папку custom-chart в редакторе кода.

Установите зависимости, используя npm:

npm install

Скачайте ядро проекта. В корне создайте папку prebuild и распакуйте туда содержимое ядра.

Результат должен выглядеть так:

Запустите проект командой:

npm run start:dev

По умолчанию приложение откроется на http://localhost:7000.

При необходимости измените порт в файле ./config/index.js (свойство server_port).

Теперь в Modus BI откройте новый или существующий отчет и включите режим редактирования.

В списке визуализаций появится плитка DEV — это ваш будущий плагин. Перетащите ее в область «Компонент отображения не задан».

После этого появится пустой шаблон — это значит, плагин подключен и готов к разработке.

Настройка редактора компонента

Настройки редактора компонента подключаются в файле src/modules/CustomSettings/Settings.js и автоматически отображаются в боковой панели редактора. Modus BI предоставляет готовые стандартные секции для базовых настроек.

Этот набор можно расширять собственными элементами управления. Например, добавить настройку выбора цвета фона.

Сначала добавьте свойство для цвета фона в defaultConfig.json:

{

  "bgColor": null,

  // ... остальные настройки

}

Для удобного доступа к настройкам добавьте файл getDefaultConfig.js:

import defaultConfig from './defaultConfig.json';

export function getDefaultConfig() {

  return defaultConfig;

}

/

Теперь создайте компонент выбора цвета.

В папке src/modules/CustomSettings/CommonSettingItems/ добавьте файл ColorSettingItem.jsx.

const ColorSettingItem = ({ pluginImports, title, onChange, color }) => {

  const { SettingsItem, SettingsColorPicker } = pluginImports.components;

  return (

    <SettingsItem type='inline' title={title}>

      <SettingsColorPicker color={color} onChange={onChange} />

    </SettingsItem>

  );

};

Здесь мы используем встроенные компоненты API Modus BI (SettingsItem, SettingsColorPicker). Они реализуют стандартную логику отображения, а мы добавляем дополнительную настройку — выбор цвета фона.

Добавьте обработчик изменения цвета в файле src\modules\CustomReducers\changeCustomChartReducer.js. Этот файл нужен для того, чтобы сохранять и применять настройки диаграммы, относящиеся к плагину. Через API платформы разработчик реализует свою логику обработки команд (например, изменение цвета фона).

if (action.command === 'changeStyleBgColor') {

  configDraft.bgColor = action.settings.value;

}

Теперь в редакторе отчета появится опция «Цвет фона». Выбор цвета будет сразу применяться к плагину.

Подготовка структуры осей диаграммы

Прежде чем визуализация сможет работать с данными, нужно настроить для них структуру осей. В Modus BI структура осей (axes) настраивается через полки — области, куда пользователь перетаскивает поля из датасета. Для прогресс‑бара понадобятся:

  • категории (например, проекты или регионы);
  • значения (числовые показатели);
  • фильтры.

Пример структуры в defaultConfig.json:

{

  "axes": [

    { "name": "Значения", "title": "Значения", "type": "values", "fields": [] },

    { "name": "Категории", "title": "Категории", "type": "categories", "fields": [] },

    { "name": "filters", "title": "Фильтры", "type": "filters", "fields": [] }

  ]

}

В файле CustomAxes/index.js настройте сортировку:

export function sortAxes(props) {

  const { config } = props;

  return config.axes;

}

Кроме сортировки важно определить, как будут выглядеть и работать «пилюли» — визуальные элементы, которые отображают поля, добавленные на полки.

Для этого в CustomAxes/index.js добавьте функции, которые управляют отображением и активностью элементов. Добавляя каждую функцию, мы заменяем ее дефолтную версию на свою кастомную и так кастомизируем диаграмму в целом.

// Настройка видимости пунктов меню

export function isVisibleAxeDragItemMenuOption(props) {

  const { optionName, field } = props;

  if (field.type === 'values') {

    return ['renderTitleInput', 'renderSortMenuItem', 'renderAggregationMenuItem'].includes(optionName);

  }

  if (field.type === 'categories') {

    return ['renderTitleInput', 'renderSortMenuItem'].includes(optionName);

  }

  if (field.type === 'filters') {

    return ['renderTitleInput', 'renderFilterSettings'].includes(optionName);

  }

  return false;

}

// Настройка визуальных элементов пилюли

export function isVisibleAxeDragItemElement(props) {

  const { elementName, field } = props;

  if (field.type === 'values') {

    return ['renderAggregationSelector', 'renderSortSelector'].includes(elementName);

  }

  if (field.type === 'categories') {

    return ['renderSortSelector'].includes(elementName);

  }

  return false;

}

Эти функции задают правила для разных типов полей:

  • для «Значений» отображается селектор агрегации и сортировки;
  • для «Категорий» — только сортировка;
  • для «Фильтров» — специфические настройки фильтрации.

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

Полный пример реализации доступен в репозитории Modus BI ConfigEditor.js.

Работа с данными

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

import CommonDataAdaptor from '../../duplicates/adaptors/CommonChartAdaptor/CommonDataAdaptor';

export default class DataAdaptor extends CommonDataAdaptor {}

Основной метод в CommonDataAdaptor — remapData. Он подготавливает данные из портала и приводит их к структурированному формату, удобному для отображения.

Создание визуализации

Чтобы плагин работал в системе, подключаем компонент в CustomChart.js — это точка входа для всех кастомных визуализаций.

Визуализация реализуется в компоненте ProgressBarChart.jsx. Он использует:

  • хук useStyles управляет цветами и отступами контейнера;
  • SCSS‑стили — задают структуру и оформление элементов;
  • Header.jsx — отдельный компонент для заголовка;
  • getDescription и getLocal — функции для описания и локализации;
  • LoadProgress — встроенный компонент для отображения загрузки данных.

Финальный вариант ProgressBarChart.jsx:

const ProgressBarChart = React.forwardRef(({ config, theme, componentId, pluginImports }, ref) => {

  const { LoadProgress } = pluginImports.components;

  const styles = useStyles(config, theme);

  const title = getLocal(config, 'title');

  const showHeader = config?.showtitle || false;

  return (

    <div className='hsChartContainer ProgressBarChart' style={styles.container} ref={ref}>

      {showHeader && <Header title={title} />}

      <div className='componentBody ProgressBarChartBody'>

        <div className='componentContainer ProgressBarChartComponent' style={styles.component}>

          <div>

            <LinearProgress value={40}>

              ProgressBarChart

            </LinearProgress>

          </div>

        </div>

        <LoadProgress />

      </div>

    </div>

  );

});

Полный код компонентов и стилей смотрите в репозитории Modus BI.

Подключение реальных данных

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

Адаптер (remapData) — это функция, которая преобразует данные из системы Modus BI в формат, удобный для визуализации. Когда пользователь добавляет поля на полки (axes), данные автоматически проходят через метод remapData. В простейшем случае он возвращает числовое значение, которое мы передаем в компонент прогресс‑бара

Пример работы с данными:

import DataAdaptor from '../dataAdaptor';

export function useLoadData({ config, loadDatas, cacheId, data, editorActive, componentId, reloadDatas, inEditor }) {

  const [loaded, setLoaded] = useState(false);

  const [loading, setLoading] = useState(false);

  const [dataAdaptor, setDataAdaptor] = useState(null);

  const datasetId = getDatasetId(config);

  const queryObjects = DataAdaptor.getQueryObjects(config);

  const stringQueryObjects = JSON.stringify(queryObjects);

  useEffect(() => {

    if (!datasetId) return;

    runLoadData();

  }, [datasetId, cacheId, stringQueryObjects, data, editorActive, inEditor]);

  const runLoadData = () => {

    if (!datasetId) return;

    if (!data && cacheId) {

      setLoading(true);

      loadDatas(datasetId, null, config.filters, queryObjects, { editor: editorActive, componentId })

        .then((res) => {

          setLoaded(true);

          const adapter = new DataAdaptor(res.data, config, null, cacheId);

          setDataAdaptor(adapter);

        })

        .finally(() => {

          setLoading(false);

        });

    } else if (data && !data.fetching) {

        setDataAdaptor(new DataAdaptor(data.data, config, null, cacheId));

    }

  };

  return { dataAdaptor, loadedData: loaded, loadingData: loading, };

}

Теперь подключим хук в ProgressBarChart и передадим полученное значение в LinearProgress:

import { useLoadData } from './useLoadData';

const ProgressBarChart = React.forwardRef(({ config, datas, theme, pluginImports }, ref) => {

  const { LoadProgress } = pluginImports.components;

  const styles = useStyles(config, theme);

  const { dataAdaptor, loadingData } = useLoadData({ config, cacheId, loadDatas, reloadDatas, data, componentId, editorActive, inEditor});

  let list = [];

  const valuesAxe = _.find(config.axes, ['type', 'values']);

  if (valuesAxe?.fields.length > 1) {

    list = dataAdaptor?.plotData.map(data => ({

        id: data['ID0'],

        category: data.categories || 'Без категории',

        value: getPercentage(data['values0'], data['values1'])

    })) || [];

  }

  return (

    <div className='hsChartContainer ProgressBarChart' style={styles.container} ref={ref}>

      <div className='componentBody ProgressBarChartBody'>

        <div className='componentContainer ProgressBarChartComponent' style={styles.component}>

          <div className='ProgressBarChartList'>

              {list.length === 0 ? <b>Нет данных</b> : null}

              {list.map((item) => (

                <LinearProgress key={item.id} value={item.value} containerColor={'white'} linearColor={'red'}>

                  {item.category}

                </LinearProgress>

              ))}

            </div>

        </div>

        <LoadProgress />

      </div>

    </div>

  );

});

В итоге, получили плагин, который работает с данными.

Проверка результата

После всех шагов убедитесь, что:

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

Если все это выполняется — плагин готов к использованию.
Вы создали плагин прогресс-бара для Modus BI, который:

  1. Настраивается через редактор (цвет фона, заголовки)
  2. Работает с данными через стандартные механизмы платформы
  3. Отображает данные в виде прогресс-баров
  4. Подключается к реальным данным из портала

Готовый код доступен в ветке tutorial-progress репозитория Modus BI. Этот пример можно использовать как основу для создания других типов визуализаций.

Связи контента