P41 · Power BI · DAX · Продвинутые техники

SVG в Power BI: продвинутые техники

Как возвращать SVG-разметку прямо из DAX-меры и показывать её в ячейках таблицы или в карточках. Sparklines, progress bar, mini-donut, status icon — без единого custom visual. Готовые сниппеты, escape-правила, типичные ошибки.

Коротко. Power BI понимает SVG как картинку, если скормить ему data-URI (data:image/svg+xml;utf8,...) через поле с типом данных Image URL. DAX-мера возвращает строку с этим URI, и в ячейке таблицы появляется отрисованный SVG. Этим способом делаются sparklines, progress bar, custom KPI-карточки, mini-donut, status-иконки. Главное правило — escape кавычек и # в HEX-цветах. Дальше по статье — готовые сниппеты, которые можно копировать в свой проект.

Как Power BI понимает SVG

Power BI Desktop умеет показывать картинки в ячейках таблиц и матриц — через тип данных Image URL. Обычно туда кладут реальные ссылки (https://...), но Power BI поддерживает и data-URI — inline-кодированную картинку прямо в строке. Для SVG это означает: можно вернуть из меры строку вида data:image/svg+xml;utf8,<svg>...</svg> — и Power BI отрисует её как картинку.

Для Card-визуала прямого SVG-рендера нет, поэтому там используется обходной приём: помещаем поле с Image URL в Multi-row card или в Table с одной строкой и настраиваем как «карточку».

Прочему это круто:

  • Никаких custom visuals. Стандартная Table / Matrix умеет рендерить SVG из коробки.
  • Динамика. SVG строится из значений мер, поэтому реагирует на фильтры мгновенно.
  • Полная свобода. Любая визуализация, которую можно описать SVG-разметкой, — ваша. Sparkline, donut, mini-bar, progress, иконка статуса, custom KPI — всё.

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

Базовый шаблон SVG-меры

Любая SVG-мера состоит из трёх частей: префикс data-URI, само SVG-тело, конкатенация значений из других мер. Минимальный шаблон:

SVG Demo =
VAR _w = 100
VAR _h = 30
VAR _color = "#3B52D5"
VAR _value = [Sales Amount]
VAR _ratio = DIVIDE(_value, [Sales Target])

VAR _svg =
    "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' " &
    "viewBox='0 0 " & _w & " " & _h & "' width='" & _w & "' height='" & _h & "'>" &
    "<rect x='0' y='5' width='" & _w & "' height='20' fill='%23E5E7EB' rx='3'/>" &
    "<rect x='0' y='5' width='" & ROUND(_w * _ratio, 0) & "' height='20' fill='" & SUBSTITUTE(_color, "#", "%23") & "' rx='3'/>" &
    "</svg>"

RETURN _svg

Что тут важно:

  1. Префикс data:image/svg+xml;utf8,. Без него Power BI считает строку обычным текстом.
  2. Атрибут xmlns='http://www.w3.org/2000/svg'. Обязателен — иначе SVG не валиден и Power BI не нарисует.
  3. Одинарные кавычки внутри SVG. Двойные ломают конкатенацию DAX — приходится их экранировать через UNICHAR(34), что резко удлиняет код.
  4. Escape # в цветах. Хеш в data-URI — это якорь URL. Заменяем на %23, иначе цвет потеряется. Делается через SUBSTITUTE(_color, "#", "%23") или сразу пишем %23 в литерале.
  5. Round чисел до 0–1 знаков. Длинные дроби в координатах раздувают строку и ломают точность отрисовки.

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

  • Откройте свойства поля → Data category → выберите Image URL.
  • Положите меру в Table или Matrix.
  • В настройках столбца увеличьте Image height до разумного (обычно 20–40 px).

Готовые сниппеты — 6 кейсов из production

1. Status icon — иконка по значению

Цветной кружок с иконкой статуса в строке таблицы. Зелёный = факт ≥ план. Жёлтый = 80–100% плана. Красный < 80%.
Status SVG =
VAR _ratio = DIVIDE([Sales Amount], [Sales Target])
VAR _color =
    SWITCH(TRUE(),
        _ratio >= 1,    "%2310B981",   // зелёный
        _ratio >= 0.8,  "%23F7A81B",   // жёлтый
                        "%23C73E5A"    // красный
    )
RETURN
    "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' " &
    "viewBox='0 0 20 20' width='20' height='20'>" &
    "<circle cx='10' cy='10' r='8' fill='" & _color & "'/>" &
    "</svg>"

Простой пример, но показывает базовый паттерн. Дальше всё — расширения этой темы.

2. Mini progress bar

Прогресс к плану — фон-полоска и залитая часть пропорционально проценту. Удобно в таблице по регионам или продуктам.
Progress Bar =
VAR _ratio = MIN(DIVIDE([Sales Amount], [Sales Target]), 1)
VAR _w = 200
VAR _filled = ROUND(_w * _ratio, 0)
VAR _pct = ROUND(_ratio * 100, 0)
RETURN
    "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' " &
    "viewBox='0 0 " & _w & " 20' width='" & _w & "' height='20'>" &
    "<rect x='0' y='6' width='" & _w & "' height='8' fill='%23E5E7EB' rx='3'/>" &
    "<rect x='0' y='6' width='" & _filled & "' height='8' fill='%233B52D5' rx='3'/>" &
    "<text x='" & (_w - 28) & "' y='14' font-size='10' " &
    "font-family='Segoe UI' font-weight='600' fill='%231F2025'>" & _pct & "%25</text>" &
    "</svg>"

Заметьте: знак % в тексте — это %25 в data-URI. Иначе Power BI прочитает следующие два символа как hex-код.

3. Sparkline — мини-линия динамики

12 месяцев выручки одной строкой. Принципиально полезно для executive-таблиц «регион → KPI → тренд». Native sparkline в Power BI работает с 2023, но SVG-версия даёт больше контроля над цветом, точкой выделения, толщиной.
Sparkline =
VAR _series =
    ADDCOLUMNS(
        VALUES(dimDate[YearMonth]),
        "@val", [Sales Amount]
    )
VAR _xMax = 12
VAR _yMin = MINX(_series, [@val])
VAR _yMax = MAXX(_series, [@val])
VAR _yRange = _yMax - _yMin

// Превращаем точки в строку SVG-path
VAR _path =
    CONCATENATEX(
        ADDCOLUMNS(_series, "@idx", RANK.EQ(dimDate[YearMonth], dimDate[YearMonth], ASC)),
        VAR _x = ROUND(([@idx] - 1) * 180 / (_xMax - 1), 1)
        VAR _y = ROUND(28 - ([@val] - _yMin) / _yRange * 24, 1)
        RETURN _x & " " & _y,
        " L ", [@idx], ASC
    )

VAR _color =
    IF([Sales Amount] >= CALCULATE([Sales Amount], DATEADD(dimDate[Date], -1, YEAR)),
        "%233B52D5", "%23C73E5A")

RETURN
    "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' " &
    "viewBox='0 0 180 32' width='180' height='32'>" &
    "<path d='M " & _path & "' stroke='" & _color & "' " &
    "stroke-width='1.5' fill='none'/>" &
    "</svg>"

Это самый длинный сниппет в статье. Главная сложность — превратить серию точек в строку SVG path. Делается через CONCATENATEX с вычислением координат для каждой точки. Префикс "M " в начале добавляется отдельно (move-to первой точки), остальные через разделитель " L " (line-to).

Параметр RANK.EQ — чтобы пронумеровать месяцы. Без него CONCATENATEX может выдать значения в произвольном порядке.

4. Mini donut — одна доля

Мини-донат — альтернатива progress bar в узких колонках или на карточках. Используется через stroke-dasharray: одна окружность фон, поверх неё вторая с частичной обводкой.
Mini Donut =
VAR _ratio = MIN(DIVIDE([Sales Amount], [Sales Target]), 1)
VAR _circumference = 2 * PI() * 14   // радиус 14
VAR _filled = ROUND(_circumference * _ratio, 1)
VAR _empty = ROUND(_circumference - _filled, 1)
VAR _pct = ROUND(_ratio * 100, 0)
RETURN
    "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' " &
    "viewBox='0 0 40 40' width='40' height='40'>" &
    "<circle cx='20' cy='20' r='14' fill='none' stroke='%23E5E7EB' stroke-width='6'/>" &
    "<circle cx='20' cy='20' r='14' fill='none' stroke='%233B52D5' stroke-width='6' " &
        "stroke-dasharray='" & _filled & " " & _empty & "' " &
        "transform='rotate(-90 20 20)'/>" &
    "<text x='20' y='24' text-anchor='middle' font-size='9' " &
        "font-family='Segoe UI' font-weight='600' fill='%231F2025'>" & _pct & "%25</text>" &
    "</svg>"

Поворот rotate(-90 20 20) начинает рисовать дугу с двенадцати часов, а не с трёх (как по умолчанию). Без этого донут смотрится «повёрнутым на бок».

5. Variance bar — отклонение от нуля

Отклонение план/факт — bar от вертикальной нулевой линии. Положительное — зелёный вправо, отрицательное — красный влево. Незаменимо для P&L variance, ABC, остатков.
Variance Bar =
VAR _delta = DIVIDE([Sales Amount] - [Sales Target], [Sales Target])
VAR _maxAbsScale = 0.30  // -30% .. +30% — фиксированная шкала
VAR _wHalf = 100
VAR _ratio = MIN(MAX(_delta / _maxAbsScale, -1), 1)
VAR _barW = ROUND(ABS(_ratio) * _wHalf, 0)
VAR _barX = IF(_delta < 0, _wHalf - _barW, _wHalf)
VAR _color = IF(_delta < 0, "%23C73E5A", "%2310B981")
VAR _label =
    IF(_delta < 0, "", "+") & FORMAT(_delta, "0%")
VAR _labelX = IF(_delta < 0, _wHalf - _barW - 4, _wHalf + _barW + 4)
VAR _labelAnchor = IF(_delta < 0, "end", "start")
RETURN
    "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' " &
    "viewBox='0 0 200 24' width='200' height='24'>" &
    "<line x1='100' y1='2' x2='100' y2='22' stroke='%231F2025' stroke-width='1'/>" &
    "<rect x='" & _barX & "' y='8' width='" & _barW & "' height='8' " &
        "fill='" & _color & "' rx='2'/>" &
    "<text x='" & _labelX & "' y='15' text-anchor='" & _labelAnchor & "' " &
        "font-size='10' font-family='Segoe UI' font-weight='600' fill='" & _color & "'>" &
        SUBSTITUTE(_label, "%", "%25") & "</text>" &
    "</svg>"

Главный приём — фиксированная шкала _maxAbsScale. Без неё каждая строка таблицы рисуется со своим относительным масштабом, и визуально нельзя сравнить «−5%» одной строки с «−10%» другой — оба покажут максимум. Жёсткая шкала ±30% решает.

6. Star rating — оценка по 5 шкале

Звёздный рейтинг — любимый формат для NPS-агрегаций, оценок ревью, табелей качества. SVG позволяет точную позицию + динамическую заливку по значению.
Star Rating =
VAR _rating = MIN(MAX([Avg Rating], 0), 5)
VAR _full = INT(_rating)
VAR _starPath = "10,2 12,7 17,7 13,11 15,17 10,14 5,17 7,11 3,7 8,7"
VAR _stars =
    CONCATENATEX(
        GENERATESERIES(1, 5),
        VAR _i = [Value]
        VAR _x = (_i - 1) * 20
        VAR _color = IF(_i <= _full, "%23F7A81B", "%23E5E7EB")
        RETURN
            "<polygon points='" &
            CONCATENATEX(
                GENERATESERIES(1, 10),
                VAR _pt = [Value]
                VAR _coords = SWITCH(_pt,
                    1, "10,2",  2, "12,7",  3, "17,7",  4, "13,11", 5, "15,17",
                    6, "10,14", 7, "5,17",  8, "7,11",  9, "3,7",  10, "8,7"
                )
                RETURN _coords,
                " "
            ) & "' transform='translate(" & _x & " 0)' fill='" & _color & "'/>",
        ""
    )
RETURN
    "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' " &
    "viewBox='0 0 100 20' width='100' height='20'>" & _stars & "</svg>"

Этот сниппет показывает важный приём — генерация повторяющихся фигур через GENERATESERIES. Тот же подход работает для иконок, мини-баров, точек. Звёздная форма — просто 10 точек polygon, повторённая 5 раз с разными translate-X и цветом по индексу.

Подводные камни

1. Размер строки. DAX-меры с data-URI могут быть длинными — несколько килобайт на ячейку. Power BI кэширует их в модели, так что на большой таблице это ощутимо. Если 1000 строк × 4 SVG-меры × 2 КБ — это 8 МБ только на SVG. Optimization: округляйте координаты до 0–1 знака, упрощайте SVG, убирайте лишние пробелы.
2. Кодировка. Кириллицу в <text>-тегах надо аккуратно: data-URI с ;utf8 поддерживает Unicode напрямую, но на старых версиях Power BI бывают сюрпризы. Альтернатива — ;base64 с дополнительной кодировкой строки, но это уже сложнее.
3. Power BI Service vs Desktop. Иногда SVG корректно отрисовывается в Desktop, но в Service возникают артефакты — чаще из-за разной интерпретации escape-последовательностей. Тестируйте сразу в Service после публикации, не только в Desktop.
4. Размер строки таблицы. Когда положили SVG в колонку с Image URL, Power BI ставит фиксированную высоту строки. Регулируйте через Format → Cell elements → Image height. Слишком высоко — таблица занимает много места; слишком низко — SVG обрезается.
5. Refresh. При большом количестве SVG-мер обновление модели замедляется (каждая ячейка строится заново). На больших датасетах — используйте aggregations и стройте SVG только для агрегированного слоя.

Альтернатива — HTML Content

Когда нужен SVG большего размера (полноценный mini-dashboard, marimekko, sankey) — ячейки таблицы становятся тесными. Для таких случаев есть HTML Content — certified visual от Daniel Marsh-Patrick, который рендерит произвольный HTML/SVG из DAX-меры на всю площадь визуала.

Это один из немногих case'ов, когда мы в DEEONE рекомендуем custom visual — потому что он certified, активно поддерживается, и используется как общий «движок» для любых ваших SVG-композиций. Поставили один раз — дальше любые кастомные графики делаются через DAX.

Главное правило: HTML Content не значит «полная свобода». Используйте его для того, что стандартные визуалы не умеют (marimekko, sankey, custom-композиции), а не «потому что красивее». Стандартный bar+conditional formatting почти всегда лучше custom-альтернативы по скорости и поддержке.

Когда НЕ использовать SVG-меры

  • Большие визуализации. SVG в ячейке — только маленькие глифы (до 200×40). Большие — через HTML Content или стандартные визуалы.
  • Когда стандартное справляется. Если native sparkline в Power BI делает то же самое — используйте его, не кастомное.
  • Команда без DAX-эксперта. Поддержка SVG-мер — это DAX уровня middle+. Если следующий разработчик увидит и не поймёт — через год отчёт сломается, и никто не сможет его починить.
  • Слабая модель. SVG-меры тяжёлые. На flat-таблице 5М строк без агрегатов — забудьте.

Связанные материалы