feat(superset): подача дашборда «Бизнес-сюжеты»

- Зачем:
  - экран отвечал только графиками: итога за период не было, доли категорий
    читались по высоте столбиков, а известные пользователи лежали на нуле
    рядом с посетителями.
- Что:
  - добавлены ряд крупных чисел (выручка, заказы, средний чек, посетители),
    кольцо долей категорий и линия доли известных пользователей;
  - средний чек и доля известных заведены метриками датасетов, знаменатель
    через `nullIf`: на пустом периоде деление Decimal на ноль в ClickHouse
    роняет панель;
  - фильтр «Период» сужает все панели, включая сверку по `order_day`;
  - `label_colors` закрепляет цвет за категорией и за рядами трафика;
  - деньги без копеек, единица в названии панели; подписи дат читаются как
    даты; сводная сверки развернута классами по строкам и уместилась без
    прокрутки;
  - обновлены справочник Superset и глава путеводителя.
- Проверка:
  - `make lint`;
  - `superset import-directory /app/import --overwrite` на живом стенде, экран
    снят в браузере: девять панелей отрисованы, цвет категории одинаков на
    столбиках и в кольце;
  - период 03.06–06.06 сузил все панели, включая сводную по `order_day`;
  - `SUM(revenue) / nullIf(SUM(orders), 0)` на периоде без заказов возвращает
    NULL, без `nullIf` — исключение 153.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ddadminandClaude Opus 5 committed 2026-09-08 03:35:00 -04:00
1 parent 3ad377e02e
commit b6c10ecf21
14 files changed
+378 -32

No files matched your search

