@evergis/react 4.0.153 → 4.0.154

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3358,6 +3358,46 @@ const MAX_CHART_WIDTH = 300;
3358
3358
  const DEFAULT_CHART_ANGLE = 4;
3359
3359
  const DEFAULT_CHART_HEIGHT = 90;
3360
3360
  const STACK_BAR_TOTAL_HEIGHT = 20;
3361
+ /** Высота строки текста подписи оси (шрифт 10px) — по макету. */
3362
+ const AXIS_TITLE_LINE_HEIGHT = 12;
3363
+ /** Зазор между подписью оси и значениями шкалы — по макету. */
3364
+ const AXIS_TITLE_GAP = 4;
3365
+ /**
3366
+ * Поле SVG графика под выступ крайних делений шкалы значений.
3367
+ *
3368
+ * d3 центрирует подпись деления на его линии (`dy: 0.32em`), поэтому у крайнего деления половина подписи
3369
+ * выходит за поле графика. Если поле вплотную к краю SVG, половина оказывается за SVG, и её срезает
3370
+ * обёртка: у линейного графика в fill — `ChartFillMeasure` (`overflow: hidden`), у барчарта всегда —
3371
+ * `BarChartContainer` (`overflow-x: hidden`, отчего и по вертикали содержимое не `visible`).
3372
+ */
3373
+ const Y_AXIS_LABEL_OVERHANG = 6;
3374
+ /** Зазор от строки горизонтальных подписей осей до верхней линии сетки — по макету. */
3375
+ const AXIS_TITLE_TOP_GRID_GAP = 9;
3376
+ /** Число делений шкал линейного графика; у левой и правой шкал оно общее, чтобы совпадала сетка. */
3377
+ const LINE_CHART_Y_TICKS = 4;
3378
+ /**
3379
+ * Высота строки горизонтальных подписей осей (`axis.titlePosition: "top"`). Вместе с верхним полем SVG
3380
+ * (Y_AXIS_LABEL_OVERHANG) даёт строку текста и 9px до верхней линии сетки: деление центрировано на
3381
+ * линии и выступает над ней на ~5px, так что между подписью и значением шкалы остаются те же 4px,
3382
+ * что и у вертикальной подписи.
3383
+ */
3384
+ const AXIS_TITLE_TOP_HEIGHT = AXIS_TITLE_LINE_HEIGHT + AXIS_TITLE_TOP_GRID_GAP - Y_AXIS_LABEL_OVERHANG;
3385
+ /**
3386
+ * Пустая полоса под подписями оси X внутри SVG линейного графика: под ось зарезервировано
3387
+ * LINE_CHART_LABEL_MARGIN (20px), а подписи сдвинуты вниз на +16px, поэтому строка подписи
3388
+ * (шрифт d3 10px) кончается примерно в 14px от низа SVG.
3389
+ */
3390
+ const LINE_CHART_X_LABELS_BLANK = 14;
3391
+ /** Зазор между подписями оси X и подписью самой оси — по макету. */
3392
+ const X_AXIS_TITLE_GAP = 4;
3393
+ /** Зазор между подписью оси X и легендой, идущей отдельной строкой, — по макету. */
3394
+ const X_AXIS_TITLE_LEGEND_GAP = 8;
3395
+ /** Выравнивание подписи оси X → `justify-content` строки-подвала графика. */
3396
+ const CHART_AXIS_ALIGN_JUSTIFY = {
3397
+ left: "flex-start",
3398
+ center: "center",
3399
+ right: "flex-end",
3400
+ };
3361
3401
  const FILTER_PREFIX = "%";
3362
3402
  /** Имя системного геометрического фильтра — выделение, нарисованное пользователем на карте. */
3363
3403
  const GEOMETRY_FILTER_NAME = "geometry";
