Skip to main content
Пример Dynamic графика Динамические графики позволяют создавать кастомные визуализации с использованием библиотеки Apache ECharts.

Обзор

Dynamic Chart предоставляет полный контроль над визуализацией через JavaScript-код, который преобразует данные куба в конфигурацию ECharts.
Dynamic Chart подходит для случаев, когда стандартные типы графиков не покрывают требования визуализации.

Поля

type
"dynamic_chart"
required
Тип графика
queries
Record<string, CubeQuery>
required
Именованные Cube.js запросы. Каждый ключ — произвольное имя запроса, значение — стандартный объект CubeQuery.
queries:
  main:
    measures:
      - sales.revenue
      - sales.quantity
    dimensions:
      - products.category
  trends:
    measures:
      - sales.revenue
    timeDimensions:
      - dimension: sales.created_at
        granularity: month
transform_code
string
required
JavaScript-код функции, которая преобразует результаты Cube.js запросов в объект конфигурации ECharts.Код должен быть строкой вида function(queries) { ... }, где queries — объект типа Record<string, FormattedResultSet>. Ключи соответствуют именам запросов из поля queries.
Функция должна возвращать валидный объект конфигурации ECharts. Используйте return в конце.
compare_period_enabled
boolean
default:"false"
Включить сравнение периодов. При включении в data добавляются данные за предыдущий период.

Примеры

Горизонтальная столбчатая диаграмма с порогом

Пример: процент скидки по локациям с выделением значений выше порога.
type: dynamic_chart
compare_period_enabled: false
queries:
  locations:
    measures:
      - sales.revenue
      - sales.discount_sum
    dimensions:
      - sales.department
transform_code: |
  function(queries) {
    const data = queries.locations.data;
    
    if (data.length === 0) {
      return { title: { text: 'No data available' } };
    }
    
    const chartData = data.map(row => {
      const revenue = row['sales.revenue'].value || 0;
      const discount = row['sales.discount_sum'].value || 0;
      const discountPercent = revenue > 0 ? (discount / revenue * 100) : 0;
      
      return {
        name: row['sales.department'].formattedValue || 'Unknown',
        value: discountPercent,
        discountFormatted: discountPercent.toFixed(2) + '%',
        revenueFormatted: row['sales.revenue'].formattedValue,
        itemStyle: {
          color: discountPercent > 50 ? '#e74c3c' : '#27ae60'
        }
      };
    }).sort((a, b) => b.value - a.value);
    
    return {
      tooltip: {
        trigger: 'axis',
        axisPointer: { type: 'shadow' },
        formatter: function(params) {
          const d = params[0].data;
          return d.name + '<br/>' +
            'Процент скидки: <b>' + d.discountFormatted + '</b><br/>' +
            'Выручка: ' + d.revenueFormatted;
        }
      },
      grid: {
        left: '15%',
        right: '5%',
        bottom: '15%',
        containLabel: true
      },
      xAxis: {
        type: 'value',
        name: 'Процент скидки (%)',
        axisLabel: { formatter: '{value}%' }
      },
      yAxis: {
        type: 'category',
        data: chartData.map(d => d.name)
      },
      series: [{
        type: 'bar',
        data: chartData,
        label: {
          show: true,
          position: 'right',
          formatter: function(params) {
            return params.data.discountFormatted;
          }
        },
        markLine: {
          data: [
            { xAxis: 50, lineStyle: { color: '#f39c12', type: 'dashed', width: 2 }, label: { formatter: '50% порог' } }
          ]
        }
      }]
    };
  }

KPI-плитка (только текст)

Используйте ECharts title для отображения числового KPI без графических элементов.
type: dynamic_chart
compare_period_enabled: false
queries:
  locations:
    measures:
      - sales.revenue
      - sales.discount_sum
    dimensions:
      - sales.department