+53 -4
View File
@@ -36,7 +36,7 @@ Airflow. В ClickHouse он не пишет ничего.
| `metadata.yaml` | версия формата, `1.0.0`; больше в нем ничего нет | | `metadata.yaml` | версия формата, `1.0.0`; больше в нем ничего нет |
| `databases/clickhouse.yaml` | подключение к ClickHouse: URI без пароля и постоянный uuid | | `databases/clickhouse.yaml` | подключение к ClickHouse: URI без пароля и постоянный uuid |
| `datasets/ClickHouse/*.yaml` | три витрины: `revenue_daily_v`, `purchase_vs_orders_v`, `daily_traffic_v` | | `datasets/ClickHouse/*.yaml` | три витрины: `revenue_daily_v`, `purchase_vs_orders_v`, `daily_traffic_v` |
| `charts/*.yaml` | три графика: столбики выручки, сводная таблица сверки, линии трафика | | `charts/*.yaml` | девять графиков: четыре крупных числа, столбики выручки и кольцо долей, сводная таблица сверки, линии трафика и доля известных |
| `dashboards/business_stories.yaml` | дашборд «Бизнес-сюжеты» и раскладка панелей | | `dashboards/business_stories.yaml` | дашборд «Бизнес-сюжеты» и раскладка панелей |
Связаны файлы не путями, а uuid. Датасет называет `database_uuid`, график — Связаны файлы не путями, а uuid. Датасет называет `database_uuid`, график —
@@ -46,20 +46,51 @@ Airflow. В ClickHouse он не пишет ничего.
Uuid у стенда розданы рукой: общее начало Uuid у стенда розданы рукой: общее начало
`19600000-0000-4000-8000-`, дальше номер объекта в последнем блоке — `1`–`3` `19600000-0000-4000-8000-`, дальше номер объекта в последнем блоке — `1`–`3`
у датасетов, `101`–`103` у графиков, `201` у дашборда у датасетов, `101`–`109` у графиков, `201` у дашборда
(`…-000000000101`). Экспорт из интерфейса приносит свои, сгенерированные; (`…-000000000101`). Экспорт из интерфейса приносит свои, сгенерированные;
их заменяют на эти, иначе повторный импорт заведет вторую копию объекта вместо их заменяют на эти, иначе повторный импорт заведет вторую копию объекта вместо
обновления первой. обновления первой.
Датасет несет русские имена колонок, формат денег и метрику — а график только Датасет несет русские имена колонок, формат чисел и метрики — а график только
ссылается на них по имени. Значит, подпись или формат правятся в одном месте, а ссылается на них по имени. Значит, подпись или формат правятся в одном месте, а
не в каждом графике. не в каждом графике. Два исключения проверены на живом стенде, Superset 6.1.
Подписи оси график берет не из датасета, а из своего `y_axis_format`: без него
ось выручки уходит в «1.2M». И разрез сводной таблицы подписан именем колонки
(`mismatch_class`), хотя колонке даты в той же таблице русское имя достается.
Датасеты объявлены физическими (`schema: dm`, `sql: null`), хотя за именем стоит Датасеты объявлены физическими (`schema: dm`, `sql: null`), хотя за именем стоит
представление ClickHouse. Импорт в источник не ходит и разницы между таблицей и представление ClickHouse. Импорт в источник не ходит и разницы между таблицей и
представлением не знает, а виртуальный датасет со своим SQL просто повторил бы представлением не знает, а виртуальный датасет со своим SQL просто повторил бы
запрос, который уже живет в `sql/ddl/60-dm-views.sql`. запрос, который уже живет в `sql/ddl/60-dm-views.sql`.
## Из чего собран экран
Четыре строки панелей:
- ряд крупных чисел — выручка, заказы, средний чек, посетители;
- выручка по дням и категориям, рядом доля категорий кольцом;
- сводная сверки purchase и заказов;
- посетители и известные пользователи, рядом доля известных.
Правая панель пары отвечает на вопрос «сколько из целого» и берет цвета левой:
категория окрашена одинаково на столбиках и в кольце, доля известных — как
линия известных пользователей рядом. Цвет закрепляет `label_colors` в
`metadata` дашборда. Шесть категорий выписаны в него руками по каталогу
генератора (`generator/src/clickstream_generator/catalog.py`): седьмая
останется без закрепленного цвета, пока файл не поправят.
Средний чек и доля известных считаются метриками датасета —
`SUM(revenue) / nullIf(SUM(orders), 0)` и то же для доли известных. Знаменатель
обернут не зря: на периоде без заказов деление Decimal на ноль в ClickHouse —
не `nan`, а исключение, и панель показала бы ошибку вместо пустоты. У витрины
выручки есть готовая колонка `aov`, но среднее по ней дало бы другое число:
день с тремя заказами получил бы тот же вес, что день с тремя сотнями.
«Период» — единственный фильтр дашборда, тип `filter_time` с пустой целью
(`targets: [{}]`). Такой фильтр колонку не называет: он подставляет выбранный
период в то временное условие, которое уже стоит у каждого графика. Поэтому
сверка сужается по `order_day`, а остальные панели — по `report_date`.
## Импорт при каждой инициализации ## Импорт при каждой инициализации
Последняя строка `infra/superset/init.sh`: Последняя строка `infra/superset/init.sh`:
@@ -152,6 +183,17 @@ superset import-directory /app/import --overwrite
завершается нулем в любом случае. Поэтому импорт проверяют глазами или завершается нулем в любом случае. Поэтому импорт проверяют глазами или
запросом к API — но не тем, что команда отработала. запросом к API — но не тем, что команда отработала.
**Знак рубля приходит не из формата валюты.** Символ Superset берет у
`Intl.NumberFormat` по коду валюты и английской локали стенда: код `RUB`
печатается как «RUB», а знак «₽» вызов роняет, и форматирование молча
откатывается к числу без валюты. Поэтому единица стоит в названии панели —
«Выручка, ₽», — а метрика несет обычный формат `,d`.
**`label_colors` знает имя ряда, а не подпись.** Для разреза имя ряда — это
значение колонки («Бытовая техника»), для графика без разреза — имя метрики
(`known_share`), а не ее русская подпись. Цвет, закрепленный за подписью
«Доля известных», не применяется.
**`--overwrite` синхронизирует датасет разрушительно.** Колонки и метрики **`--overwrite` синхронизирует датасет разрушительно.** Колонки и метрики
берутся из YAML целиком: добавленная в интерфейсе метрика или подпись колонки берутся из YAML целиком: добавленная в интерфейсе метрика или подпись колонки
исчезнет при следующем старте контейнера. Правка датасета — это правка файла. исчезнет при следующем старте контейнера. Правка датасета — это правка файла.
@@ -196,6 +238,13 @@ superset import-directory /app/import --overwrite
каждой витрины, — поэтому фильтр периода дашборду разрешен, а кросс-фильтрация каждой витрины, — поэтому фильтр периода дашборду разрешен, а кросс-фильтрация
нет. нет.
**Вкладки и блоки «что смотреть».** Вкладки были условным вариантом: их
заводят, когда экран перестает читаться одним взглядом. Четыре строки панелей
читаются сразу, поэтому вкладки спрятали бы половину экрана за щелчком без
выигрыша. Блок с текстом «что смотреть» рядом с панелью означал
бы, что панель сама себя не объясняет: тогда чинить надо название и подписи, а
не дописывать к ним пояснение.
**Пороги и раскраска панелей.** Правило **Пороги и раскраска панелей.** Правило
[спеки](../specs/2026-07-30-stand-v2-realism.md), раздел 9: на дашборд смотрят [спеки](../specs/2026-07-30-stand-v2-realism.md), раздел 9: на дашборд смотрят
глазами, красное и зеленое живет в проверках. глазами, красное и зеленое живет в проверках.
+11 -9
View File
@@ -1,21 +1,23 @@
# Немного Superset # Немного Superset
Superset показывает витрины слоя DM в виде графиков. К ClickHouse он Superset показывает витрины слоя DM в виде графиков. К ClickHouse он
подключается пользователем `bi`. На дашборде стенда «Бизнес-сюжеты» три подключается пользователем `bi`. Дашборд стенда «Бизнес-сюжеты» собран из трех
графика: витрин:
| График | Витрина | | Витрина | Что показывает |
|---|---| |---|---|
| «Выручка по дням и категориям» | `dm.revenue_daily_v` | | `dm.revenue_daily_v` | выручку, заказы и средний чек крупными числами, выручку по дням и категориям столбиками, доли категорий кольцом |
| «Сверка purchase и заказов» | `dm.purchase_vs_orders_v` | | `dm.purchase_vs_orders_v` | сводную сверки: сколько заказов дня попало в каждый класс расхождения |
| «Посетители и известные пользователи» | `dm.daily_traffic_v` | | `dm.daily_traffic_v` | число посетителей крупно, посетителей и известных пользователей по дням, отдельно — долю известных |
Слева от панелей стоит фильтр «Период». Он задает даты сразу для всех панелей.
## Как витрина становится графиком ## Как витрина становится графиком
От витрины до экрана три объекта Superset: датасет → график → дашборд. Датасет От витрины до экрана три объекта Superset: датасет → график → дашборд. Датасет
описывает витрину: задает русские имена колонок и формат денег. График описывает витрину: задает русские имена колонок, формат чисел и показатели —
ссылается на эти имена и задает способ показа. Дашборд собирает готовые графики например, средний чек. График ссылается на эти имена и задает способ показа.
на одном экране. Дашборд собирает готовые графики на одном экране.
## Стендовые объекты и ваши ## Стендовые объекты и ваши
@@ -0,0 +1,19 @@
slice_name: Средний чек, ₽
description: null
certified_by: null
certification_details: null
viz_type: big_number_total
params:
viz_type: big_number_total
metric: avg_check
adhoc_filters:
- expressionType: SIMPLE
subject: report_date
operator: TEMPORAL_RANGE
comparator: No filter
clause: WHERE
header_font_size: 0.3
cache_timeout: null
uuid: 19600000-0000-4000-8000-000000000106
version: 1.0.0
dataset_uuid: 19600000-0000-4000-8000-000000000001
@@ -0,0 +1,19 @@
slice_name: Заказы
description: null
certified_by: null
certification_details: null
viz_type: big_number_total
params:
viz_type: big_number_total
metric: orders
adhoc_filters:
- expressionType: SIMPLE
subject: report_date
operator: TEMPORAL_RANGE
comparator: No filter
clause: WHERE
header_font_size: 0.3
cache_timeout: null
uuid: 19600000-0000-4000-8000-000000000105
version: 1.0.0
dataset_uuid: 19600000-0000-4000-8000-000000000001
@@ -0,0 +1,19 @@
slice_name: Выручка, ₽
description: null
certified_by: null
certification_details: null
viz_type: big_number_total
params:
viz_type: big_number_total
metric: revenue
adhoc_filters:
- expressionType: SIMPLE
subject: report_date
operator: TEMPORAL_RANGE
comparator: No filter
clause: WHERE
header_font_size: 0.3
cache_timeout: null
uuid: 19600000-0000-4000-8000-000000000104
version: 1.0.0
dataset_uuid: 19600000-0000-4000-8000-000000000001
@@ -0,0 +1,19 @@
slice_name: Посетители
description: null
certified_by: null
certification_details: null
viz_type: big_number_total
params:
viz_type: big_number_total
metric: visitors
adhoc_filters:
- expressionType: SIMPLE
subject: report_date
operator: TEMPORAL_RANGE
comparator: No filter
clause: WHERE
header_font_size: 0.3
cache_timeout: null
uuid: 19600000-0000-4000-8000-000000000107
version: 1.0.0
dataset_uuid: 19600000-0000-4000-8000-000000000003
@@ -0,0 +1,31 @@
slice_name: Доля известных пользователей
description: null
certified_by: null
certification_details: null
viz_type: echarts_timeseries_line
params:
viz_type: echarts_timeseries_line
x_axis: report_date
metrics:
- known_share
groupby: []
adhoc_filters:
- expressionType: SIMPLE
subject: report_date
operator: TEMPORAL_RANGE
comparator: No filter
clause: WHERE
row_limit: 10000
seriesType: line
show_legend: false
rich_tooltip: true
show_value: false
markerEnabled: true
markerSize: 6
y_axis_format: .1%
x_axis_time_format: '%d.%m'
tooltipTimeFormat: '%d.%m.%Y'
cache_timeout: null
uuid: 19600000-0000-4000-8000-000000000109
version: 1.0.0
dataset_uuid: 19600000-0000-4000-8000-000000000003
@@ -6,9 +6,9 @@ viz_type: pivot_table_v2
params: params:
viz_type: pivot_table_v2 viz_type: pivot_table_v2
groupbyRows: groupbyRows:
- order_day
groupbyColumns:
- mismatch_class - mismatch_class
groupbyColumns:
- order_day
metrics: metrics:
- order_count - order_count
adhoc_filters: adhoc_filters:
@@ -23,8 +23,7 @@ params:
colOrder: key_a_to_z colOrder: key_a_to_z
order_desc: true order_desc: true
row_limit: 10000 row_limit: 10000
valueFormat: SMART_NUMBER date_format: '%d.%m.%Y'
date_format: smart_date
cache_timeout: null cache_timeout: null
uuid: 19600000-0000-4000-8000-000000000102 uuid: 19600000-0000-4000-8000-000000000102
version: 1.0.0 version: 1.0.0
@@ -1,4 +1,4 @@
slice_name: Выручка по дням и категориям slice_name: Выручка по дням и категориям, ₽
description: null description: null
certified_by: null certified_by: null
certification_details: null certification_details: null
@@ -20,10 +20,13 @@ params:
stack: Stack stack: Stack
orientation: vertical orientation: vertical
show_legend: true show_legend: true
legendType: plain
legendOrientation: bottom
rich_tooltip: true rich_tooltip: true
show_value: false show_value: false
y_axis_format: ',.2f' y_axis_format: ',d'
x_axis_time_format: smart_date x_axis_time_format: '%d.%m'
tooltipTimeFormat: '%d.%m.%Y'
cache_timeout: null cache_timeout: null
uuid: 19600000-0000-4000-8000-000000000101 uuid: 19600000-0000-4000-8000-000000000101
version: 1.0.0 version: 1.0.0
@@ -0,0 +1,29 @@
slice_name: Доля категорий в выручке
description: null
certified_by: null
certification_details: null
viz_type: pie
params:
viz_type: pie
groupby:
- product_category
metric: revenue
adhoc_filters:
- expressionType: SIMPLE
subject: report_date
operator: TEMPORAL_RANGE
comparator: No filter
clause: WHERE
row_limit: 10000
sort_by_metric: true
donut: true
innerRadius: 30
outerRadius: 70
show_labels: true
labels_outside: false
label_type: percent
show_legend: false
cache_timeout: null
uuid: 19600000-0000-4000-8000-000000000108
version: 1.0.0
dataset_uuid: 19600000-0000-4000-8000-000000000001
@@ -17,12 +17,16 @@ params:
comparator: No filter comparator: No filter
clause: WHERE clause: WHERE
row_limit: 10000 row_limit: 10000
seriesType: echarts_timeseries_line seriesType: line
show_legend: true show_legend: true
legendOrientation: bottom
rich_tooltip: true rich_tooltip: true
show_value: false show_value: false
markerEnabled: true
markerSize: 6
y_axis_format: ',d' y_axis_format: ',d'
x_axis_time_format: smart_date x_axis_time_format: '%d.%m'
tooltipTimeFormat: '%d.%m.%Y'
cache_timeout: null cache_timeout: null
uuid: 19600000-0000-4000-8000-000000000103 uuid: 19600000-0000-4000-8000-000000000103
version: 1.0.0 version: 1.0.0
@@ -19,9 +19,79 @@ position:
parents: parents:
- ROOT_ID - ROOT_ID
children: children:
- ROW-kpi
- ROW-revenue - ROW-revenue
- ROW-pvo - ROW-pvo
- ROW-traffic - ROW-traffic
ROW-kpi:
id: ROW-kpi
type: ROW
parents:
- ROOT_ID
- GRID_ID
children:
- CHART-kpi-revenue
- CHART-kpi-orders
- CHART-kpi-avg-check
- CHART-kpi-visitors
meta:
background: BACKGROUND_TRANSPARENT
CHART-kpi-revenue:
id: CHART-kpi-revenue
type: CHART
parents:
- ROOT_ID
- GRID_ID
- ROW-kpi
children: []
meta:
chartId: 4
height: 20
width: 3
sliceName: Выручка, ₽
uuid: 19600000-0000-4000-8000-000000000104
CHART-kpi-orders:
id: CHART-kpi-orders
type: CHART
parents:
- ROOT_ID
- GRID_ID
- ROW-kpi
children: []
meta:
chartId: 5
height: 20
width: 3
sliceName: Заказы
uuid: 19600000-0000-4000-8000-000000000105
CHART-kpi-avg-check:
id: CHART-kpi-avg-check
type: CHART
parents:
- ROOT_ID
- GRID_ID
- ROW-kpi
children: []
meta:
chartId: 6
height: 20
width: 3
sliceName: Средний чек, ₽
uuid: 19600000-0000-4000-8000-000000000106
CHART-kpi-visitors:
id: CHART-kpi-visitors
type: CHART
parents:
- ROOT_ID
- GRID_ID
- ROW-kpi
children: []
meta:
chartId: 7
height: 20
width: 3
sliceName: Посетители
uuid: 19600000-0000-4000-8000-000000000107
ROW-revenue: ROW-revenue:
id: ROW-revenue id: ROW-revenue
type: ROW type: ROW
@@ -30,6 +100,7 @@ position:
- GRID_ID - GRID_ID
children: children:
- CHART-revenue - CHART-revenue
- CHART-revenue-share
meta: meta:
background: BACKGROUND_TRANSPARENT background: BACKGROUND_TRANSPARENT
CHART-revenue: CHART-revenue:
@@ -43,9 +114,23 @@ position:
meta: meta:
chartId: 1 chartId: 1
height: 50 height: 50
width: 12 width: 8
sliceName: Выручка по дням и категориям sliceName: Выручка по дням и категориям, ₽
uuid: 19600000-0000-4000-8000-000000000101 uuid: 19600000-0000-4000-8000-000000000101
CHART-revenue-share:
id: CHART-revenue-share
type: CHART
parents:
- ROOT_ID
- GRID_ID
- ROW-revenue
children: []
meta:
chartId: 8
height: 50
width: 4
sliceName: Доля категорий в выручке
uuid: 19600000-0000-4000-8000-000000000108
ROW-pvo: ROW-pvo:
id: ROW-pvo id: ROW-pvo
type: ROW type: ROW
@@ -66,7 +151,7 @@ position:
children: [] children: []
meta: meta:
chartId: 2 chartId: 2
height: 50 height: 44
width: 12 width: 12
sliceName: Сверка purchase и заказов sliceName: Сверка purchase и заказов
uuid: 19600000-0000-4000-8000-000000000102 uuid: 19600000-0000-4000-8000-000000000102
@@ -78,6 +163,7 @@ position:
- GRID_ID - GRID_ID
children: children:
- CHART-traffic - CHART-traffic
- CHART-known-share
meta: meta:
background: BACKGROUND_TRANSPARENT background: BACKGROUND_TRANSPARENT
CHART-traffic: CHART-traffic:
@@ -91,15 +177,57 @@ position:
meta: meta:
chartId: 3 chartId: 3
height: 50 height: 50
width: 12 width: 6
sliceName: Посетители и известные пользователи sliceName: Посетители и известные пользователи
uuid: 19600000-0000-4000-8000-000000000103 uuid: 19600000-0000-4000-8000-000000000103
CHART-known-share:
id: CHART-known-share
type: CHART
parents:
- ROOT_ID
- GRID_ID
- ROW-traffic
children: []
meta:
chartId: 9
height: 50
width: 6
sliceName: Доля известных пользователей
uuid: 19600000-0000-4000-8000-000000000109
metadata: metadata:
color_scheme: supersetColors color_scheme: supersetColors
label_colors:
Товары для дома: '#1FA8C9'
Текстиль: '#454E7C'
Посуда: '#5AC189'
Бытовая техника: '#FF7F44'
Детские товары: '#E04355'
Одежда и обувь: '#A868B7'
visitors: '#1FA8C9'
known_users: '#454E7C'
known_share: '#454E7C'
cross_filters_enabled: false cross_filters_enabled: false
chart_configuration: {} chart_configuration: {}
expanded_slices: {} expanded_slices: {}
default_filters: '{}' default_filters: '{}'
native_filter_configuration: [] native_filter_configuration:
- id: NATIVE_FILTER-period
name: Период
filterType: filter_time
type: NATIVE_FILTER
targets:
- {}
controlValues:
enableEmptyFilter: false
defaultDataMask:
extraFormData: {}
filterState: {}
ownState: {}
cascadeParentIds: []
scope:
rootPath:
- ROOT_ID
excluded: []
description: ''
theme_uuid: null theme_uuid: null
version: 1.0.0 version: 1.0.0
@@ -36,6 +36,15 @@ metrics:
currency: null currency: null
extra: null extra: null
warning_text: null warning_text: null
- metric_name: known_share
verbose_name: Доля известных
metric_type: null
expression: SUM(known_users) / nullIf(SUM(visitors), 0)
description: null
d3format: .1%
currency: null
extra: null
warning_text: null
columns: columns:
- column_name: _load_ts - column_name: _load_ts
verbose_name: Время загрузки verbose_name: Время загрузки
@@ -23,10 +23,26 @@ metrics:
metric_type: null metric_type: null
expression: SUM(revenue) expression: SUM(revenue)
description: null description: null
d3format: ',.2f' d3format: ',d'
currency: currency: null
symbol: ₽ extra: null
symbolPosition: suffix warning_text: null
- metric_name: orders
verbose_name: Заказы
metric_type: null
expression: SUM(orders)
description: null
d3format: ',d'
currency: null
extra: null
warning_text: null
- metric_name: avg_check
verbose_name: Средний чек
metric_type: null
expression: SUM(revenue) / nullIf(SUM(orders), 0)
description: null
d3format: ',d'
currency: null
extra: null extra: null
warning_text: null warning_text: null
columns: columns:
@@ -44,7 +60,7 @@ columns:
datetime_format: null datetime_format: null
extra: null extra: null
- column_name: aov - column_name: aov
verbose_name: Средний чек verbose_name: Средний чек за день и категорию
is_dttm: false is_dttm: false
is_active: true is_active: true
type: DECIMAL(18, 2) type: DECIMAL(18, 2)