@@ -3467,7 +3507,76 @@ const CONTAINER_BODY_FILL_STYLE = {
3467
3507
  */
3468
3508
  const CONTAINERS_GROUP_DEFAULTS = { boxSizing: "border-box" };
3469
3509
 
3470
- const ChartTooltipTable = styled.table.withConfig({ displayName: "ChartTooltipTable", componentId: "sc-1fk3ts5" }) `
3510
+ /** Обёртка линейного графика: строка горизонтальных подписей над телом, вертикальные — по бокам. */
3511
+ const LineChartLayout = styled.div.withConfig({ displayName: "LineChartLayout", componentId: "sc-gmzloe" }) `
3512
+ display: flex;
3513
+ flex-direction: column;
3514
+ width: 100%;
3515
+ height: 100%;
3516
+ min-width: 0;
3517
+ min-height: 0;
3518
+ `;
3519
+ const LineChartRow = styled.div.withConfig({ displayName: "LineChartRow", componentId: "sc-dxzi1m" }) `
3520
+ display: flex;
3521
+ flex: 1 1 auto;
3522
+ align-items: stretch;
3523
+ min-width: 0;
3524
+ min-height: 0;
3525
+ `;
3526
+ /**
3527
+ * Тело линейного графика между вертикальными подписями осей. Забирает всю оставшуюся ширину строки —
3528
+ * её и измеряем, чтобы отдать d3 пиксельную ширину: подписи бывают в одну и в две строки.
3529
+ */
3530
+ const LineChartBody = styled.div.withConfig({ displayName: "LineChartBody", componentId: "sc-1sbsc7u" }) `
3531
+ flex: 1 1 auto;
3532
+ min-width: 0;
3533
+ `;
3534
+ const AxisTitlesRow = styled.div.withConfig({ displayName: "AxisTitlesRow", componentId: "sc-1hskx6f" }) `
3535
+ display: flex;
3536
+ flex: 0 0 ${AXIS_TITLE_TOP_HEIGHT}px;
3537
+ justify-content: space-between;
3538
+ align-items: flex-start;
3539
+ gap: 0.5rem;
3540
+ height: ${AXIS_TITLE_TOP_HEIGHT}px;
3541
+ min-width: 0;
3542
+ `;
3543
+ const AxisTitleText = styled.div.withConfig({ displayName: "AxisTitleText", componentId: "sc-9shs4l" }) `
3544
+ font-size: 0.625rem;
3545
+ line-height: ${AXIS_TITLE_LINE_HEIGHT}px;
3546
+ color: ${({ $color, theme: { palette } }) => $color || palette.iconDisabled};
3547
+ `;
3548
+ const AxisTitleTop = styled(AxisTitleText).withConfig({ displayName: "AxisTitleTop", componentId: "sc-pv3dgm" }) `
3549
+ overflow: hidden;
3550
+ white-space: nowrap;
3551
+ text-overflow: ellipsis;
3552
+ `;
3553
+ /**
3554
+ * Колонка вертикальной подписи оси: ширина — по тексту (одна или две строки) плюс зазор до значений
3555
+ * шкалы. Вертикальные отступы колонки повторяют поля SVG, поэтому подпись центрируется по полю
3556
+ * графика, а не по всей высоте вместе с подписями оси X.
3557
+ */
3558
+ const AxisTitleSideColumn = styled.div.withConfig({ displayName: "AxisTitleSideColumn", componentId: "sc-11nmkfu" }) `
3559
+ display: flex;
3560
+ flex: 0 0 auto;
3561
+ align-items: center;
3562
+ justify-content: center;
3563
+ box-sizing: border-box;
3564
+ padding: ${({ $plotTop }) => $plotTop}px 0 ${({ $plotBottom }) => $plotBottom}px;
3565
+ ${({ $side }) => ($side === "left" ? "padding-right" : "padding-left")}: ${AXIS_TITLE_GAP}px;
3566
+ `;
3567
+ /**
3568
+ * Сам текст вертикальной подписи. Поворот вынесен на внутренний элемент: повёрнутая целиком колонка
3569
+ * перенесла бы зазор до шкалы на внешнюю сторону. Левая подпись читается снизу вверх, правая — сверху
3570
+ * вниз, как на макете.
3571
+ */
3572
+ const AxisTitleSide = styled(AxisTitleText).withConfig({ displayName: "AxisTitleSide", componentId: "sc-lretfl" }) `
3573
+ max-height: 100%;
3574
+ overflow: hidden;
3575
+ text-align: center;
3576
+ writing-mode: vertical-rl;
3577
+ transform: ${({ $side }) => ($side === "left" ? "rotate(180deg)" : "none")};
3578
+ `;
3579
+ const ChartTooltipTable = styled.table.withConfig({ displayName: "ChartTooltipTable", componentId: "sc-1j0a1z" }) `
3471
3580
  td {
3472
3581
  padding: 0;
3473
3582
  }
@@ -3476,7 +3585,7 @@ const ChartTooltipTable = styled.table.withConfig({ displayName: "ChartTooltipTa
3476
3585
  padding-bottom: 0.375rem;
3477
3586
  }
3478
3587
  `;
3479
- const ChartTooltip = styled(uilibGl.Flex).withConfig({ displayName: "ChartTooltip", componentId: "sc-1iyyyv7" }) `
3588
+ const ChartTooltip = styled(uilibGl.Flex).withConfig({ displayName: "ChartTooltip", componentId: "sc-1ps09qa" }) `
3480
3589
  flex-wrap: nowrap;
3481
3590
  background: rgb(48, 69, 79);
3482
3591
  border-radius: 0.25rem;
@@ -3486,26 +3595,26 @@ const ChartTooltip = styled(uilibGl.Flex).withConfig({ displayName: "ChartToolti
3486
3595
  margin-bottom: 0.25rem;
3487
3596
  }
3488
3597
  `;
3489
- const ChartTooltipLabel = styled.div.withConfig({ displayName: "ChartTooltipLabel", componentId: "sc-gl0qxz" }) `
3598
+ const ChartTooltipLabel = styled.div.withConfig({ displayName: "ChartTooltipLabel", componentId: "sc-1chgokj" }) `
3490
3599
  margin-right: 0.25rem;
3491
3600
  `;
3492
- const ChartTooltipColor = styled.div.withConfig({ displayName: "ChartTooltipColor", componentId: "sc-tmvvbh" }) `
3601
+ const ChartTooltipColor = styled.div.withConfig({ displayName: "ChartTooltipColor", componentId: "sc-1un94mw" }) `
3493
3602
  width: 0.625rem;
3494
3603
  height: 0.625rem;
3495
3604
  background-color: ${({ $color }) => $color};
3496
3605
  border-radius: ${({ theme: { borderRadius } }) => borderRadius.smallest};
3497
3606
  margin-right: 0.25rem;
3498
3607
  `;
3499
- const ChartLegendColor$1 = styled.div.withConfig({ displayName: "ChartLegendColor", componentId: "sc-l66k4q" }) `
3608
+ const ChartLegendColor$1 = styled.div.withConfig({ displayName: "ChartLegendColor", componentId: "sc-sw1cfw" }) `
3500
3609
  width: 0.5rem;
3501
3610
  height: 0.5rem;
3502
3611
  background-color: ${({ $color }) => $color};
3503
3612
  border-radius: 0.125rem;
3504
3613
  `;
3505
- const ChartLegendValue = styled.div.withConfig({ displayName: "ChartLegendValue", componentId: "sc-1d7z775" }) `
3614
+ const ChartLegendValue = styled.div.withConfig({ displayName: "ChartLegendValue", componentId: "sc-1s1kjum" }) `
3506
3615
  opacity: 0.65;
3507
3616
  `;
3508
- const ChartTooltipRow = styled(uilibGl.Flex).withConfig({ displayName: "ChartTooltipRow", componentId: "sc-ih1uc5" }) `
3617
+ const ChartTooltipRow = styled(uilibGl.Flex).withConfig({ displayName: "ChartTooltipRow", componentId: "sc-ftllia" }) `
3509
3618
  display: flex;
3510
3619
  flex-direction: row;
3511
3620
  flex-wrap: nowrap;
@@ -3517,11 +3626,11 @@ const ChartTooltipRow = styled(uilibGl.Flex).withConfig({ displayName: "ChartToo
3517
3626
  margin-right: 0.25rem;
3518
3627
  }
3519
3628
  `;
3520
- const ChartTooltipName = styled.div.withConfig({ displayName: "ChartTooltipName", componentId: "sc-sm4g0t" }) `
3629
+ const ChartTooltipName = styled.div.withConfig({ displayName: "ChartTooltipName", componentId: "sc-16ov53z" }) `
3521
3630
  margin-right: 1rem;
3522
3631
  opacity: 0.65;
3523
3632
  `;
3524
- const PieChartCenter = styled.div.withConfig({ displayName: "PieChartCenter", componentId: "sc-1le93jy" }) `
3633
+ const PieChartCenter = styled.div.withConfig({ displayName: "PieChartCenter", componentId: "sc-5m536c" }) `
3525
3634
  position: absolute;
3526
3635
  top: 50%;
3527
3636
  left: 50%;
@@ -3530,7 +3639,7 @@ const PieChartCenter = styled.div.withConfig({ displayName: "PieChartCenter", co
3530
3639
  font-weight: bold;
3531
3640
  font-size: 1.5rem;
3532
3641
  `;
3533
- const ChartWrapperContainer = styled.div.withConfig({ displayName: "ChartWrapperContainer", componentId: "sc-1f4x9zv" }) `
3642
+ const ChartWrapperContainer = styled.div.withConfig({ displayName: "ChartWrapperContainer", componentId: "sc-1ixngl" }) `
3534
3643
  position: relative;
3535
3644
  width: 100%;
3536
3645
  `;
@@ -3539,7 +3648,7 @@ const ChartWrapperContainer = styled.div.withConfig({ displayName: "ChartWrapper
3539
3648
  * на неё вешается ref для ResizeObserver — меряем обе оси без цикла. Плоский div (не Flex uilib), как соседний
3540
3649
  * ChartWrapperContainer, чтобы ref гарантированно дошёл до DOM-узла. Тело графика центрируется внутри.
3541
3650
  */
3542
- const ChartFillMeasure = styled.div.withConfig({ displayName: "ChartFillMeasure", componentId: "sc-1r4oerb" }) `
3651
+ const ChartFillMeasure = styled.div.withConfig({ displayName: "ChartFillMeasure", componentId: "sc-3rqqmt" }) `
3543
3652
  position: relative;
3544
3653
  display: flex;
3545
3654
  align-items: center;
@@ -3557,7 +3666,7 @@ const ChartFillMeasure = styled.div.withConfig({ displayName: "ChartFillMeasure"
3557
3666
  display: block;
3558
3667
  }
3559
3668
  `;
3560
- const Tooltip = styled.div.withConfig({ displayName: "Tooltip", componentId: "sc-1va8cy3" }) `
3669
+ const Tooltip = styled.div.withConfig({ displayName: "Tooltip", componentId: "sc-k91v0" }) `
3561
3670
  position: relative;
3562
3671
  border-radius: 0.25rem;
3563
3672
  background-color: rgba(28, 33, 48);
@@ -3662,7 +3771,7 @@ const LineChartStyles = styled.createGlobalStyle `
3662
3771
  fill: ${({ theme: { palette } }) => palette.textDisabled};
3663
3772
  }
3664
3773
  `;
3665
- const StyledBarChart = styled(charts.BarChart).withConfig({ displayName: "StyledBarChart", componentId: "sc-1l1kx80" }) `
3774
+ const StyledBarChart = styled(charts.BarChart).withConfig({ displayName: "StyledBarChart", componentId: "sc-16gz38d" }) `
3666
3775
  .domain {
3667
3776
  display: none;
3668
3777
  }
@@ -4550,6 +4659,79 @@ const getAttributeConfigurationByName = (attributes, name) => {
4550
4659
  return attributes[name];
4551
4660
  };
4552
4661
 
4662
+ const EMPTY_AXIS = {};
4663
+ const warnedLegacySources = new Set();
4664
+ // Сборщик консьюмера подставляет `process.env.NODE_ENV` строкой, поэтому выражение должно быть
4665
+ // ровно таким — без `typeof process` и без `?.`. Сам `process` webpack 5 в браузер не завозит:
4666
+ // проверка `typeof process` отключила бы предупреждения и в dev. Консьюмер без сборщика получит
4667
+ // ReferenceError — он гасится, и предупреждений просто нет.
4668
+ const isDevEnvironment = (() => {
4669
+ try {
4670
+ return process.env.NODE_ENV !== "production";
4671
+ }
4672
+ catch {
4673
+ return false;
4674
+ }
4675
+ })();
4676
+ /**
4677
+ * Один раз на источник сообщает, что конфиг остался на устаревшей плоской форме.
4678
+ *
4679
+ * Без такого маркера момент «легаси закончилось» не наступает никогда, и временный слой
4680
+ * совместимости становится постоянным.
4681
+ */
4682
+ const warnLegacyAxis = ({ dataSourceName, attributeName }) => {
4683
+ if (!isDevEnvironment)
4684
+ return;
4685
+ const key = `${dataSourceName ?? "?"}:${attributeName ?? "?"}`;
4686
+ if (warnedLegacySources.has(key))
4687
+ return;
4688
+ warnedLegacySources.add(key);
4689
+ // eslint-disable-next-line no-console
4690
+ console.warn(`[Dashboard] Источник "${key}" использует устаревшие поля оси (chartAxis/axisColor/hideAxis). ` +
4691
+ "Перепишите их в объект axis: { type, side, color, title, hide }. " +
4692
+ "Слой совместимости временный.");
4693
+ };
4694
+ /**
4695
+ * Настройки оси источника.
4696
+ *
4697
+ * Читает `axis`, а при его отсутствии собирает объект из устаревших плоских полей
4698
+ * `chartAxis` / `axisColor` / `hideAxis`. Заданный `axis` имеет приоритет: узел, который уже
4699
+ * правили в редакторе, не должен откатываться к старым значениям.
4700
+ */
4701
+ const resolveAxis = (source) => {
4702
+ if (!source)
4703
+ return EMPTY_AXIS;
4704
+ const { axis, chartAxis, axisColor, hideAxis } = source;
4705
+ if (axis)
4706
+ return axis;
4707
+ if (chartAxis === undefined && axisColor === undefined && hideAxis === undefined)
4708
+ return EMPTY_AXIS;
4709
+ warnLegacyAxis(source);
4710
+ return {
4711
+ type: chartAxis === "x" ? "x" : "y",
4712
+ color: axisColor,
4713
+ hide: hideAxis,
4714
+ };
4715
+ };
4716
+ /**
4717
+ * Источник питает ось значений.
4718
+ *
4719
+ * Отсутствие `axis.type` означает `"y"`: конфиг без явного типа — самый частый случай,
4720
+ * и раньше такой источник молча выпадал из графика.
4721
+ */
4722
+ const isValueAxis = (source) => (resolveAxis(source).type ?? "y") === "y";
4723
+ /** Сторона оси; отсутствие `axis.side` означает `"left"`. */
4724
+ const getAxisSide = (source) => resolveAxis(source).side ?? "left";
4725
+ /** Источник выводится на правой оси. */
4726
+ const isRightAxis = (source) => getAxisSide(source) === "right";
4727
+ /**
4728
+ * Запись описывает ось чарта, а не источник фильтра.
4729
+ *
4730
+ * `BarChartFilter` собирает `relatedConfig` вручную и осей не задаёт — ему нужна исходная палитра,
4731
+ * а не растянутая под число элементов.
4732
+ */
4733
+ const hasAxisConfig = ({ axis, chartAxis }) => axis !== undefined || chartAxis !== undefined;
4734
+
4553
4735
  const getAttributesConfiguration = (layer) => {
4554
4736
  const layerAttributeConfiguration = layer?.configuration?.attributesConfiguration ?? {};
4555
4737
  const { geometryAttribute, idAttribute } = layerAttributeConfiguration;
@@ -4574,7 +4756,7 @@ const getAttributesConfiguration = (layer) => {
4574
4756
 
4575
4757
  const formatChartRelatedValue = (t, value, layerInfo, relatedAttributes) => {
4576
4758
  const layerDefinition = getAttributesConfiguration(layerInfo);
4577
- const relatedAxis = relatedAttributes.find(({ chartAxis }) => chartAxis === "y");
4759
+ const relatedAxis = relatedAttributes.find(isValueAxis);
4578
4760
  const attribute = getAttributeConfigurationByName(layerDefinition.attributes, relatedAxis?.attributeName);
4579
4761
  return attribute
4580
4762
  ? formatAttributeValue({ t, type: attribute.type, value, stringFormat: attribute.stringFormat })
@@ -5419,11 +5601,11 @@ const getAttributeValue = (element, attributes) => {
5419
5601
  return typeof value === "string" && maxLength && maxLength < value.length ? (jsxRuntime.jsx(TextTrim, { maxLength: maxLength, wordBreak: wordBreak, expandable: expandable, lineBreak: lineBreak, children: value })) : (value);
5420
5602
  };
5421
5603
 
5422
- const getChartAxes = (chartElement) => chartElement?.options?.relatedDataSources?.filter(({ chartAxis }) => chartAxis === "y");
5604
+ const getChartAxes = (chartElement) => chartElement?.options?.relatedDataSources?.filter(isValueAxis);
5423
5605
 
5424
5606
  const getChartFilterName = (relatedDataSources) => {
5425
5607
  const relatedAttributes = relatedDataSources || [];
5426
- const axes = relatedAttributes.filter(({ chartAxis }) => chartAxis === "y");
5608
+ const axes = relatedAttributes.filter(isValueAxis);
5427
5609
  return axes?.[0]?.filterName;
5428
5610
  };
5429
5611
 
@@ -7053,6 +7235,96 @@ const useFetchImageWithAuth = (url) => {
7053
7235
  return isExternal ? url : blobUrl;
7054
7236
  };
7055
7237
 
7238
+ /**
7239
+ * Оси значений линейного графика и атрибуты, по `stringFormat` которых форматируются деления.
7240
+ *
7241
+ * У левой и правой осей это разные атрибуты, поэтому они резолвятся по отдельности: иначе на
7242
+ * правой оси окажутся единицы измерения левой.
7243
+ */
7244
+ const useChartAxisAttributes = ({ dataSources, relatedAttributes }) => {
7245
+ const { layerInfos, dataSources: fetchedDataSources } = useWidgetContext();
7246
+ const axes = React.useMemo(() => (relatedAttributes || []).filter(isValueAxis), [relatedAttributes]);
7247
+ const strokeColors = React.useMemo(() => axes.map(related => resolveAxis(related).color), [axes]);
7248
+ const leftAxis = React.useMemo(() => axes.find(related => !isRightAxis(related)) ?? axes[0] ?? relatedAttributes?.[0], [axes, relatedAttributes]);
7249
+ const rightAxis = React.useMemo(() => axes.find(isRightAxis), [axes]);
7250
+ const resolveAxisAttribute = React.useCallback((related) => {
7251
+ if (!related)
7252
+ return undefined;
7253
+ const layerInfo = getDataSourceLayerInfo({
7254
+ layerInfos,
7255
+ configDataSource: dataSources?.find(({ name }) => name === related.dataSourceName),
7256
+ fetchedDataSource: getDataSource(related.dataSourceName, fetchedDataSources),
7257
+ });
7258
+ return getLayerInfoAttribute(layerInfo, related.attributeName);
7259
+ }, [dataSources, fetchedDataSources, layerInfos]);
7260
+ const leftAxisAttribute = React.useMemo(() => resolveAxisAttribute(leftAxis), [resolveAxisAttribute, leftAxis]);
7261
+ const rightAxisAttribute = React.useMemo(() => resolveAxisAttribute(rightAxis), [resolveAxisAttribute, rightAxis]);
7262
+ return { axes, strokeColors, leftAxisAttribute, rightAxisAttribute };
7263
+ };
7264
+
7265
+ /**
7266
+ * Форматтеры делений левой и правой шкал линейного графика — по `stringFormat` своего атрибута.
7267
+ *
7268
+ * Отдаются в `tickFormat` генератора оси, а не подменяют текст после отрисовки: d3 измеряет ширину
7269
+ * шкалы по уже нарисованным подписям и по ней сдвигает поле графика. Подмена текста задним числом
7270
+ * (`30` → `30,0`) делала подпись шире замера, и она заезжала на подпись оси слева.
7271
+ */
7272
+ const useChartAxisTickFormat = ({ dataSources, relatedAttributes }) => {
7273
+ const { t } = useGlobalContext();
7274
+ const { leftAxisAttribute, rightAxisAttribute } = useChartAxisAttributes({ dataSources, relatedAttributes });
7275
+ const createTickFormat = React.useCallback((attribute) => (value) => {
7276
+ if (!value)
7277
+ return "0";
7278
+ return attribute
7279
+ ? String(formatAttributeValue({
7280
+ t,
7281
+ type: attribute.type,
7282
+ value: Number(value),
7283
+ stringFormat: attribute.stringFormat,
7284
+ noUnits: true,
7285
+ }))
7286
+ : String(value);
7287
+ }, [t]);
7288
+ const formatLeftTick = React.useMemo(() => createTickFormat(leftAxisAttribute), [createTickFormat, leftAxisAttribute]);
7289
+ const formatRightTick = React.useMemo(() => createTickFormat(rightAxisAttribute), [createTickFormat, rightAxisAttribute]);
7290
+ return { formatLeftTick, formatRightTick };
7291
+ };
7292
+
7293
+ /**
7294
+ * Подписи осей линейного графика и место, которое они занимают над телом графика.
7295
+ *
7296
+ * Режим размещения общий для всех осей (`options.axis.titlePosition`), текст — у каждой оси свой.
7297
+ * Цвет подписи отдельной опцией не задаётся: одна серия на стороне — подпись в её цвет, несколько —
7298
+ * нейтральная, потому что тогда подпись описывает всю ось, а не одну из линий. Ширину вертикальных
7299
+ * подписей здесь не считаем — она зависит от числа строк текста и измеряется по факту.
7300
+ */
7301
+ const useChartAxisTitles = ({ options, axes, isLineChart }) => {
7302
+ const axisTitlePosition = options?.axis?.titlePosition ?? "side";
7303
+ const axisTitles = React.useMemo(() => {
7304
+ const pick = (side) => {
7305
+ const sideAxes = axes.filter(related => getAxisSide(related) === side);
7306
+ const title = sideAxes.map(resolveAxis).find(axis => axis.title)?.title;
7307
+ if (!title)
7308
+ return undefined;
7309
+ return { title, color: sideAxes.length === 1 ? resolveAxis(sideAxes[0]).color : undefined };
7310
+ };
7311
+ return { left: pick("left"), right: pick("right") };
7312
+ }, [axes]);
7313
+ const hasAxisTitles = !!(isLineChart && (axisTitles.left || axisTitles.right));
7314
+ const hasTopTitles = hasAxisTitles && axisTitlePosition === "top";
7315
+ const hasSideTitles = hasAxisTitles && axisTitlePosition === "side";
7316
+ const hideLeftAxis = React.useMemo(() => axes.filter(related => !isRightAxis(related)).every(related => resolveAxis(related).hide), [axes]);
7317
+ const hideRightAxis = React.useMemo(() => axes.filter(isRightAxis).every(related => resolveAxis(related).hide), [axes]);
7318
+ return {
7319
+ axisTitles,
7320
+ hasTopTitles,
7321
+ hasSideTitles,
7322
+ axisTitlesHeight: hasTopTitles ? AXIS_TITLE_TOP_HEIGHT : 0,
7323
+ hideLeftAxis,
7324
+ hideRightAxis,
7325
+ };
7326
+ };
7327
+
7056
7328
  const HEIGHT_OFFSET = 20;
7057
7329
  const FILL_OPACITY = 0.28;
7058
7330
  const lineGenerator = line()
@@ -7098,20 +7370,10 @@ function wrap() {
7098
7370
  }
7099
7371
  }
7100
7372
  const useChartChange = ({ dataSources, chartId, width, height, relatedAttributes, defaultColor, fontColor, markers, showMarkers, }) => {
7101
- const { t } = useGlobalContext();
7102
- const { layerInfos, dataSources: fetchedDataSources } = useWidgetContext();
7103
- const strokeColors = relatedAttributes
7104
- .filter(({ chartAxis }) => chartAxis === "y")
7105
- .map(({ axisColor }) => axisColor);
7106
- const yAxis = React.useMemo(() => relatedAttributes?.find(({ chartAxis }) => chartAxis === "y") ?? relatedAttributes?.[0], [relatedAttributes]);
7107
- const yAxisAttribute = React.useMemo(() => {
7108
- const layerInfo = getDataSourceLayerInfo({
7109
- layerInfos,
7110
- configDataSource: dataSources?.find(({ name }) => name === yAxis?.dataSourceName),
7111
- fetchedDataSource: getDataSource(yAxis?.dataSourceName, fetchedDataSources),
7112
- });
7113
- return getLayerInfoAttribute(layerInfo, yAxis?.attributeName);
7114
- }, [dataSources, fetchedDataSources, layerInfos, yAxis]);
7373
+ const { strokeColors } = useChartAxisAttributes({
7374
+ dataSources,
7375
+ relatedAttributes,
7376
+ });
7115
7377
  const ref = React.useRef({
7116
7378
  path: null,
7117
7379
  area: null,
@@ -7164,22 +7426,9 @@ const useChartChange = ({ dataSources, chartId, width, height, relatedAttributes
7164
7426
  svg
7165
7427
  .selectAll(`.${charts.lineChartClassNames.lineChartXScaleGlobal} .tick text`)
7166
7428
  .attr("y", 16);
7167
- svg
7168
- .selectAll(`.${charts.lineChartClassNames.lineChartYScaleGlobal} .tick text`)
7169
- .text(item => {
7170
- if (!item)
7171
- return 0;
7172
- return yAxisAttribute
7173
- ? formatAttributeValue({
7174
- t,
7175
- type: yAxisAttribute.type,
7176
- value: item,
7177
- stringFormat: yAxisAttribute.stringFormat,
7178
- noUnits: true,
7179
- })
7180
- : item;
7181
- })
7182
- .each(wrap);
7429
+ // Текст делений форматируется ещё в генераторе оси (useChartAxisTickFormat), до замера ширины
7430
+ // шкалы; здесь его только укорачиваем.
7431
+ svg.selectAll(`.${charts.lineChartClassNames.lineChartYScaleGlobal} .tick text`).each(wrap);
7183
7432
  svg.selectAll(".tick text").attr("style", `color: ${fontColor}`);
7184
7433
  const global = svg.select(`.${charts.lineChartClassNames.lineChartLinesGlobal}`);
7185
7434
  const lineChartLines = svg.selectAll(`.${charts.lineChartClassNames.lineChartLine}`);
@@ -7215,7 +7464,6 @@ const useChartChange = ({ dataSources, chartId, width, height, relatedAttributes
7215
7464
  svg.append("defs").html(() => defs.join(""));
7216
7465
  }, [
7217
7466
  fontColor,
7218
- yAxisAttribute,
7219
7467
  chartId,
7220
7468
  strokeColors,
7221
7469
  defaultColor,
@@ -7223,7 +7471,6 @@ const useChartChange = ({ dataSources, chartId, width, height, relatedAttributes
7223
7471
  onChange,
7224
7472
  showMarkers,
7225
7473
  markers,
7226
- t,
7227
7474
  ]);
7228
7475
  return [customize, onChange];
7229
7476
  };
@@ -7380,10 +7627,10 @@ const useChartData = ({ element, type }) => {
7380
7627
  const configDataSources = useConfigDataSources(type);
7381
7628
  const relatedAttributes = React.useMemo(() => element?.options?.relatedDataSources || [], [element?.options?.relatedDataSources]);
7382
7629
  const loading = React.useMemo(() => !!relatedAttributes?.length &&
7383
- !dataSources?.some(({ name }) => relatedAttributes.some(({ chartAxis, dataSourceName }) => chartAxis === "y" && dataSourceName === name)), [dataSources, relatedAttributes]);
7630
+ !dataSources?.some(({ name }) => relatedAttributes.some(related => isValueAxis(related) && related.dataSourceName === name)), [dataSources, relatedAttributes]);
7384
7631
  const fetchedData = React.useMemo(() => {
7385
7632
  const isRelated = !!relatedAttributes?.length;
7386
- const filteredAttributes = relatedAttributes.filter(({ chartAxis }) => chartAxis === "y");
7633
+ const filteredAttributes = relatedAttributes.filter(isValueAxis);
7387
7634
  if (!element)
7388
7635
  return [];
7389
7636
  if (isRelated) {
@@ -7391,7 +7638,7 @@ const useChartData = ({ element, type }) => {
7391
7638
  return [];
7392
7639
  return filteredAttributes
7393
7640
  .map(relatedAxis => {
7394
- const { dataSourceName, axisColor, attributeName, attributeUnits } = relatedAxis;
7641
+ const { dataSourceName, attributeName, attributeUnits } = relatedAxis;
7395
7642
  const dataSource = configDataSources.find(({ name }) => name === dataSourceName);
7396
7643
  const featureDataSource = getDataSource(dataSourceName, dataSources);
7397
7644
  const layerInfo = getDataSourceLayerInfo({
@@ -7411,7 +7658,10 @@ const useChartData = ({ element, type }) => {
7411
7658
  layerInfo,
7412
7659
  })
7413
7660
  : null,
7414
- color: axisColor,
7661
+ color: resolveAxis(relatedAxis).color,
7662
+ // Сторона едет вместе с данными: серии без значений отсеиваются ниже, и сопоставлять
7663
+ // их с осями по индексу уже нельзя.
7664
+ axisSide: getAxisSide(relatedAxis),
7415
7665
  attributeName,
7416
7666
  attributeUnits,
7417
7667
  dataSourceName,
@@ -7424,6 +7674,7 @@ const useChartData = ({ element, type }) => {
7424
7674
  return [
7425
7675
  {
7426
7676
  items: getDataFromAttributes(t, element, attributes),
7677
+ axisSide: "left",
7427
7678
  layerInfo: null,
7428
7679
  attributeName: null,
7429
7680
  attributeUnits: null,
@@ -7443,7 +7694,7 @@ const useChartData = ({ element, type }) => {
7443
7694
  ]);
7444
7695
  const isResolved = React.useMemo(() => !relatedAttributes?.length ||
7445
7696
  relatedAttributes
7446
- .filter(({ chartAxis }) => chartAxis === "y")
7697
+ .filter(isValueAxis)
7447
7698
  .every(({ dataSourceName }) => Array.isArray(getDataSource(dataSourceName, dataSources)?.features)), [relatedAttributes, dataSources]);
7448
7699
  const [data, setData] = React.useState(fetchedData);
7449
7700
  React.useEffect(() => {
@@ -8975,6 +9226,15 @@ const ChartLegendContainer = styled(uilibGl.Flex).withConfig({ displayName: "Cha
8975
9226
  flex-direction: column;
8976
9227
  flex-wrap: wrap;
8977
9228
  justify-content: ${({ twoColumns }) => (twoColumns ? "flex-start" : "center")};
9229
+
9230
+ /* Явная опция column перебивает раскладку, которую легенде навязывает ChartContainer
9231
+ (под графиком — в ряд, сбоку — столбиком). Без опции поведение прежнее. */
9232
+ ${({ $column }) => $column !== undefined &&
9233
+ styled.css `
9234
+ &&& {
9235
+ flex-direction: ${$column ? "column" : "row"};
9236
+ }
9237
+ `}
8978
9238
  `;
8979
9239
  const ChartLegendItem = styled(uilibGl.Flex).withConfig({ displayName: "ChartLegendItem", componentId: "sc-16m9piw" }) `
8980
9240
  cursor: ${({ onClick }) => (onClick ? "pointer" : "default")};
@@ -9043,12 +9303,129 @@ const ContainerChart = styled(uilibGl.Flex).withConfig({ displayName: "Container
9043
9303
  `}
9044
9304
  `;
9045
9305
  const ContainerLegend = styled(uilibGl.Flex).withConfig({ displayName: "ContainerLegend", componentId: "sc-z1n1s8" }) ``;
9046
- const ContainerUnits = styled.div.withConfig({ displayName: "ContainerUnits", componentId: "sc-1vurgy9" }) `
9306
+ /**
9307
+ * Выравнивание строки по полю графика: ширина тела графика, а внутренние отступы — до краёв поля
9308
+ * (колонка подписи оси и значения шкалы). Тело графика центрируется в слоте, поэтому и строка
9309
+ * центрируется. Правило поднято `&&&` над шириной, которую строкам задаёт `ContainerValue`.
9310
+ */
9311
+ const plotInsetsMixin = styled.css `
9312
+ ${({ $insets }) => $insets &&
9313
+ styled.css `
9314
+ &&& {
9315
+ box-sizing: border-box;
9316
+ align-self: center;
9317
+ width: ${$insets.width}px;
9318
+ max-width: 100%;
9319
+ padding-left: ${$insets.left}px;
9320
+ padding-right: ${$insets.right}px;
9321
+ }
9322
+ `}
9323
+ `;
9324
+ /**
9325
+ * Строка ждёт, пока график сообщит положение поля: место занимает, но не видна. Иначе до первой
9326
+ * отрисовки d3 она стояла бы по краю контейнера и потом прыгала на место.
9327
+ */
9328
+ const awaitingPlotMixin = styled.css `
9329
+ ${({ $awaitingPlot }) => $awaitingPlot &&
9330
+ styled.css `
9331
+ visibility: hidden;
9332
+ `}
9333
+ `;
9334
+ /**
9335
+ * Строка под телом графика: подпись оси X и, при `legendInline`, легенда справа от неё.
9336
+ *
9337
+ * `$offsetTop` подтягивает строку к подписям оси X: под ними внутри SVG остаётся пустая полоса,
9338
+ * и без поправки зазор получился бы втрое больше макетного.
9339
+ */
9340
+ const ChartFooter = styled(uilibGl.Flex).withConfig({ displayName: "ChartFooter", componentId: "sc-nii28w" }) `
9341
+ align-items: center;
9342
+ flex-wrap: nowrap;
9343
+ gap: 0.5rem;
9344
+ width: 100%;
9345
+ margin-top: ${({ $offsetTop }) => $offsetTop ?? 0}px;
9346
+ justify-content: ${({ $align }) => $align};
9347
+
9348
+ ${ContainerLegend} {
9349
+ justify-content: flex-end;
9350
+ margin-left: auto;
9351
+ }
9352
+
9353
+ /* Легенда в строке с подписью: записи прижаты вправо, без отступа сверху, который ей задаёт
9354
+ ContainerValue, и без нижнего отступа записей — иначе она съедет вниз относительно подписи.
9355
+ Прижимать нужно сами записи: обёртки легенды — Flex из uilib-gl с width: 100%, и выравнивание
9356
+ внешнего контейнера до записей не доходит, а своё у легенды — по центру. */
9357
+ &&& ${ChartLegendContainer} {
9358
+ justify-content: flex-end;
9359
+ margin-top: 0;
9360
+ }
9361
+
9362
+ &&& ${ChartLegendItem} {
9363
+ margin-bottom: 0;
9364
+ }
9365
+
9366
+ ${plotInsetsMixin};
9367
+ ${awaitingPlotMixin};
9368
+ `;
9369
+ const ChartXAxisTitle = styled.div.withConfig({ displayName: "ChartXAxisTitle", componentId: "sc-1vsgzi9" }) `
9370
+ font-size: 0.625rem;
9371
+ line-height: 0.75rem;
9372
+ white-space: nowrap;
9373
+ color: ${({ $color, theme: { palette } }) => $color || palette.textDisabled};
9374
+ `;
9375
+ /**
9376
+ * Строка легенды под графиком. `$gapTop` заменяет отступ сверху, который легенде задаёт `ContainerValue`;
9377
+ * `$offsetTop` сдвигает саму строку — подтягивает её к подписям оси X, как строку с подписью оси.
9378
+ *
9379
+ * `$justify` выравнивает сами записи. Выравнивание строки (`justifyContent`) до них не доходит: обёртки
9380
+ * легенды — Flex из uilib-gl с `width: 100%`, а записи внутри легенда всегда центрирует.
9381
+ */
9382
+ const ChartLegendRow = styled(ContainerLegend).withConfig({ displayName: "ChartLegendRow", componentId: "sc-1a12wrz" }) `
9383
+ ${({ $offsetTop }) => $offsetTop !== undefined &&
9384
+ styled.css `
9385
+ margin-top: ${$offsetTop}px;
9386
+ `}
9387
+
9388
+ ${({ $gapTop }) => $gapTop !== undefined &&
9389
+ styled.css `
9390
+ &&& ${ChartLegendContainer} {
9391
+ margin-top: ${$gapTop}px;
9392
+ }
9393
+ `}
9394
+
9395
+ ${({ $justify }) => $justify &&
9396
+ styled.css `
9397
+ &&& ${ChartLegendContainer} {
9398
+ justify-content: ${$justify};
9399
+ }
9400
+ `}
9401
+
9402
+ ${plotInsetsMixin};
9403
+ ${awaitingPlotMixin};
9404
+ `;
9405
+ /**
9406
+ * Колонка «график + подпись оси X» для легенды сбоку (`twoColumns`). Без неё подпись встала бы третьим
9407
+ * элементом ряда рядом с легендой, а не под графиком.
9408
+ */
9409
+ const ChartWithFooter = styled(uilibGl.Flex).withConfig({ displayName: "ChartWithFooter", componentId: "sc-140gv2s" }) `
9410
+ flex-direction: column;
9411
+ flex-wrap: nowrap;
9412
+ min-width: 0;
9413
+
9414
+ ${({ $fill }) => $fill &&
9415
+ styled.css `
9416
+ min-height: 0;
9417
+ `}
9418
+
9419
+ &&& ${ContainerChart} {
9420
+ width: 100%;
9421
+ }
9422
+ `;
9423
+ const ContainerUnits = styled.div.withConfig({ displayName: "ContainerUnits", componentId: "sc-vj15z6" }) `
9047
9424
  margin-left: 0.5rem;
9048
9425
  white-space: nowrap;
9049
9426
  font-size: 0.75rem;
9050
9427
  `;
9051
- const ContainerValue = styled(uilibGl.Flex).withConfig({ displayName: "ContainerValue", componentId: "sc-aj1rs9" }) `
9428
+ const ContainerValue = styled(uilibGl.Flex).withConfig({ displayName: "ContainerValue", componentId: "sc-1bnfj5v" }) `
9052
9429
  justify-content: flex-end;
9053
9430
  align-items: center;
9054
9431
  flex-wrap: nowrap;
@@ -9067,7 +9444,7 @@ const ContainerValue = styled(uilibGl.Flex).withConfig({ displayName: "Container
9067
9444
  width: ${({ column }) => (column ? "100%" : "auto")};
9068
9445
  }
9069
9446
 
9070
- ${ContainerChart}, ${ContainerLegend} {
9447
+ ${ContainerChart}, ${ContainerLegend}, ${ChartWithFooter} {
9071
9448
  width: ${({ column }) => (column ? "100%" : "50%")};
9072
9449
  }
9073
9450
 
@@ -9090,7 +9467,7 @@ const SizeIconMixin = styled.css `
9090
9467
  font-size: ${({ $fontSize }) => $fontSize}px !important;
9091
9468
  }
9092
9469
  `;
9093
- styled(uilibGl.Icon).withConfig({ displayName: "ContainerIcon", componentId: "sc-1w9w0bq" }) `
9470
+ styled(uilibGl.Icon).withConfig({ displayName: "ContainerIcon", componentId: "sc-18399bz" }) `
9094
9471
  width: auto;
9095
9472
  height: auto;
9096
9473
  margin-bottom: 0.5rem;
@@ -9104,7 +9481,7 @@ const SvgContainerColorMixin$1 = styled.css `
9104
9481
  fill: ${({ $fontColor }) => $fontColor};
9105
9482
  }
9106
9483
  `;
9107
- const SvgContainer$1 = styled.div.withConfig({ displayName: "SvgContainer", componentId: "sc-1qs5gmx" }) `
9484
+ const SvgContainer$1 = styled.div.withConfig({ displayName: "SvgContainer", componentId: "sc-8l0ajh" }) `
9108
9485
  &&& {
9109
9486
  min-width: ${({ $width }) => ($width ? `${$width}px` : "1rem")};
9110
9487
  max-width: ${({ $width }) => ($width ? `${$width}px` : "1rem")};
@@ -9116,7 +9493,7 @@ const SvgContainer$1 = styled.div.withConfig({ displayName: "SvgContainer", comp
9116
9493
  }
9117
9494
  }
9118
9495
  `;
9119
- const TwoColumnContainerWrapper = styled(Container).withConfig({ displayName: "TwoColumnContainerWrapper", componentId: "sc-1ogv4d3" }) `
9496
+ const TwoColumnContainerWrapper = styled(Container).withConfig({ displayName: "TwoColumnContainerWrapper", componentId: "sc-1yxeknf" }) `
9120
9497
  flex-direction: row;
9121
9498
  flex-wrap: nowrap;
9122
9499
  align-items: center;
@@ -9143,6 +9520,16 @@ const CameraContainer = React.memo(({ elementConfig, type, renderElement }) => {
9143
9520
  return (jsxRuntime.jsxs(ContainerRoot, { ...root, children: [jsxRuntime.jsx(ContainerBackground, { elementConfig: elementConfig, renderElement: renderElement }), jsxRuntime.jsx(ExpandableTitle, { elementConfig: elementConfig, type: type, renderElement: renderElement }), isVisible && (jsxRuntime.jsxs(Container, { ...body, children: [jsxRuntime.jsx(ContainerAlias, { hasBottomMargin: true, children: renderElement({ id: "alias" }) }), jsxRuntime.jsx(ContainerValue, { children: renderElement({ id: "value", wrap: false }) })] }))] }));
9144
9521
  });
9145
9522
 
9523
+ /**
9524
+ * Контекст, через который элемент `chart` сообщает контейнеру, где лежит поле графика.
9525
+ *
9526
+ * По макету подпись оси X и легенда выравниваются по полю графика, а не по краям контейнера.
9527
+ * Ширину шкал d3 вычисляет только при отрисовке, а подвал с подписью и легендой рисует
9528
+ * `ChartContainer` — снаружи элемента. Без контейнера (элемент сам по себе) провайдера нет,
9529
+ * и отчёт просто не отправляется.
9530
+ */
9531
+ const ChartPlotContext = React.createContext({});
9532
+
9146
9533
  /**
9147
9534
  * Контекст вписывания графика (`options.fill` контейнера Chart).
9148
9535
  *
@@ -9152,14 +9539,40 @@ const CameraContainer = React.memo(({ elementConfig, type, renderElement }) => {
9152
9539
  */
9153
9540
  const FillContext = React.createContext({ fill: false, fitHeight: false });
9154
9541
 
9542
+ const isSameInsets = (prev, next) => !!prev && prev.left === next.left && prev.right === next.right && prev.width === next.width;
9155
9543
  const ChartContainer = React.memo(({ elementConfig, isVisible, type, renderElement }) => {
9156
9544
  const { options, children } = elementConfig || {};
9157
- const { twoColumns, hideEmpty, height } = options || {};
9545
+ const { twoColumns, hideEmpty, height, legendInline } = options || {};
9158
9546
  const fill = options?.fill;
9159
9547
  const fillContextValue = React.useMemo(() => ({ fill: !!fill, fitHeight: !!fill && height != null }), [fill, height]);
9548
+ // Поле графика отчитывается после каждой отрисовки d3; одинаковые значения не пишем в стейт,
9549
+ // иначе отчёт и ре-рендер гоняли бы друг друга.
9550
+ const [plotInsets, setPlotInsets] = React.useState(null);
9551
+ const reportPlotInsets = React.useCallback((next) => setPlotInsets(prev => (isSameInsets(prev, next) ? prev : next)), []);
9552
+ const plotContextValue = React.useMemo(() => ({ reportPlotInsets }), [reportPlotInsets]);
9160
9553
  const aliasElement = children.find(child => child.id === "alias");
9161
9554
  const chartElement = children.find(child => child.id === "chart");
9162
9555
  const legendElement = children.find(child => child.id === "legend");
9556
+ const isLineChart = chartElement?.options?.chartType === "line";
9557
+ const xAxisTitle = chartElement?.options?.axis?.x?.title;
9558
+ const footerJustify = React.useMemo(() => CHART_AXIS_ALIGN_JUSTIFY[chartElement?.options?.axis?.x?.align ?? "center"], [chartElement?.options?.axis?.x?.align]);
9559
+ // Легенда встаёт в строку с подписью оси X только когда та есть и легенда не вынесена вбок.
9560
+ const isLegendInline = React.useMemo(() => !!legendInline && !twoColumns && !!xAxisTitle, [legendInline, twoColumns, xAxisTitle]);
9561
+ // По макету подпись оси X и легенда у линейного графика выравниваются по полю графика, а не по
9562
+ // контейнеру. Легенда сбоку (twoColumns) живёт в своей колонке и в выравнивании не участвует.
9563
+ const footerInsets = React.useMemo(() => (isLineChart ? plotInsets : null), [isLineChart, plotInsets]);
9564
+ const isPlotLegend = isLineChart && !twoColumns;
9565
+ // Положение поля становится известно только после отрисовки: LineChart рисует d3 в useEffect, уже
9566
+ // после первого кадра. До отчёта подвал встал бы по краю контейнера и затем прыгнул на место, поэтому
9567
+ // прячем его — место под ним сохраняется, а появляется он вместе с линиями графика.
9568
+ const isAwaitingPlot = isLineChart && !plotInsets;
9569
+ // Пока подпись оси X не задана, легенда выравнивается прежней опцией `center` (по умолчанию — к левой
9570
+ // границе поля графика); с подписью — по ней, иначе подпись и легенда разъезжаются по разным краям.
9571
+ const legendJustify = React.useMemo(() => {
9572
+ if (xAxisTitle)
9573
+ return footerJustify;
9574
+ return legendElement?.options?.center ? "center" : "flex-start";
9575
+ }, [xAxisTitle, footerJustify, legendElement?.options?.center]);
9163
9576
  const { root, body } = useContainerRoot({ elementConfig });
9164
9577
  const { data, loading } = useChartData({
9165
9578
  element: chartElement,
@@ -9171,7 +9584,12 @@ const ChartContainer = React.memo(({ elementConfig, isVisible, type, renderEleme
9171
9584
  const hasItems = !!data[0]?.items?.length;
9172
9585
  if (!loading && !hasItems && hideEmpty)
9173
9586
  return null;
9174
- return (jsxRuntime.jsxs(ContainerRoot, { ...root, children: [jsxRuntime.jsx(ContainerBackground, { elementConfig: elementConfig, renderElement: renderElement }), jsxRuntime.jsx(ExpandableTitle, { elementConfig: elementConfig, type: type, renderElement: renderElement }), isVisible && (jsxRuntime.jsxs(Container, { ...body, isColumn: true, children: [aliasElement && jsxRuntime.jsx(ContainerAlias, { hasBottomMargin: true, children: renderElement({ id: "alias" }) }), jsxRuntime.jsx(ContainerValue, { column: !twoColumns, alignItems: twoColumns && fill ? "stretch" : "center", "$fill": !!fill, children: hasItems ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(ContainerChart, { "$fill": !!fill, children: jsxRuntime.jsx(FillContext.Provider, { value: fillContextValue, children: renderElement({ id: "chart" }) }) }), jsxRuntime.jsx(ContainerLegend, { justifyContent: legendElement?.options?.center ? "center" : "flex-start", children: renderElement({ id: "legend" }) })] })) : (jsxRuntime.jsx(jsxRuntime.Fragment, { children: "\u2014" })) })] }))] }));
9587
+ const chartSlot = (jsxRuntime.jsx(ContainerChart, { "$fill": !!fill, children: jsxRuntime.jsx(FillContext.Provider, { value: fillContextValue, children: jsxRuntime.jsx(ChartPlotContext.Provider, { value: plotContextValue, children: renderElement({ id: "chart" }) }) }) }));
9588
+ const footer = xAxisTitle ? (jsxRuntime.jsxs(ChartFooter, { "$align": footerJustify, "$insets": footerInsets, "$offsetTop": isLineChart ? X_AXIS_TITLE_GAP - LINE_CHART_X_LABELS_BLANK : 0, "$awaitingPlot": isAwaitingPlot, children: [jsxRuntime.jsx(ChartXAxisTitle, { children: xAxisTitle }), isLegendInline && jsxRuntime.jsx(ContainerLegend, { children: renderElement({ id: "legend" }) })] })) : null;
9589
+ // Легенда линейного графика: под подписью оси X — через X_AXIS_TITLE_LEGEND_GAP, без подписи — на её
9590
+ // месте, вплотную под подписями делений оси X. У остальных графиков — прежняя раскладка общей легенды.
9591
+ const legendRow = isLegendInline ? null : (jsxRuntime.jsx(ChartLegendRow, { justifyContent: legendJustify, "$insets": isPlotLegend ? footerInsets : null, "$gapTop": isPlotLegend ? (xAxisTitle ? X_AXIS_TITLE_LEGEND_GAP : 0) : undefined, "$offsetTop": isPlotLegend && !xAxisTitle ? X_AXIS_TITLE_GAP - LINE_CHART_X_LABELS_BLANK : undefined, "$justify": isPlotLegend ? legendJustify : undefined, "$awaitingPlot": isPlotLegend && isAwaitingPlot, children: renderElement({ id: "legend" }) }));
9592
+ return (jsxRuntime.jsxs(ContainerRoot, { ...root, children: [jsxRuntime.jsx(ContainerBackground, { elementConfig: elementConfig, renderElement: renderElement }), jsxRuntime.jsx(ExpandableTitle, { elementConfig: elementConfig, type: type, renderElement: renderElement }), isVisible && (jsxRuntime.jsxs(Container, { ...body, isColumn: true, children: [aliasElement && jsxRuntime.jsx(ContainerAlias, { hasBottomMargin: true, children: renderElement({ id: "alias" }) }), jsxRuntime.jsx(ContainerValue, { column: !twoColumns, alignItems: twoColumns && fill ? "stretch" : "center", "$fill": !!fill, children: hasItems ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [twoColumns && footer ? (jsxRuntime.jsxs(ChartWithFooter, { "$fill": !!fill, children: [chartSlot, footer] })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [chartSlot, footer] })), legendRow] })) : (jsxRuntime.jsx(jsxRuntime.Fragment, { children: "\u2014" })) })] }))] }));
9175
9593
  });
9176
9594
 
9177
9595
  const InnerContainerWrapper = styled.div.withConfig({ displayName: "InnerContainerWrapper", componentId: "sc-1a1fl7q" }) `
@@ -9529,6 +9947,7 @@ const DEFAULT_BAR_CHART_HEIGHT = 100;
9529
9947
  const BAR_CHART_HEADER_HEIGHT = 40;
9530
9948
  const BAR_CHART_FOOTER_MARGIN = 16;
9531
9949
  const LINE_CHART_LABEL_MARGIN = 20;
9950
+ const LINE_CHART_X_AXIS_PADDING = 15;
9532
9951
  const DEFAULT_BAR_CHART_MARGINS = {
9533
9952
  top: 0,
9534
9953
  left: 0,
@@ -15192,9 +15611,9 @@ const ElementImage = React.memo(({ type, elementConfig }) => {
15192
15611
  });
15193
15612
 
15194
15613
  const ElementLegend = React.memo(({ type, element, elementConfig }) => {
15195
- const { attributes, dataSources, expandedContainers } = useWidgetContext(type);
15614
+ const { attributes, expandedContainers } = useWidgetContext(type);
15196
15615
  const { options } = elementConfig || {};
15197
- const { twoColumns, chartId, relatedDataSources, fontSize } = options || {};
15616
+ const { twoColumns, column, chartId, relatedDataSources, fontSize } = options || {};
15198
15617
  const chartElement = React.useMemo(() => findAnd.returnFound(element?.children, { id: chartId }), [chartId, element?.children]);
15199
15618
  const { data, loading } = useChartData({
15200
15619
  element: chartElement,
@@ -15205,10 +15624,10 @@ const ElementLegend = React.memo(({ type, element, elementConfig }) => {
15205
15624
  if (!data[0]?.items?.length)
15206
15625
  return [];
15207
15626
  if (chartElement?.options?.chartType === "line") {
15208
- return axes.map(({ alias, axisColor }) => {
15627
+ return axes.map(related => {
15209
15628
  return {
15210
- name: alias,
15211
- color: axisColor,
15629
+ name: related.alias,
15630
+ color: resolveAxis(related).color,
15212
15631
  };
15213
15632
  });
15214
15633
  }
@@ -15222,7 +15641,7 @@ const ElementLegend = React.memo(({ type, element, elementConfig }) => {
15222
15641
  });
15223
15642
  }, [data, chartElement?.options?.chartType, relatedDataSources, axes, attributes]);
15224
15643
  return !chartElement?.options?.expanded ||
15225
- expandedContainers?.[chartElement.id] ? (jsxRuntime.jsx(ChartLegend, { data: legendData, loading: loading, chartElement: chartElement, twoColumns: twoColumns, fontSize: fontSize, type: type })) : null;
15644
+ expandedContainers?.[chartElement.id] ? (jsxRuntime.jsx(ChartLegend, { data: legendData, loading: loading, chartElement: chartElement, twoColumns: twoColumns, column: column, fontSize: fontSize, type: type })) : null;
15226
15645
  });
15227
15646
 
15228
15647
  const ExternalLink = styled(uilibGl.IconButton).attrs(() => ({
@@ -16790,7 +17209,11 @@ const getDataFromRelatedFeatures = ({ t, config, filters, relatedConfig, dataSou
16790
17209
  if (isOtherSliced) {
16791
17210
  data = data.slice(0, config?.options?.otherItems);
16792
17211
  }
16793
- const gradientArray = relatedConfig.chartAxis
17212
+ // Источник чарта отличается от источника фильтра наличием осей: BarChartFilter собирает
17213
+ // relatedConfig вручную и оси не задаёт, потому что ему нужна исходная палитра, а не растянутая
17214
+ // под число элементов. Гейт смотрит и на устаревший chartAxis — иначе у старых конфигов
17215
+ // молча поехали бы цвета столбцов и долей.
17216
+ const gradientArray = hasAxisConfig(relatedConfig)
16794
17217
  ? stretchPalette(colors, data.length)
16795
17218
  : colors;
16796
17219
  const filter = getConfigFilter(relatedConfig?.filterName, filters);
@@ -18856,6 +19279,15 @@ const StackBar = ({ data, filterName, type, alias, options, fill, renderElement,
18856
19279
  return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [(alias || showTotal) && (jsxRuntime.jsxs(StackBarHeader, { children: [jsxRuntime.jsx(StackBarAlias, { children: renderElement({ id: "alias" }) }), showTotal && (jsxRuntime.jsxs(StackBarTotal, { children: [jsxRuntime.jsx(StackBarValue, { children: formatValue(total) }), !!units && jsxRuntime.jsx(StackBarUnits, { children: units })] }))] })), groupTooltip ? (jsxRuntime.jsx(uilibGl.Tooltip, { placement: "top", arrow: true, content: renderGroupTooltip, children: ref => (jsxRuntime.jsx(StackBarContainer, { ref: ref, "$fill": fill, children: renderItems })) })) : (jsxRuntime.jsx(StackBarContainer, { "$fill": fill, children: renderItems }))] }));
18857
19280
  };
18858
19281
 
19282
+ /** Горизонтальные подписи осей над телом графика: левая прижата влево, правая — вправо. */
19283
+ const AxisTitlesTop = ({ left, right, neutralColor }) => (jsxRuntime.jsxs(AxisTitlesRow, { children: [jsxRuntime.jsx(AxisTitleTop, { "$color": left?.color || neutralColor, title: left?.title, children: left?.title }), jsxRuntime.jsx(AxisTitleTop, { "$color": right?.color || neutralColor, title: right?.title, children: right?.title })] }));
19284
+ /** Вертикальная подпись оси сбоку от тела графика. Без подписи ничего не рендерит. */
19285
+ const AxisTitleSideSlot = ({ side, axisTitle, neutralColor, plotTop, plotBottom, }) => {
19286
+ if (!axisTitle)
19287
+ return null;
19288
+ return (jsxRuntime.jsx(AxisTitleSideColumn, { "$side": side, "$plotTop": plotTop, "$plotBottom": plotBottom, children: jsxRuntime.jsx(AxisTitleSide, { "$side": side, "$color": axisTitle.color || neutralColor, title: axisTitle.title, children: axisTitle.title }) }));
19289
+ };
19290
+
18859
19291
  const Chart = React.memo(({ config, element, elementConfig, type, renderElement }) => {
18860
19292
  const theme = styled.useTheme();
18861
19293
  const { palette } = theme;
@@ -18876,12 +19308,18 @@ const Chart = React.memo(({ config, element, elementConfig, type, renderElement
18876
19308
  const radius = options?.radius || DEFAULT_BARCHART_RADIUS;
18877
19309
  const padding = options?.padding || DEFAULT_BAR_PADDING;
18878
19310
  const fontColor = options?.fontColor || palette.iconDisabled;
19311
+ // Нейтральная подпись оси — тем же цветом, что и значения шкалы (на макете у них один стиль).
19312
+ const axisTitleColor = options?.fontColor || palette.textDisabled;
18879
19313
  const angle = options?.angle || DEFAULT_CHART_ANGLE;
18880
19314
  const barWidth = options?.barWidth || DEFAULT_BAR_WIDTH;
18881
19315
  const cornerRadius = options?.cornerRadius || 0;
18882
19316
  const relatedAttributes = React.useMemo(() => relatedDataSources || [], [relatedDataSources]);
18883
- const axes = React.useMemo(() => relatedAttributes.filter(({ chartAxis }) => chartAxis === "y"), [relatedAttributes]);
19317
+ const axes = React.useMemo(() => relatedAttributes.filter(isValueAxis), [relatedAttributes]);
18884
19318
  const filterName = getChartFilterName(relatedDataSources);
19319
+ const { axisTitles, hasTopTitles, hasSideTitles, axisTitlesHeight, hideLeftAxis, hideRightAxis, } = useChartAxisTitles({ options, axes, isLineChart });
19320
+ // Ширина вертикальных подписей зависит от числа строк текста, поэтому тело между ними меряем
19321
+ // по факту, а не вычитаем константу.
19322
+ const [lineBodyRef, { width: measuredBodyWidth }] = useResizeBox(hasSideTitles);
18885
19323
  const isRelated = !!relatedAttributes?.length;
18886
19324
  const { data, loading } = useChartData({ element, type });
18887
19325
  const totalWidthFromBarWidth = (data[0]?.items?.length || 0) * (barWidth + padding);
@@ -18909,17 +19347,54 @@ const Chart = React.memo(({ config, element, elementConfig, type, renderElement
18909
19347
  const chartHeight = React.useMemo(() => (fitsHeight && measuredHeight ? measuredHeight : height), [fitsHeight, measuredHeight, height]);
18910
19348
  // Круглый график вписываем по короткой стороне (object-fit: contain), центрируя в ячейке.
18911
19349
  const pieSide = React.useMemo(() => (fitsHeight && measuredHeight ? Math.min(chartWidth, measuredHeight) : chartWidth), [fitsHeight, measuredHeight, chartWidth]);
19350
+ // Тело линейного графика — это ячейка за вычетом подписей осей. По высоте вычитаем только в
19351
+ // fill: вне fill chartHeight означает высоту самого тела, и обёртка честно растёт на подписи.
19352
+ const lineBodyWidth = React.useMemo(() => {
19353
+ if (!hasSideTitles)
19354
+ return chartWidth;
19355
+ if (measuredBodyWidth)
19356
+ return measuredBodyWidth;
19357
+ // До первого замера — оценка: одна строка текста и зазор на каждую подпись.
19358
+ const sideTitlesCount = Number(!!axisTitles.left) + Number(!!axisTitles.right);
19359
+ return Math.max(chartWidth - sideTitlesCount * (AXIS_TITLE_LINE_HEIGHT + AXIS_TITLE_GAP), 0);
19360
+ }, [hasSideTitles, measuredBodyWidth, chartWidth, axisTitles]);
19361
+ const lineBodyHeight = React.useMemo(() => (fitsHeight ? Math.max(chartHeight - axisTitlesHeight, 0) : chartHeight), [fitsHeight, chartHeight, axisTitlesHeight]);
19362
+ // Обёртка выше тела на строку горизонтальных подписей. В fill вся высота уже отмерена ячейкой,
19363
+ // поэтому прибавлять нечего — иначе тело вылезло бы за нижнюю границу.
19364
+ const lineWrapperHeight = React.useMemo(() => (fitsHeight ? chartHeight : chartHeight + axisTitlesHeight), [fitsHeight, chartHeight, axisTitlesHeight]);
18912
19365
  const [customize] = useChartChange({
18913
19366
  dataSources: config.dataSources,
18914
19367
  chartId: elementConfig?.id,
18915
- width: chartWidth,
18916
- height: chartHeight,
19368
+ width: lineBodyWidth,
19369
+ height: lineBodyHeight,
18917
19370
  fontColor,
18918
19371
  relatedAttributes,
18919
19372
  defaultColor: primaryColor,
18920
19373
  markers: configMarkers,
18921
19374
  showMarkers,
18922
19375
  });
19376
+ const { formatLeftTick, formatRightTick } = useChartAxisTickFormat({
19377
+ dataSources: config.dataSources,
19378
+ relatedAttributes,
19379
+ });
19380
+ const { reportPlotInsets } = React.useContext(ChartPlotContext);
19381
+ // Положение поля графика известно только после отрисовки: ширину шкал вычисляет d3. Смещение тела
19382
+ // берём из offset-размеров, а не из getBoundingClientRect: они не искажаются масштабом превью и
19383
+ // transform предков, а координаты шкалы всегда в пикселях SVG.
19384
+ const customizeLine = React.useCallback(args => {
19385
+ customize?.(args);
19386
+ const body = lineBodyRef.current;
19387
+ const layout = body?.parentElement?.parentElement;
19388
+ if (!reportPlotInsets || !body || !layout)
19389
+ return;
19390
+ const bodyLeft = body.offsetLeft - layout.offsetLeft;
19391
+ const [plotLeft, plotRight] = args.xScale.range();
19392
+ reportPlotInsets({
19393
+ left: Math.round(bodyLeft + plotLeft),
19394
+ right: Math.round(layout.offsetWidth - bodyLeft - plotRight),
19395
+ width: layout.offsetWidth,
19396
+ });
19397
+ }, [customize, reportPlotInsets, lineBodyRef]);
18923
19398
  const { formatFilterColor, onFilter } = useWidgetFilters(type, filterName, data?.[0]?.items);
18924
19399
  const formattedAttributes = React.useMemo(() => getFormattedAttributes(t, (data[0]?.items || []), attributes, element), [attributes, data, element, t]);
18925
19400
  const totalSum = React.useMemo(() => isRelated
@@ -18978,6 +19453,17 @@ const Chart = React.memo(({ config, element, elementConfig, type, renderElement
18978
19453
  left: markersMargin,
18979
19454
  };
18980
19455
  }, [showMarkers, markers?.length]);
19456
+ // Значения шкалы барчарта d3 центрирует на делении, и крайние деления наполовину выступают за поле.
19457
+ // При нулевом поле SVG выступ уходит за SVG, где его срезает BarChartContainer (overflow-x: hidden,
19458
+ // отчего и по вертикали содержимое не visible). Место под выступ резервируем внутри SVG — внешние
19459
+ // размеры обёрток не меняются. Без значений шкалы (showLabels) выступать нечему.
19460
+ const barMargin = React.useMemo(() => showLabels
19461
+ ? {
19462
+ ...margin,
19463
+ top: Math.max(margin.top, Y_AXIS_LABEL_OVERHANG),
19464
+ bottom: Math.max(margin.bottom, Y_AXIS_LABEL_OVERHANG),
19465
+ }
19466
+ : margin, [showLabels, margin]);
18981
19467
  const formatTooltipName = React.useCallback((name) => isRelated ? name : tooltipNameFromAttributes(name, formattedAttributes), [formattedAttributes, isRelated]);
18982
19468
  const formatTooltipValue = React.useCallback((value, name) => isRelated
18983
19469
  ? tooltipValueFromRelatedFeatures(t, value, relatedAttributes, data[0]?.layerInfo)
@@ -19004,26 +19490,42 @@ const Chart = React.memo(({ config, element, elementConfig, type, renderElement
19004
19490
  attributeUnits: item.attributeUnits,
19005
19491
  dataSourceName: item.dataSourceName,
19006
19492
  stroke: item.color || primaryColor,
19493
+ axis: item.axisSide ?? "left",
19007
19494
  values: item.items
19008
19495
  ?.filter(({ value }) => !isEmptyValue(value))
19009
19496
  ?.map(({ value }) => Number(value)),
19010
19497
  }));
19011
- const isHidedY = axes.every(({ hideAxis }) => hideAxis);
19012
- const chartValues = lineChartData
19013
- .flat()
19498
+ const isHidedY = hideLeftAxis && hideRightAxis;
19499
+ const valuesOfSide = (side) => lineChartData
19500
+ .filter(({ axis }) => axis === side)
19014
19501
  .map(({ values }) => values)
19015
19502
  .flat();
19503
+ const leftValues = valuesOfSide("left");
19504
+ const rightValues = valuesOfSide("right");
19016
19505
  // Резервируем нижнюю полосу под подписи оси X: без неё ось прижата к нижней границе,
19017
19506
  // подписи (сдвинутые на +16px) вылезают за тело и режутся overflow: hidden у ChartFillMeasure.
19507
+ // Сверху резервируем выступ верхнего деления шкалы: оно центрировано на линии сетки и при нулевом
19508
+ // поле наполовину уходит за SVG, где в fill его срезает overflow: hidden у ChartFillMeasure.
19509
+ const topOverhang = Math.max(Y_AXIS_LABEL_OVERHANG - margin.top, 0);
19018
19510
  const lineMargin = {
19019
19511
  ...margin,
19512
+ top: margin.top + topOverhang,
19020
19513
  bottom: Math.max(margin.bottom, LINE_CHART_LABEL_MARGIN),
19021
19514
  };
19022
- return (jsxRuntime.jsxs(AnyChartWrapper, { height: chartHeight, children: [jsxRuntime.jsx(LineChartStyles, {}), jsxRuntime.jsx(charts.LineChart, { data: lineChartData, labels: labels, markers: markers, width: chartWidth, height: chartHeight, xAxisPadding: 15, yAxisPadding: 0, min: Math.min(...chartValues), customYAxisSelection: yAxis => {
19023
- if (isHidedY) {
19024
- yAxis.remove();
19025
- }
19026
- }, customYAxis: yAxis => yAxis.ticks(4), renderTooltip: renderLineChartTooltip, customize: customize, dotSnapping: dotSnapping, dynamicTooltipEnable: true, stackedTooltip: true, tooltipClassName: "dashboardLineChartTooltip", drawGridX: !isHidedY, margin: lineMargin })] }));
19515
+ // draw.ts вычитает верхнее поле и из нижней границы поля графика — компенсируем добавленное,
19516
+ // чтобы низ поля и зазор до подписей оси X остались на месте.
19517
+ const lineXAxisPadding = LINE_CHART_X_AXIS_PADDING - topOverhang;
19518
+ // Колонки вертикальных подписей повторяют поля SVG, чтобы подпись центрировалась по полю
19519
+ // графика, а не по всей высоте вместе с подписями оси X.
19520
+ const plotTop = lineMargin.top;
19521
+ const plotBottom = lineMargin.top + lineMargin.bottom + lineXAxisPadding;
19522
+ return (jsxRuntime.jsxs(AnyChartWrapper, { height: lineWrapperHeight, children: [jsxRuntime.jsx(LineChartStyles, {}), jsxRuntime.jsxs(LineChartLayout, { children: [hasTopTitles && (jsxRuntime.jsx(AxisTitlesTop, { left: axisTitles.left, right: axisTitles.right, neutralColor: axisTitleColor })), jsxRuntime.jsxs(LineChartRow, { children: [hasSideTitles && (jsxRuntime.jsx(AxisTitleSideSlot, { side: "left", axisTitle: axisTitles.left, neutralColor: axisTitleColor, plotTop: plotTop, plotBottom: plotBottom })), jsxRuntime.jsx(LineChartBody, { ref: lineBodyRef, children: jsxRuntime.jsx(charts.LineChart, { data: lineChartData, labels: labels, markers: markers, width: lineBodyWidth, height: lineBodyHeight, xAxisPadding: lineXAxisPadding, yAxisPadding: 0, min: leftValues.length ? Math.min(...leftValues) : undefined, rightMin: rightValues.length ? Math.min(...rightValues) : undefined, customYAxisSelection: yAxis => {
19523
+ // Обе оси приходят в один колбэк, поэтому сторону определяем по классу.
19524
+ const isRight = yAxis.classed(charts.lineChartClassNames.lineChartYScaleRight);
19525
+ if (isRight ? hideRightAxis : hideLeftAxis) {
19526
+ yAxis.remove();
19527
+ }
19528
+ }, customYAxis: yAxis => yAxis.ticks(LINE_CHART_Y_TICKS).tickFormat(formatLeftTick), customYAxisRight: yAxis => yAxis.ticks(LINE_CHART_Y_TICKS).tickFormat(formatRightTick), renderTooltip: renderLineChartTooltip, customize: customizeLine, dotSnapping: dotSnapping, dynamicTooltipEnable: true, stackedTooltip: true, tooltipClassName: "dashboardLineChartTooltip", drawGridX: !isHidedY, margin: lineMargin }) }), hasSideTitles && (jsxRuntime.jsx(AxisTitleSideSlot, { side: "right", axisTitle: axisTitles.right, neutralColor: axisTitleColor, plotTop: plotTop, plotBottom: plotBottom }))] })] })] }));
19027
19529
  }
19028
19530
  if (isStackBar) {
19029
19531
  // Вне fill chartHeight — высота самой полосы, и обёртка честно становится выше на строку
@@ -19053,7 +19555,7 @@ const Chart = React.memo(({ config, element, elementConfig, type, renderElement
19053
19555
  const barRadius = options?.cornerRadius ?? DEFAULT_BARCHART_RADIUS;
19054
19556
  bars.attr("rx", barRadius);
19055
19557
  bars.attr("ry", barRadius);
19056
- }, margin: margin, xAxisPadding: 0, yAxisPadding: 0, formatTooltipValue: formatTooltipValue, formatTooltipName: formatTooltipName, hideTooltipGroupName: true, dynamicTooltipEnable: true, isBarTooltip: true, onBarClick: filterName ? item => onFilter(item.name) : undefined }) }) }));
19558
+ }, margin: barMargin, xAxisPadding: 0, yAxisPadding: 0, formatTooltipValue: formatTooltipValue, formatTooltipName: formatTooltipName, hideTooltipGroupName: true, dynamicTooltipEnable: true, isBarTooltip: true, onBarClick: filterName ? item => onFilter(item.name) : undefined }) }) }));
19057
19559
  }, [
19058
19560
  customXAxisBottom,
19059
19561
  customYAxisLeft,
@@ -19069,6 +19571,15 @@ const Chart = React.memo(({ config, element, elementConfig, type, renderElement
19069
19571
  formatFilterColor,
19070
19572
  chartWidth,
19071
19573
  chartHeight,
19574
+ lineBodyWidth,
19575
+ lineBodyHeight,
19576
+ lineWrapperHeight,
19577
+ axisTitles,
19578
+ hasTopTitles,
19579
+ hasSideTitles,
19580
+ axisTitleColor,
19581
+ hideLeftAxis,
19582
+ hideRightAxis,
19072
19583
  fitsHeight,
19073
19584
  pieSide,
19074
19585
  fill,
@@ -19078,11 +19589,14 @@ const Chart = React.memo(({ config, element, elementConfig, type, renderElement
19078
19589
  formatTooltipValue,
19079
19590
  formatTooltipName,
19080
19591
  filterName,
19081
- axes,
19082
19592
  labels,
19083
19593
  margin,
19594
+ barMargin,
19084
19595
  renderLineChartTooltip,
19085
- customize,
19596
+ customizeLine,
19597
+ lineBodyRef,
19598
+ formatLeftTick,
19599
+ formatRightTick,
19086
19600
  primaryColor,
19087
19601
  type,
19088
19602
  elementConfig?.children,
@@ -19099,14 +19613,14 @@ const Chart = React.memo(({ config, element, elementConfig, type, renderElement
19099
19613
  ]);
19100
19614
  if (!isVisibleContainer(id, expandable, expanded, expandedContainers))
19101
19615
  return null;
19102
- const chartBody = (jsxRuntime.jsxs(ChartWrapper, { width: isPieChart && fill ? pieSide : width, height: isStackBar ? "auto" : isPieChart ? (fill ? pieSide : width) : chartHeight, column: column, loading: loading, children: [jsxRuntime.jsx(ChartTooltipStyles, {}), renderChart] }));
19616
+ const chartBody = (jsxRuntime.jsxs(ChartWrapper, { width: isPieChart && fill ? pieSide : width, height: isStackBar ? "auto" : isPieChart ? (fill ? pieSide : width) : isLineChart ? lineWrapperHeight : chartHeight, column: column, loading: loading, children: [jsxRuntime.jsx(ChartTooltipStyles, {}), renderChart] }));
19103
19617
  if (!fill)
19104
19618
  return chartBody;
19105
19619
  // fill: тело центрируется в измеряемой обёртке, ref которой питает ResizeObserver обеими осями.
19106
19620
  return jsxRuntime.jsx(ChartFillMeasure, { ref: chartRef, children: chartBody });
19107
19621
  });
19108
19622
 
19109
- const ChartLegend = ({ data, chartElement, fontSize, type, twoColumns, loading }) => {
19623
+ const ChartLegend = ({ data, chartElement, fontSize, type, twoColumns, column, loading }) => {
19110
19624
  const { t } = useGlobalContext();
19111
19625
  const { sliceItems, showMore, onShowMore } = useShownOtherItems(chartElement?.options);
19112
19626
  const axes = React.useMemo(() => getChartAxes(chartElement), [chartElement]);
@@ -19115,7 +19629,7 @@ const ChartLegend = ({ data, chartElement, fontSize, type, twoColumns, loading }
19115
19629
  const showMoreBtn = !!shownItems && data.length > shownItems;
19116
19630
  if (!loading && !data?.length && !!hideEmpty)
19117
19631
  return null;
19118
- return (jsxRuntime.jsxs(uilibGl.Flex, { flexDirection: "column", opacity: loading ? FILTERED_VALUE_OPACITY / 100 : 1, children: [jsxRuntime.jsx(ChartLegendContainer, { twoColumns: twoColumns, children: sliceItems(data)?.map(({ name, color }, index) => (jsxRuntime.jsxs(ChartLegendItem, { hasAnyFilter: hasAnyFilter, isFiltered: isFiltered(name), onClick: axes?.[0]?.filterName ? () => onFilter(name) : undefined, children: [jsxRuntime.jsx(ChartLegendColor, { color: color }), jsxRuntime.jsx(ChartLegendName, { "$fontSize": fontSize, "$fontColor": fontColor, children: name })] }, index))) }), showMoreBtn && (jsxRuntime.jsx(uilibGl.LegendToggler, { onClick: onShowMore, toggled: showMore, children: t("other", { ns: "dashboard", defaultValue: "Другое" }) }))] }));
19632
+ return (jsxRuntime.jsxs(uilibGl.Flex, { flexDirection: "column", opacity: loading ? FILTERED_VALUE_OPACITY / 100 : 1, children: [jsxRuntime.jsx(ChartLegendContainer, { twoColumns: twoColumns, "$column": column, children: sliceItems(data)?.map(({ name, color }, index) => (jsxRuntime.jsxs(ChartLegendItem, { hasAnyFilter: hasAnyFilter, isFiltered: isFiltered(name), onClick: axes?.[0]?.filterName ? () => onFilter(name) : undefined, children: [jsxRuntime.jsx(ChartLegendColor, { color: color }), jsxRuntime.jsx(ChartLegendName, { "$fontSize": fontSize, "$fontColor": fontColor, children: name })] }, index))) }), showMoreBtn && (jsxRuntime.jsx(uilibGl.LegendToggler, { onClick: onShowMore, toggled: showMore, children: t("other", { ns: "dashboard", defaultValue: "Другое" }) }))] }));
19119
19633
  };
19120
19634
 
19121
19635
  const ContainerChildren = React.memo(({ type, items, isColumn, isMain, renderElement }) => {
@@ -19973,6 +20487,10 @@ const DEFAULT_HEATMAP_STYLE = {
19973
20487
  exports.ALIGNMENTS = ALIGNMENTS;
19974
20488
  exports.ALIGN_ITEMS = ALIGN_ITEMS;
19975
20489
  exports.ATTRIBUTE_ICON_ELEMENT_TYPES = ATTRIBUTE_ICON_ELEMENT_TYPES;
20490
+ exports.AXIS_TITLE_GAP = AXIS_TITLE_GAP;
20491
+ exports.AXIS_TITLE_LINE_HEIGHT = AXIS_TITLE_LINE_HEIGHT;
20492
+ exports.AXIS_TITLE_TOP_GRID_GAP = AXIS_TITLE_TOP_GRID_GAP;
20493
+ exports.AXIS_TITLE_TOP_HEIGHT = AXIS_TITLE_TOP_HEIGHT;
19976
20494
  exports.AddButtonRow = AddButtonRow;
19977
20495
  exports.AddFeatureButton = AddFeatureButton;
19978
20496
  exports.AddFeatureContainer = AddFeatureContainer;
@@ -19982,6 +20500,7 @@ exports.AttributeGalleryContainer = AttributeGalleryContainer;
19982
20500
  exports.AttributeLabel = AttributeLabel;
19983
20501
  exports.BASE_CONTAINER_STYLE = BASE_CONTAINER_STYLE;
19984
20502
  exports.BG_IMAGE_SLOT_ID = BG_IMAGE_SLOT_ID;
20503
+ exports.CHART_AXIS_ALIGN_JUSTIFY = CHART_AXIS_ALIGN_JUSTIFY;
19985
20504
  exports.CHART_TYPES = CHART_TYPES;
19986
20505
  exports.COMPACT_FRACTION_DIGITS = COMPACT_FRACTION_DIGITS;
19987
20506
  exports.CONFIG_PAGES_ID = CONFIG_PAGES_ID;
@@ -20135,6 +20654,8 @@ exports.HiddenTitleItems = HiddenTitleItems;
20135
20654
  exports.IconContainer = IconContainer;
20136
20655
  exports.ImageContainer = ImageContainer;
20137
20656
  exports.LEFT_PANEL_HEADER_HEIGHT = LEFT_PANEL_HEADER_HEIGHT;
20657
+ exports.LINE_CHART_X_LABELS_BLANK = LINE_CHART_X_LABELS_BLANK;
20658
+ exports.LINE_CHART_Y_TICKS = LINE_CHART_Y_TICKS;
20138
20659
  exports.Layer = Layer;
20139
20660
  exports.LayerDescription = LayerDescription;
20140
20661
  exports.LayerGroupList = LayerGroupList;
@@ -20214,6 +20735,9 @@ exports.TwoColumnContainer = TwoColumnContainer;
20214
20735
  exports.UploadContainer = UploadContainer;
20215
20736
  exports.VIEW_MODES = VIEW_MODES;
20216
20737
  exports.VoteContainer = VoteContainer;
20738
+ exports.X_AXIS_TITLE_GAP = X_AXIS_TITLE_GAP;
20739
+ exports.X_AXIS_TITLE_LEGEND_GAP = X_AXIS_TITLE_LEGEND_GAP;
20740
+ exports.Y_AXIS_LABEL_OVERHANG = Y_AXIS_LABEL_OVERHANG;
20217
20741
  exports.ZOOM_FILTER_NAME = ZOOM_FILTER_NAME;
20218
20742
  exports.addDataSource = addDataSource;
20219
20743
  exports.addDataSources = addDataSources;
@@ -20273,6 +20797,7 @@ exports.getAttributeIconUrl = getAttributeIconUrl;
20273
20797
  exports.getAttributeValue = getAttributeValue;
20274
20798
  exports.getAttributesConfiguration = getAttributesConfiguration;
20275
20799
  exports.getAverageTrackSize = getAverageTrackSize;
20800
+ exports.getAxisSide = getAxisSide;
20276
20801
  exports.getChartAxes = getChartAxes;
20277
20802
  exports.getChartFilterName = getChartFilterName;
20278
20803
  exports.getChartMarkers = getChartMarkers;
@@ -20324,6 +20849,7 @@ exports.getTrackSize = getTrackSize;
20324
20849
  exports.getTrackSizeKey = getTrackSizeKey;
20325
20850
  exports.getTrackSizes = getTrackSizes;
20326
20851
  exports.getWrapperSizeStyle = getWrapperSizeStyle;
20852
+ exports.hasAxisConfig = hasAxisConfig;
20327
20853
  exports.hasContainerBgImage = hasContainerBgImage;
20328
20854
  exports.hexToRgba = hexToRgba;
20329
20855
  exports.isCrossOriginUrl = isCrossOriginUrl;
@@ -20340,8 +20866,10 @@ exports.isNotValidSelectedTab = isNotValidSelectedTab;
20340
20866
  exports.isNumeric = isNumeric;
20341
20867
  exports.isObject = isObject;
20342
20868
  exports.isProxyService = isProxyService;
20869
+ exports.isRightAxis = isRightAxis;
20343
20870
  exports.isRootOwningContainer = isRootOwningContainer;
20344
20871
  exports.isTreeFilterValue = isTreeFilterValue;
20872
+ exports.isValueAxis = isValueAxis;
20345
20873
  exports.isVisibleContainer = isVisibleContainer;
20346
20874
  exports.mapNodeById = mapNodeById;
20347
20875
  exports.mergeAttributeConfigurations = mergeAttributeConfigurations;
@@ -20357,6 +20885,7 @@ exports.pointOptions = pointOptions;
20357
20885
  exports.removeDataSource = removeDataSource;
20358
20886
  exports.removeTracks = removeTracks;
20359
20887
  exports.replaceNodesByIds = replaceNodesByIds;
20888
+ exports.resolveAxis = resolveAxis;
20360
20889
  exports.rgbToHex = rgbToHex;
20361
20890
  exports.roundFr = roundFr;
20362
20891
  exports.roundTotalSum = roundTotalSum;
@@ -20384,6 +20913,9 @@ exports.useAttachmentsView = useAttachmentsView;
20384
20913
  exports.useAutoCompleteControl = useAutoCompleteControl;
20385
20914
  exports.useBeforeSave = useBeforeSave;
20386
20915
  exports.useBgImageHost = useBgImageHost;
20916
+ exports.useChartAxisAttributes = useChartAxisAttributes;
20917
+ exports.useChartAxisTickFormat = useChartAxisTickFormat;
20918
+ exports.useChartAxisTitles = useChartAxisTitles;
20387
20919
  exports.useChartChange = useChartChange;
20388
20920
  exports.useChartData = useChartData;
20389
20921
  exports.useConfigDataSources = useConfigDataSources;