transform_code: |
  function(queries) {
    const data = queries.locations.data;
    
    if (data.length === 0) {
      return { title: { text: 'No data available', left: 'center', top: 'center' } };
    }
    
    const locationsWithDiscount = data.map(row => {
      const revenue = row['sales.revenue'].value || 0;
      const discount = row['sales.discount_sum'].value || 0;
      const discountPercent = revenue > 0 ? (discount / revenue * 100) : 0;
      return { discountPercent };
    });
    
    const highDiscountCount = locationsWithDiscount.filter(l => l.discountPercent > 50).length;
    const totalCount = locationsWithDiscount.length;
    
    return {
      title: {
        text: highDiscountCount + ' из ' + totalCount,
        subtext: 'Локаций со скидкой >50%',
        left: 'center',
        top: 'center',
        textStyle: {
          fontSize: 48,
          fontWeight: 'bold',
          color: highDiscountCount > 0 ? '#e74c3c' : '#27ae60'
        },
        subtextStyle: {
          fontSize: 18,
          color: '#666'
        }
      }
    };
  }

Линейный график с несколькими запросами

Пример использования нескольких именованных запросов.
type: dynamic_chart
compare_period_enabled: false
queries:
  revenue:
    measures:
      - sales.revenue
    timeDimensions:
      - dimension: sales.created_at
        granularity: month
  orders:
    measures:
      - orders.count
    timeDimensions:
      - dimension: orders.created_at
        granularity: month
transform_code: |
  function(queries) {
    const revenueData = queries.revenue.data;
    const ordersData = queries.orders.data;
    
    if (revenueData.length === 0) {
      return { title: { text: 'No data available' } };
    }
    
    const months = revenueData.map(d => d['sales.created_at'].formattedValue);
    const revenue = revenueData.map(d => d['sales.revenue'].value);
    const orders = ordersData.map(d => d['orders.count'].value);
    
    return {
      tooltip: { trigger: 'axis' },
      legend: { data: ['Выручка', 'Заказы'] },
      xAxis: {
        type: 'category',
        data: months
      },
      yAxis: [
        { type: 'value', name: 'Выручка' },
        { type: 'value', name: 'Заказы' }
      ],
      series: [
        {
          name: 'Выручка',
          type: 'line',
          data: revenue,
          smooth: true
        },
        {
          name: 'Заказы',
          type: 'line',
          yAxisIndex: 1,
          data: orders,
          smooth: true
        }
      ]
    };
  }

Радарная диаграмма

type: dynamic_chart
compare_period_enabled: false
queries:
  metrics:
    measures:
      - sales.revenue
      - sales.quantity
      - sales.margin
    dimensions:
      - products.category
transform_code: |
  function(queries) {
    const data = queries.metrics.data;
    
    if (data.length === 0) {
      return { title: { text: 'No data available' } };
    }
    
    const categories = data.map(d => d['products.category'].formattedValue);
    const maxRevenue = Math.max(...data.map(d => d['sales.revenue'].value));
    const maxQuantity = Math.max(...data.map(d => d['sales.quantity'].value));
    const maxMargin = Math.max(...data.map(d => d['sales.margin'].value));
    
    return {
      tooltip: {},
      radar: {
        indicator: [
          { name: 'Выручка', max: maxRevenue },
          { name: 'Количество', max: maxQuantity },
          { name: 'Маржа', max: maxMargin }
        ]
      },
      series: [{
        type: 'radar',
        data: categories.map((cat, i) => ({
          name: cat,
          value: [
            data[i]['sales.revenue'].value,
            data[i]['sales.quantity'].value,
            data[i]['sales.margin'].value
          ]
        }))
      }]
    };
  }

Редактирование в JSON-режиме

При редактировании конфигурации в JSON-режиме значение transform_code должно быть однострочной строкой — переносы строк записываются как \n, табуляция как \t. Это требование формата JSON (многострочные строки не поддерживаются).
В YAML-режиме (transform_code: |) код можно писать в обычном многострочном виде. В JSON-режиме — только в одну строку с escape-символами.
{
  "type": "dynamic_chart",
  "compare_period_enabled": false,
  "queries": {
    "locations": {
      "measures": [
        "dm_order.dish_discount_sum_int",
        "dm_order.dish_sum_int"
      ],
      "dimensions": [
        "dm_order.department_name"
      ]
    }
  },
  "transform_code": "function(queries) {\n  const data = queries.locations.data;\n  \n  if (data.length === 0) {\n    return { title: { text: 'No data available', left: 'center', top: 'center' } };\n  }\n  \n  // Calculate discount percentage for each location\n  const locationsWithDiscount = data.map(row => {\n    const revenue = row['dm_order.dish_sum_int'].value || 0;\n    const afterDiscount = row['dm_order.dish_discount_sum_int'].value || 0;\n    const discount = revenue - afterDiscount;\n    const discountPercent = revenue > 0 ? (discount / revenue * 100) : 0;\n    return { discountPercent };\n  });\n  \n  // Count locations with >50% discount\n  const highDiscountCount = locationsWithDiscount.filter(l => l.discountPercent > 50).length;\n  const totalCount = locationsWithDiscount.length;\n  \n  return {\n    title: {\n      text: highDiscountCount + ' из ' + totalCount,\n      subtext: 'Локаций со скидкой >50%',\n      left: 'center',\n      top: 'center',\n      textStyle: {\n        fontSize: 48,\n        fontWeight: 'bold',\n        color: highDiscountCount > 0 ? '#e74c3c' : '#27ae60'\n      },\n      subtextStyle: {\n        fontSize: 18,\n        color: '#666'\n      }\n    }\n  };\n}"
}

Использование annotation для динамических подписей

Вместо хардкода подписей осей можно читать shortTitle из метаданных куба.
type: dynamic_chart
compare_period_enabled: false
queries:
  main:
    measures:
      - sales.revenue
    dimensions:
      - products.category
transform_code: |
  function(queries) {
    const result = queries.main;
    const data = result.data;
    const annotation = result.annotation;
    
    if (data.length === 0) {
      return { title: { text: 'No data available' } };
    }
    
    // Названия осей из метаданных куба
    const measureTitle = annotation.measures['sales.revenue'].shortTitle;
    const dimensionTitle = annotation.dimensions['products.category'].shortTitle;
    
    return {
      tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
      xAxis: {
        type: 'category',
        name: dimensionTitle,
        data: data.map(d => d['products.category'].formattedValue)
      },
      yAxis: {
        type: 'value',
        name: measureTitle
      },
      series: [{
        type: 'bar',
        data: data.map(d => d['sales.revenue'].value)
      }]
    };
  }

Рекомендации

1

Изучите документацию ECharts

Ознакомьтесь с примерами ECharts для понимания возможностей.
2

Структура данных

Значения в data — это объекты FormattedResultValue с полями value (сырое значение) и formattedValue (отформатированная строка). Используйте .value для вычислений и .formattedValue для отображения в подписях и tooltip.
3

Используйте annotation для динамических подписей

Вместо хардкода названий осей и легенды, используйте queries.имя.annotation.measures['cube.measure'].shortTitle для получения заголовков из метаданных куба.
4

Несколько запросов

Используйте несколько именованных запросов в queries, когда данные приходят из разных кубов или требуют разных группировок. Все результаты будут доступны в transform_code через queries.имя_запроса.
5

Обрабатывайте пустые данные

Всегда проверяйте наличие данных: data.length === 0 и значений: row['measure'].value || 0
6

Функции в ECharts

Функции в возвращаемом объекте (например, tooltip.formatter, label.formatter) поддерживаются — они автоматически сериализуются и восстанавливаются. Используйте обычный синтаксис function(params) { ... }.
7

Начните с простого

Создайте базовый график с одним запросом, затем добавляйте сложность постепенно.
JavaScript-код выполняется в изолированном Web Worker на клиенте. Доступ к DOM, window и document недоступен. Избегайте тяжёлых вычислений и бесконечных циклов — при превышении таймаута выполнение будет прервано. Функция transform_code должна быть строкой вида function(queries) { ... return echartsOptions; }.

Генерация с помощью LLM

Вы можете использовать LLM (ChatGPT, Claude и т.д.) для генерации конфигурации dynamic_chart. Скопируйте промпт ниже, заполните верхнюю часть (запросы и описание графика) и отправьте в LLM. Полученный JSON можно вставить в редактор графика.

Полезные ссылки

ECharts Examples

Галерея примеров ECharts

ECharts Options

Справочник по опциям

Cube.js Query Format

Формат запросов Cube.js

Круговые диаграммы | Обзор графиков