@evergis/react 4.0.153 → 4.0.155

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.
Files changed (28) hide show
  1. package/dist/components/Dashboard/componentTypes.d.ts +3 -3
  2. package/dist/components/Dashboard/components/Chart/ChartPlotContext.d.ts +24 -0
  3. package/dist/components/Dashboard/components/Chart/components/AxisTitles.d.ts +23 -0
  4. package/dist/components/Dashboard/components/Chart/styled.d.ts +33 -0
  5. package/dist/components/Dashboard/components/ChartLegend/index.d.ts +1 -0
  6. package/dist/components/Dashboard/components/ChartLegend/styled.d.ts +1 -0
  7. package/dist/components/Dashboard/constants.d.ts +37 -1
  8. package/dist/components/Dashboard/containers/AttachmentContainer/constants.d.ts +1 -0
  9. package/dist/components/Dashboard/containers/ChartContainer/types.d.ts +3 -1
  10. package/dist/components/Dashboard/containers/FiltersContainer/constants.d.ts +1 -0
  11. package/dist/components/Dashboard/containers/styled.d.ts +38 -0
  12. package/dist/components/Dashboard/elements/ElementIcon/components/AttachmentIcon/index.d.ts +7 -0
  13. package/dist/components/Dashboard/elements/ElementIcon/components/AttachmentIcon/styled.d.ts +3 -0
  14. package/dist/components/Dashboard/elements/ElementIcon/components/AttachmentIcon/types.d.ts +15 -0
  15. package/dist/components/Dashboard/elements/ElementIcon/constants.d.ts +2 -0
  16. package/dist/components/Dashboard/elements/ElementIcon/utils/getAttachmentIcon.d.ts +7 -0
  17. package/dist/components/Dashboard/hooks/index.d.ts +3 -0
  18. package/dist/components/Dashboard/hooks/useChartAxisAttributes.d.ts +19 -0
  19. package/dist/components/Dashboard/hooks/useChartAxisTickFormat.d.ts +18 -0
  20. package/dist/components/Dashboard/hooks/useChartAxisTitles.d.ts +32 -0
  21. package/dist/components/Dashboard/types.d.ts +59 -1
  22. package/dist/components/Dashboard/utils/chartAxis.d.ts +29 -0
  23. package/dist/components/Dashboard/utils/index.d.ts +1 -0
  24. package/dist/index.js +679 -89
  25. package/dist/index.js.map +1 -1
  26. package/dist/react.esm.js +662 -90
  27. package/dist/react.esm.js.map +1 -1
  28. package/package.json +3 -3
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
 
@@ -5642,13 +5824,14 @@ const GRID_TILE_SIZE = "4.5rem";
5642
5824
  const LIST_ICON_SIZE = "1.5rem";
5643
5825
  const JPG_MIME_TYPE = "image/jpeg";
5644
5826
  const PNG_MIME_TYPE = "image/png";
5827
+ const SVG_MIME_TYPE = "image/svg+xml";
5645
5828
  const IMAGE_MIME_TYPES = [
5646
5829
  "image/apng",
5647
5830
  "image/avif",
5648
5831
  "image/gif",
5649
5832
  "image/jpeg",
5650
5833
  "image/png",
5651
- "image/svg+xml",
5834
+ SVG_MIME_TYPE,
5652
5835
  "image/webp",
5653
5836
  ];
5654
5837
  const XLSX_MIME_TYPES = [
@@ -7053,6 +7236,96 @@ const useFetchImageWithAuth = (url) => {
7053
7236
  return isExternal ? url : blobUrl;
7054
7237
  };
7055
7238
 
7239
+ /**
7240
+ * Оси значений линейного графика и атрибуты, по `stringFormat` которых форматируются деления.
7241
+ *
7242
+ * У левой и правой осей это разные атрибуты, поэтому они резолвятся по отдельности: иначе на
7243
+ * правой оси окажутся единицы измерения левой.
7244
+ */
7245
+ const useChartAxisAttributes = ({ dataSources, relatedAttributes }) => {
7246
+ const { layerInfos, dataSources: fetchedDataSources } = useWidgetContext();
7247
+ const axes = React.useMemo(() => (relatedAttributes || []).filter(isValueAxis), [relatedAttributes]);
7248
+ const strokeColors = React.useMemo(() => axes.map(related => resolveAxis(related).color), [axes]);
7249
+ const leftAxis = React.useMemo(() => axes.find(related => !isRightAxis(related)) ?? axes[0] ?? relatedAttributes?.[0], [axes, relatedAttributes]);
7250
+ const rightAxis = React.useMemo(() => axes.find(isRightAxis), [axes]);
7251
+ const resolveAxisAttribute = React.useCallback((related) => {
7252
+ if (!related)
7253
+ return undefined;
7254
+ const layerInfo = getDataSourceLayerInfo({
7255
+ layerInfos,
7256
+ configDataSource: dataSources?.find(({ name }) => name === related.dataSourceName),
7257
+ fetchedDataSource: getDataSource(related.dataSourceName, fetchedDataSources),
7258
+ });
7259
+ return getLayerInfoAttribute(layerInfo, related.attributeName);
7260
+ }, [dataSources, fetchedDataSources, layerInfos]);
7261
+ const leftAxisAttribute = React.useMemo(() => resolveAxisAttribute(leftAxis), [resolveAxisAttribute, leftAxis]);
7262
+ const rightAxisAttribute = React.useMemo(() => resolveAxisAttribute(rightAxis), [resolveAxisAttribute, rightAxis]);
7263
+ return { axes, strokeColors, leftAxisAttribute, rightAxisAttribute };
7264
+ };
7265
+
7266
+ /**
7267
+ * Форматтеры делений левой и правой шкал линейного графика — по `stringFormat` своего атрибута.
7268
+ *
7269
+ * Отдаются в `tickFormat` генератора оси, а не подменяют текст после отрисовки: d3 измеряет ширину
7270
+ * шкалы по уже нарисованным подписям и по ней сдвигает поле графика. Подмена текста задним числом
7271
+ * (`30` → `30,0`) делала подпись шире замера, и она заезжала на подпись оси слева.
7272
+ */
7273
+ const useChartAxisTickFormat = ({ dataSources, relatedAttributes }) => {
7274
+ const { t } = useGlobalContext();
7275
+ const { leftAxisAttribute, rightAxisAttribute } = useChartAxisAttributes({ dataSources, relatedAttributes });
7276
+ const createTickFormat = React.useCallback((attribute) => (value) => {
7277
+ if (!value)
7278
+ return "0";
7279
+ return attribute
7280
+ ? String(formatAttributeValue({
7281
+ t,
7282
+ type: attribute.type,
7283
+ value: Number(value),
7284
+ stringFormat: attribute.stringFormat,
7285
+ noUnits: true,
7286
+ }))
7287
+ : String(value);
7288
+ }, [t]);
7289
+ const formatLeftTick = React.useMemo(() => createTickFormat(leftAxisAttribute), [createTickFormat, leftAxisAttribute]);
7290
+ const formatRightTick = React.useMemo(() => createTickFormat(rightAxisAttribute), [createTickFormat, rightAxisAttribute]);
7291
+ return { formatLeftTick, formatRightTick };
7292
+ };
7293
+
7294
+ /**
7295
+ * Подписи осей линейного графика и место, которое они занимают над телом графика.
7296
+ *
7297
+ * Режим размещения общий для всех осей (`options.axis.titlePosition`), текст — у каждой оси свой.
7298
+ * Цвет подписи отдельной опцией не задаётся: одна серия на стороне — подпись в её цвет, несколько —
7299
+ * нейтральная, потому что тогда подпись описывает всю ось, а не одну из линий. Ширину вертикальных
7300
+ * подписей здесь не считаем — она зависит от числа строк текста и измеряется по факту.
7301
+ */
7302
+ const useChartAxisTitles = ({ options, axes, isLineChart }) => {
7303
+ const axisTitlePosition = options?.axis?.titlePosition ?? "side";
7304
+ const axisTitles = React.useMemo(() => {
7305
+ const pick = (side) => {
7306
+ const sideAxes = axes.filter(related => getAxisSide(related) === side);
7307
+ const title = sideAxes.map(resolveAxis).find(axis => axis.title)?.title;
7308
+ if (!title)
7309
+ return undefined;
7310
+ return { title, color: sideAxes.length === 1 ? resolveAxis(sideAxes[0]).color : undefined };
7311
+ };
7312
+ return { left: pick("left"), right: pick("right") };
7313
+ }, [axes]);
7314
+ const hasAxisTitles = !!(isLineChart && (axisTitles.left || axisTitles.right));
7315
+ const hasTopTitles = hasAxisTitles && axisTitlePosition === "top";
7316
+ const hasSideTitles = hasAxisTitles && axisTitlePosition === "side";
7317
+ const hideLeftAxis = React.useMemo(() => axes.filter(related => !isRightAxis(related)).every(related => resolveAxis(related).hide), [axes]);
7318
+ const hideRightAxis = React.useMemo(() => axes.filter(isRightAxis).every(related => resolveAxis(related).hide), [axes]);
7319
+ return {
7320
+ axisTitles,
7321
+ hasTopTitles,
7322
+ hasSideTitles,
7323
+ axisTitlesHeight: hasTopTitles ? AXIS_TITLE_TOP_HEIGHT : 0,
7324
+ hideLeftAxis,
7325
+ hideRightAxis,
7326
+ };
7327
+ };
7328
+
7056
7329
  const HEIGHT_OFFSET = 20;
7057
7330
  const FILL_OPACITY = 0.28;
7058
7331
  const lineGenerator = line()
@@ -7098,20 +7371,10 @@ function wrap() {
7098
7371
  }
7099
7372
  }
7100
7373
  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]);
7374
+ const { strokeColors } = useChartAxisAttributes({
7375
+ dataSources,
7376
+ relatedAttributes,
7377
+ });
7115
7378
  const ref = React.useRef({
7116
7379
  path: null,
7117
7380
  area: null,
@@ -7164,22 +7427,9 @@ const useChartChange = ({ dataSources, chartId, width, height, relatedAttributes
7164
7427
  svg
7165
7428
  .selectAll(`.${charts.lineChartClassNames.lineChartXScaleGlobal} .tick text`)
7166
7429
  .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);
7430
+ // Текст делений форматируется ещё в генераторе оси (useChartAxisTickFormat), до замера ширины
7431
+ // шкалы; здесь его только укорачиваем.
7432
+ svg.selectAll(`.${charts.lineChartClassNames.lineChartYScaleGlobal} .tick text`).each(wrap);
7183
7433
  svg.selectAll(".tick text").attr("style", `color: ${fontColor}`);
7184
7434
  const global = svg.select(`.${charts.lineChartClassNames.lineChartLinesGlobal}`);
7185
7435
  const lineChartLines = svg.selectAll(`.${charts.lineChartClassNames.lineChartLine}`);
@@ -7215,7 +7465,6 @@ const useChartChange = ({ dataSources, chartId, width, height, relatedAttributes
7215
7465
  svg.append("defs").html(() => defs.join(""));
7216
7466
  }, [
7217
7467
  fontColor,
7218
- yAxisAttribute,
7219
7468
  chartId,
7220
7469
  strokeColors,
7221
7470
  defaultColor,
@@ -7223,7 +7472,6 @@ const useChartChange = ({ dataSources, chartId, width, height, relatedAttributes
7223
7472
  onChange,
7224
7473
  showMarkers,
7225
7474
  markers,
7226
- t,
7227
7475
  ]);
7228
7476
  return [customize, onChange];
7229
7477
  };
@@ -7380,10 +7628,10 @@ const useChartData = ({ element, type }) => {
7380
7628
  const configDataSources = useConfigDataSources(type);
7381
7629
  const relatedAttributes = React.useMemo(() => element?.options?.relatedDataSources || [], [element?.options?.relatedDataSources]);
7382
7630
  const loading = React.useMemo(() => !!relatedAttributes?.length &&
7383
- !dataSources?.some(({ name }) => relatedAttributes.some(({ chartAxis, dataSourceName }) => chartAxis === "y" && dataSourceName === name)), [dataSources, relatedAttributes]);
7631
+ !dataSources?.some(({ name }) => relatedAttributes.some(related => isValueAxis(related) && related.dataSourceName === name)), [dataSources, relatedAttributes]);
7384
7632
  const fetchedData = React.useMemo(() => {
7385
7633
  const isRelated = !!relatedAttributes?.length;
7386
- const filteredAttributes = relatedAttributes.filter(({ chartAxis }) => chartAxis === "y");
7634
+ const filteredAttributes = relatedAttributes.filter(isValueAxis);
7387
7635
  if (!element)
7388
7636
  return [];
7389
7637
  if (isRelated) {
@@ -7391,7 +7639,7 @@ const useChartData = ({ element, type }) => {
7391
7639
  return [];
7392
7640
  return filteredAttributes
7393
7641
  .map(relatedAxis => {
7394
- const { dataSourceName, axisColor, attributeName, attributeUnits } = relatedAxis;
7642
+ const { dataSourceName, attributeName, attributeUnits } = relatedAxis;
7395
7643
  const dataSource = configDataSources.find(({ name }) => name === dataSourceName);
7396
7644
  const featureDataSource = getDataSource(dataSourceName, dataSources);
7397
7645
  const layerInfo = getDataSourceLayerInfo({
@@ -7411,7 +7659,10 @@ const useChartData = ({ element, type }) => {
7411
7659
  layerInfo,
7412
7660
  })
7413
7661
  : null,
7414
- color: axisColor,
7662
+ color: resolveAxis(relatedAxis).color,
7663
+ // Сторона едет вместе с данными: серии без значений отсеиваются ниже, и сопоставлять
7664
+ // их с осями по индексу уже нельзя.
7665
+ axisSide: getAxisSide(relatedAxis),
7415
7666
  attributeName,
7416
7667
  attributeUnits,
7417
7668
  dataSourceName,
@@ -7424,6 +7675,7 @@ const useChartData = ({ element, type }) => {
7424
7675
  return [
7425
7676
  {
7426
7677
  items: getDataFromAttributes(t, element, attributes),
7678
+ axisSide: "left",
7427
7679
  layerInfo: null,
7428
7680
  attributeName: null,
7429
7681
  attributeUnits: null,
@@ -7443,7 +7695,7 @@ const useChartData = ({ element, type }) => {
7443
7695
  ]);
7444
7696
  const isResolved = React.useMemo(() => !relatedAttributes?.length ||
7445
7697
  relatedAttributes
7446
- .filter(({ chartAxis }) => chartAxis === "y")
7698
+ .filter(isValueAxis)
7447
7699
  .every(({ dataSourceName }) => Array.isArray(getDataSource(dataSourceName, dataSources)?.features)), [relatedAttributes, dataSources]);
7448
7700
  const [data, setData] = React.useState(fetchedData);
7449
7701
  React.useEffect(() => {
@@ -8975,6 +9227,15 @@ const ChartLegendContainer = styled(uilibGl.Flex).withConfig({ displayName: "Cha
8975
9227
  flex-direction: column;
8976
9228
  flex-wrap: wrap;
8977
9229
  justify-content: ${({ twoColumns }) => (twoColumns ? "flex-start" : "center")};
9230
+
9231
+ /* Явная опция column перебивает раскладку, которую легенде навязывает ChartContainer
9232
+ (под графиком — в ряд, сбоку — столбиком). Без опции поведение прежнее. */
9233
+ ${({ $column }) => $column !== undefined &&
9234
+ styled.css `
9235
+ &&& {
9236
+ flex-direction: ${$column ? "column" : "row"};
9237
+ }
9238
+ `}
8978
9239
  `;
8979
9240
  const ChartLegendItem = styled(uilibGl.Flex).withConfig({ displayName: "ChartLegendItem", componentId: "sc-16m9piw" }) `
8980
9241
  cursor: ${({ onClick }) => (onClick ? "pointer" : "default")};
@@ -9043,12 +9304,129 @@ const ContainerChart = styled(uilibGl.Flex).withConfig({ displayName: "Container
9043
9304
  `}
9044
9305
  `;
9045
9306
  const ContainerLegend = styled(uilibGl.Flex).withConfig({ displayName: "ContainerLegend", componentId: "sc-z1n1s8" }) ``;
9046
- const ContainerUnits = styled.div.withConfig({ displayName: "ContainerUnits", componentId: "sc-1vurgy9" }) `
9307
+ /**
9308
+ * Выравнивание строки по полю графика: ширина тела графика, а внутренние отступы — до краёв поля
9309
+ * (колонка подписи оси и значения шкалы). Тело графика центрируется в слоте, поэтому и строка
9310
+ * центрируется. Правило поднято `&&&` над шириной, которую строкам задаёт `ContainerValue`.
9311
+ */
9312
+ const plotInsetsMixin = styled.css `
9313
+ ${({ $insets }) => $insets &&
9314
+ styled.css `
9315
+ &&& {
9316
+ box-sizing: border-box;
9317
+ align-self: center;
9318
+ width: ${$insets.width}px;
9319
+ max-width: 100%;
9320
+ padding-left: ${$insets.left}px;
9321
+ padding-right: ${$insets.right}px;
9322
+ }
9323
+ `}
9324
+ `;
9325
+ /**
9326
+ * Строка ждёт, пока график сообщит положение поля: место занимает, но не видна. Иначе до первой
9327
+ * отрисовки d3 она стояла бы по краю контейнера и потом прыгала на место.
9328
+ */
9329
+ const awaitingPlotMixin = styled.css `
9330
+ ${({ $awaitingPlot }) => $awaitingPlot &&
9331
+ styled.css `
9332
+ visibility: hidden;
9333
+ `}
9334
+ `;
9335
+ /**
9336
+ * Строка под телом графика: подпись оси X и, при `legendInline`, легенда справа от неё.
9337
+ *
9338
+ * `$offsetTop` подтягивает строку к подписям оси X: под ними внутри SVG остаётся пустая полоса,
9339
+ * и без поправки зазор получился бы втрое больше макетного.
9340
+ */
9341
+ const ChartFooter = styled(uilibGl.Flex).withConfig({ displayName: "ChartFooter", componentId: "sc-nii28w" }) `
9342
+ align-items: center;
9343
+ flex-wrap: nowrap;
9344
+ gap: 0.5rem;
9345
+ width: 100%;
9346
+ margin-top: ${({ $offsetTop }) => $offsetTop ?? 0}px;
9347
+ justify-content: ${({ $align }) => $align};
9348
+
9349
+ ${ContainerLegend} {
9350
+ justify-content: flex-end;
9351
+ margin-left: auto;
9352
+ }
9353
+
9354
+ /* Легенда в строке с подписью: записи прижаты вправо, без отступа сверху, который ей задаёт
9355
+ ContainerValue, и без нижнего отступа записей — иначе она съедет вниз относительно подписи.
9356
+ Прижимать нужно сами записи: обёртки легенды — Flex из uilib-gl с width: 100%, и выравнивание
9357
+ внешнего контейнера до записей не доходит, а своё у легенды — по центру. */
9358
+ &&& ${ChartLegendContainer} {
9359
+ justify-content: flex-end;
9360
+ margin-top: 0;
9361
+ }
9362
+
9363
+ &&& ${ChartLegendItem} {
9364
+ margin-bottom: 0;
9365
+ }
9366
+
9367
+ ${plotInsetsMixin};
9368
+ ${awaitingPlotMixin};
9369
+ `;
9370
+ const ChartXAxisTitle = styled.div.withConfig({ displayName: "ChartXAxisTitle", componentId: "sc-1vsgzi9" }) `
9371
+ font-size: 0.625rem;
9372
+ line-height: 0.75rem;
9373
+ white-space: nowrap;
9374
+ color: ${({ $color, theme: { palette } }) => $color || palette.textDisabled};
9375
+ `;
9376
+ /**
9377
+ * Строка легенды под графиком. `$gapTop` заменяет отступ сверху, который легенде задаёт `ContainerValue`;
9378
+ * `$offsetTop` сдвигает саму строку — подтягивает её к подписям оси X, как строку с подписью оси.
9379
+ *
9380
+ * `$justify` выравнивает сами записи. Выравнивание строки (`justifyContent`) до них не доходит: обёртки
9381
+ * легенды — Flex из uilib-gl с `width: 100%`, а записи внутри легенда всегда центрирует.
9382
+ */
9383
+ const ChartLegendRow = styled(ContainerLegend).withConfig({ displayName: "ChartLegendRow", componentId: "sc-1a12wrz" }) `
9384
+ ${({ $offsetTop }) => $offsetTop !== undefined &&
9385
+ styled.css `
9386
+ margin-top: ${$offsetTop}px;
9387
+ `}
9388
+
9389
+ ${({ $gapTop }) => $gapTop !== undefined &&
9390
+ styled.css `
9391
+ &&& ${ChartLegendContainer} {
9392
+ margin-top: ${$gapTop}px;
9393
+ }
9394
+ `}
9395
+
9396
+ ${({ $justify }) => $justify &&
9397
+ styled.css `
9398
+ &&& ${ChartLegendContainer} {
9399
+ justify-content: ${$justify};
9400
+ }
9401
+ `}
9402
+
9403
+ ${plotInsetsMixin};
9404
+ ${awaitingPlotMixin};
9405
+ `;
9406
+ /**
9407
+ * Колонка «график + подпись оси X» для легенды сбоку (`twoColumns`). Без неё подпись встала бы третьим
9408
+ * элементом ряда рядом с легендой, а не под графиком.
9409
+ */
9410
+ const ChartWithFooter = styled(uilibGl.Flex).withConfig({ displayName: "ChartWithFooter", componentId: "sc-140gv2s" }) `
9411
+ flex-direction: column;
9412
+ flex-wrap: nowrap;
9413
+ min-width: 0;
9414
+
9415
+ ${({ $fill }) => $fill &&
9416
+ styled.css `
9417
+ min-height: 0;
9418
+ `}
9419
+
9420
+ &&& ${ContainerChart} {
9421
+ width: 100%;
9422
+ }
9423
+ `;
9424
+ const ContainerUnits = styled.div.withConfig({ displayName: "ContainerUnits", componentId: "sc-vj15z6" }) `
9047
9425
  margin-left: 0.5rem;
9048
9426
  white-space: nowrap;
9049
9427
  font-size: 0.75rem;
9050
9428
  `;
9051
- const ContainerValue = styled(uilibGl.Flex).withConfig({ displayName: "ContainerValue", componentId: "sc-aj1rs9" }) `
9429
+ const ContainerValue = styled(uilibGl.Flex).withConfig({ displayName: "ContainerValue", componentId: "sc-1bnfj5v" }) `
9052
9430
  justify-content: flex-end;
9053
9431
  align-items: center;
9054
9432
  flex-wrap: nowrap;
@@ -9067,7 +9445,7 @@ const ContainerValue = styled(uilibGl.Flex).withConfig({ displayName: "Container
9067
9445
  width: ${({ column }) => (column ? "100%" : "auto")};
9068
9446
  }
9069
9447
 
9070
- ${ContainerChart}, ${ContainerLegend} {
9448
+ ${ContainerChart}, ${ContainerLegend}, ${ChartWithFooter} {
9071
9449
  width: ${({ column }) => (column ? "100%" : "50%")};
9072
9450
  }
9073
9451
 
@@ -9090,7 +9468,7 @@ const SizeIconMixin = styled.css `
9090
9468
  font-size: ${({ $fontSize }) => $fontSize}px !important;
9091
9469
  }
9092
9470
  `;
9093
- styled(uilibGl.Icon).withConfig({ displayName: "ContainerIcon", componentId: "sc-1w9w0bq" }) `
9471
+ styled(uilibGl.Icon).withConfig({ displayName: "ContainerIcon", componentId: "sc-18399bz" }) `
9094
9472
  width: auto;
9095
9473
  height: auto;
9096
9474
  margin-bottom: 0.5rem;
@@ -9104,7 +9482,7 @@ const SvgContainerColorMixin$1 = styled.css `
9104
9482
  fill: ${({ $fontColor }) => $fontColor};
9105
9483
  }
9106
9484
  `;
9107
- const SvgContainer$1 = styled.div.withConfig({ displayName: "SvgContainer", componentId: "sc-1qs5gmx" }) `
9485
+ const SvgContainer$1 = styled.div.withConfig({ displayName: "SvgContainer", componentId: "sc-8l0ajh" }) `
9108
9486
  &&& {
9109
9487
  min-width: ${({ $width }) => ($width ? `${$width}px` : "1rem")};
9110
9488
  max-width: ${({ $width }) => ($width ? `${$width}px` : "1rem")};
@@ -9116,7 +9494,7 @@ const SvgContainer$1 = styled.div.withConfig({ displayName: "SvgContainer", comp
9116
9494
  }
9117
9495
  }
9118
9496
  `;
9119
- const TwoColumnContainerWrapper = styled(Container).withConfig({ displayName: "TwoColumnContainerWrapper", componentId: "sc-1ogv4d3" }) `
9497
+ const TwoColumnContainerWrapper = styled(Container).withConfig({ displayName: "TwoColumnContainerWrapper", componentId: "sc-1yxeknf" }) `
9120
9498
  flex-direction: row;
9121
9499
  flex-wrap: nowrap;
9122
9500
  align-items: center;
@@ -9143,6 +9521,16 @@ const CameraContainer = React.memo(({ elementConfig, type, renderElement }) => {
9143
9521
  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
9522
  });
9145
9523
 
9524
+ /**
9525
+ * Контекст, через который элемент `chart` сообщает контейнеру, где лежит поле графика.
9526
+ *
9527
+ * По макету подпись оси X и легенда выравниваются по полю графика, а не по краям контейнера.
9528
+ * Ширину шкал d3 вычисляет только при отрисовке, а подвал с подписью и легендой рисует
9529
+ * `ChartContainer` — снаружи элемента. Без контейнера (элемент сам по себе) провайдера нет,
9530
+ * и отчёт просто не отправляется.
9531
+ */
9532
+ const ChartPlotContext = React.createContext({});
9533
+
9146
9534
  /**
9147
9535
  * Контекст вписывания графика (`options.fill` контейнера Chart).
9148
9536
  *
@@ -9152,14 +9540,43 @@ const CameraContainer = React.memo(({ elementConfig, type, renderElement }) => {
9152
9540
  */
9153
9541
  const FillContext = React.createContext({ fill: false, fitHeight: false });
9154
9542
 
9543
+ const isSameInsets = (prev, next) => !!prev && prev.left === next.left && prev.right === next.right && prev.width === next.width;
9155
9544
  const ChartContainer = React.memo(({ elementConfig, isVisible, type, renderElement }) => {
9156
9545
  const { options, children } = elementConfig || {};
9157
- const { twoColumns, hideEmpty, height } = options || {};
9546
+ const { twoColumns, hideEmpty, height, legendInline } = options || {};
9158
9547
  const fill = options?.fill;
9159
9548
  const fillContextValue = React.useMemo(() => ({ fill: !!fill, fitHeight: !!fill && height != null }), [fill, height]);
9549
+ // Поле графика отчитывается после каждой отрисовки d3; одинаковые значения не пишем в стейт,
9550
+ // иначе отчёт и ре-рендер гоняли бы друг друга.
9551
+ const [plotInsets, setPlotInsets] = React.useState(null);
9552
+ const reportPlotInsets = React.useCallback((next) => setPlotInsets(prev => (isSameInsets(prev, next) ? prev : next)), []);
9553
+ const plotContextValue = React.useMemo(() => ({ reportPlotInsets }), [reportPlotInsets]);
9160
9554
  const aliasElement = children.find(child => child.id === "alias");
9161
9555
  const chartElement = children.find(child => child.id === "chart");
9162
9556
  const legendElement = children.find(child => child.id === "legend");
9557
+ const isLineChart = chartElement?.options?.chartType === "line";
9558
+ // Подпись оси X — часть макета осей линейного графика, как и подписи осей Y в `Chart`. У других типов
9559
+ // её нет, даже если она осталась в конфиге после смены типа: редактор показывает настройки осей только
9560
+ // линейному графику, и убрать там такую подпись было бы нечем.
9561
+ const xAxisTitle = React.useMemo(() => (isLineChart ? chartElement?.options?.axis?.x?.title : undefined), [isLineChart, chartElement?.options?.axis?.x?.title]);
9562
+ const footerJustify = React.useMemo(() => CHART_AXIS_ALIGN_JUSTIFY[chartElement?.options?.axis?.x?.align ?? "center"], [chartElement?.options?.axis?.x?.align]);
9563
+ // Легенда встаёт в строку с подписью оси X только когда та есть и легенда не вынесена вбок.
9564
+ const isLegendInline = React.useMemo(() => !!legendInline && !twoColumns && !!xAxisTitle, [legendInline, twoColumns, xAxisTitle]);
9565
+ // По макету подпись оси X и легенда у линейного графика выравниваются по полю графика, а не по
9566
+ // контейнеру. Легенда сбоку (twoColumns) живёт в своей колонке и в выравнивании не участвует.
9567
+ const footerInsets = React.useMemo(() => (isLineChart ? plotInsets : null), [isLineChart, plotInsets]);
9568
+ const isPlotLegend = isLineChart && !twoColumns;
9569
+ // Положение поля становится известно только после отрисовки: LineChart рисует d3 в useEffect, уже
9570
+ // после первого кадра. До отчёта подвал встал бы по краю контейнера и затем прыгнул на место, поэтому
9571
+ // прячем его — место под ним сохраняется, а появляется он вместе с линиями графика.
9572
+ const isAwaitingPlot = isLineChart && !plotInsets;
9573
+ // Пока подпись оси X не задана, легенда выравнивается прежней опцией `center` (по умолчанию — к левой
9574
+ // границе поля графика); с подписью — по ней, иначе подпись и легенда разъезжаются по разным краям.
9575
+ const legendJustify = React.useMemo(() => {
9576
+ if (xAxisTitle)
9577
+ return footerJustify;
9578
+ return legendElement?.options?.center ? "center" : "flex-start";
9579
+ }, [xAxisTitle, footerJustify, legendElement?.options?.center]);
9163
9580
  const { root, body } = useContainerRoot({ elementConfig });
9164
9581
  const { data, loading } = useChartData({
9165
9582
  element: chartElement,
@@ -9171,7 +9588,12 @@ const ChartContainer = React.memo(({ elementConfig, isVisible, type, renderEleme
9171
9588
  const hasItems = !!data[0]?.items?.length;
9172
9589
  if (!loading && !hasItems && hideEmpty)
9173
9590
  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" })) })] }))] }));
9591
+ 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" }) }) }) }));
9592
+ const footer = xAxisTitle ? (jsxRuntime.jsxs(ChartFooter, { "$align": footerJustify, "$insets": footerInsets, "$offsetTop": X_AXIS_TITLE_GAP - LINE_CHART_X_LABELS_BLANK, "$awaitingPlot": isAwaitingPlot, children: [jsxRuntime.jsx(ChartXAxisTitle, { children: xAxisTitle }), isLegendInline && jsxRuntime.jsx(ContainerLegend, { children: renderElement({ id: "legend" }) })] })) : null;
9593
+ // Легенда линейного графика: под подписью оси X — через X_AXIS_TITLE_LEGEND_GAP, без подписи — на её
9594
+ // месте, вплотную под подписями делений оси X. У остальных графиков — прежняя раскладка общей легенды.
9595
+ 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" }) }));
9596
+ 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
9597
  });
9176
9598
 
9177
9599
  const InnerContainerWrapper = styled.div.withConfig({ displayName: "InnerContainerWrapper", componentId: "sc-1a1fl7q" }) `
@@ -9529,6 +9951,7 @@ const DEFAULT_BAR_CHART_HEIGHT = 100;
9529
9951
  const BAR_CHART_HEADER_HEIGHT = 40;
9530
9952
  const BAR_CHART_FOOTER_MARGIN = 16;
9531
9953
  const LINE_CHART_LABEL_MARGIN = 20;
9954
+ const LINE_CHART_X_AXIS_PADDING = 15;
9532
9955
  const DEFAULT_BAR_CHART_MARGINS = {
9533
9956
  top: 0,
9534
9957
  left: 0,
@@ -10927,6 +11350,13 @@ const StyledSvg = styled(uilibGl.Flex).withConfig({ displayName: "StyledSvg", co
10927
11350
  ${({ $fontColor }) => !!$fontColor && StyledSvgColorMixin};
10928
11351
  `;
10929
11352
 
11353
+ const AttachmentIconImg = styled.img.withConfig({ displayName: "AttachmentIconImg", componentId: "sc-1qtgzdf" }) `
11354
+ flex-shrink: 0;
11355
+ width: ${({ $size }) => $size}px;
11356
+ height: ${({ $size }) => $size}px;
11357
+ object-fit: contain;
11358
+ `;
11359
+
10930
11360
  const ContainerIconValue = styled(uilibGl.Flex).withConfig({ displayName: "ContainerIconValue", componentId: "sc-1adlcb4" }) ``;
10931
11361
  /**
10932
11362
  * Обёртка иконки плитки. Собственный styled нужен, чтобы до иконки можно было дотянуться стилями:
@@ -11031,6 +11461,12 @@ const RoundedBackgroundContainerWrapper = styled(uilibGl.Flex).withConfig({ disp
11031
11461
  width: ${BIG_ICON_SIZE} !important;
11032
11462
  height: ${BIG_ICON_SIZE} !important;
11033
11463
  }
11464
+
11465
+ /* Растровая иконка из атрибута-вложения: её размер тоже задаёт контейнер. */
11466
+ ${AttachmentIconImg} {
11467
+ width: ${BIG_ICON_SIZE};
11468
+ height: ${BIG_ICON_SIZE};
11469
+ }
11034
11470
  }
11035
11471
  `};
11036
11472
 
@@ -15128,14 +15564,55 @@ const StyledIcon = styled(uilibGl.Icon).withConfig({ displayName: "StyledIcon",
15128
15564
  ${({ fontColor }) => !!fontColor && StyledIconFontColorMixin};
15129
15565
  `;
15130
15566
 
15567
+ /** Размер иконки-вложения, px, когда в конфиге не задан `options.fontSize`. */
15568
+ const ATTACHMENT_ICON_DEFAULT_SIZE = 16;
15569
+
15570
+ /**
15571
+ * Иконка из значения атрибута-вложения: первое изображение списка, остальные файлы пропускаются.
15572
+ * Значение приходит JSON-строкой с сервера или массивом после правки — `parseAttachments` понимает оба.
15573
+ * SVG помечается отдельно: его рисуют инлайном, чтобы перекрасить цветом иконки.
15574
+ */
15575
+ const getAttachmentIcon = (value) => {
15576
+ const image = parseAttachments(value).find(({ mimeType, name }) => IMAGE_FILE_TYPES.includes(getFileType(mimeType, name)));
15577
+ if (!image)
15578
+ return null;
15579
+ return {
15580
+ url: getResourceUrl(image.link),
15581
+ name: image.name,
15582
+ isSvg: image.mimeType === SVG_MIME_TYPE,
15583
+ };
15584
+ };
15585
+
15586
+ /**
15587
+ * Иконка из атрибута-вложения. SVG рисуется инлайном и перекрашивается `fontColor`, как у ElementSvg;
15588
+ * растровый файл — картинкой как есть. Изображений среди вложений нет — не рендерится ничего.
15589
+ */
15590
+ const AttachmentIcon = React.memo(({ value, fontSize, fontColor, style }) => {
15591
+ const icon = React.useMemo(() => getAttachmentIcon(value), [value]);
15592
+ const size = React.useMemo(() => Number(fontSize) || ATTACHMENT_ICON_DEFAULT_SIZE, [fontSize]);
15593
+ const rasterUrl = React.useMemo(() => (icon && !icon.isSvg ? icon.url : null), [icon]);
15594
+ const imageSrc = useFetchImageWithAuth(rasterUrl);
15595
+ if (!icon)
15596
+ return null;
15597
+ if (icon.isSvg) {
15598
+ return jsxRuntime.jsx(SvgImage, { url: icon.url, width: size, height: size, fontColor: fontColor });
15599
+ }
15600
+ if (!imageSrc)
15601
+ return null;
15602
+ return jsxRuntime.jsx(AttachmentIconImg, { src: imageSrc, alt: icon.name, style: style, "$size": size });
15603
+ });
15604
+
15131
15605
  const ElementIcon = React.memo(({ type, elementConfig, ...rest }) => {
15132
15606
  const ctx = useWidgetContext(type);
15133
15607
  const attributes = rest.attributes || ctx.attributes;
15134
15608
  const { value, attributeName, options, style } = elementConfig || {};
15135
15609
  const { fontSize, fontColor } = options || {};
15136
- const iconValue = React.useMemo(() => attributeName
15137
- ? attributes?.find(item => item.attributeName === attributeName)?.value
15138
- : value, [attributeName, attributes, value]);
15610
+ const attribute = React.useMemo(() => (attributeName ? getAttributeByName(attributeName, attributes) : undefined), [attributeName, attributes]);
15611
+ const iconValue = React.useMemo(() => (attributeName ? attribute?.value : value), [attributeName, attribute, value]);
15612
+ // Во вложениях лежит список файлов, а не имя глифа: иконкой становится первое изображение.
15613
+ if (attribute?.subType === api.StringSubType.Attachments) {
15614
+ return jsxRuntime.jsx(AttachmentIcon, { value: attribute.value, fontSize: fontSize, fontColor: fontColor, style: style });
15615
+ }
15139
15616
  return (jsxRuntime.jsx(StyledIcon, { kind: iconValue, fontSize: fontSize, fontColor: fontColor, style: style }));
15140
15617
  });
15141
15618
 
@@ -15192,9 +15669,9 @@ const ElementImage = React.memo(({ type, elementConfig }) => {
15192
15669
  });
15193
15670
 
15194
15671
  const ElementLegend = React.memo(({ type, element, elementConfig }) => {
15195
- const { attributes, dataSources, expandedContainers } = useWidgetContext(type);
15672
+ const { attributes, expandedContainers } = useWidgetContext(type);
15196
15673
  const { options } = elementConfig || {};
15197
- const { twoColumns, chartId, relatedDataSources, fontSize } = options || {};
15674
+ const { twoColumns, column, chartId, relatedDataSources, fontSize } = options || {};
15198
15675
  const chartElement = React.useMemo(() => findAnd.returnFound(element?.children, { id: chartId }), [chartId, element?.children]);
15199
15676
  const { data, loading } = useChartData({
15200
15677
  element: chartElement,
@@ -15205,10 +15682,10 @@ const ElementLegend = React.memo(({ type, element, elementConfig }) => {
15205
15682
  if (!data[0]?.items?.length)
15206
15683
  return [];
15207
15684
  if (chartElement?.options?.chartType === "line") {
15208
- return axes.map(({ alias, axisColor }) => {
15685
+ return axes.map(related => {
15209
15686
  return {
15210
- name: alias,
15211
- color: axisColor,
15687
+ name: related.alias,
15688
+ color: resolveAxis(related).color,
15212
15689
  };
15213
15690
  });
15214
15691
  }
@@ -15222,7 +15699,7 @@ const ElementLegend = React.memo(({ type, element, elementConfig }) => {
15222
15699
  });
15223
15700
  }, [data, chartElement?.options?.chartType, relatedDataSources, axes, attributes]);
15224
15701
  return !chartElement?.options?.expanded ||
15225
- expandedContainers?.[chartElement.id] ? (jsxRuntime.jsx(ChartLegend, { data: legendData, loading: loading, chartElement: chartElement, twoColumns: twoColumns, fontSize: fontSize, type: type })) : null;
15702
+ expandedContainers?.[chartElement.id] ? (jsxRuntime.jsx(ChartLegend, { data: legendData, loading: loading, chartElement: chartElement, twoColumns: twoColumns, column: column, fontSize: fontSize, type: type })) : null;
15226
15703
  });
15227
15704
 
15228
15705
  const ExternalLink = styled(uilibGl.IconButton).attrs(() => ({
@@ -16790,7 +17267,11 @@ const getDataFromRelatedFeatures = ({ t, config, filters, relatedConfig, dataSou
16790
17267
  if (isOtherSliced) {
16791
17268
  data = data.slice(0, config?.options?.otherItems);
16792
17269
  }
16793
- const gradientArray = relatedConfig.chartAxis
17270
+ // Источник чарта отличается от источника фильтра наличием осей: BarChartFilter собирает
17271
+ // relatedConfig вручную и оси не задаёт, потому что ему нужна исходная палитра, а не растянутая
17272
+ // под число элементов. Гейт смотрит и на устаревший chartAxis — иначе у старых конфигов
17273
+ // молча поехали бы цвета столбцов и долей.
17274
+ const gradientArray = hasAxisConfig(relatedConfig)
16794
17275
  ? stretchPalette(colors, data.length)
16795
17276
  : colors;
16796
17277
  const filter = getConfigFilter(relatedConfig?.filterName, filters);
@@ -18856,6 +19337,15 @@ const StackBar = ({ data, filterName, type, alias, options, fill, renderElement,
18856
19337
  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
19338
  };
18858
19339
 
19340
+ /** Горизонтальные подписи осей над телом графика: левая прижата влево, правая — вправо. */
19341
+ 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 })] }));
19342
+ /** Вертикальная подпись оси сбоку от тела графика. Без подписи ничего не рендерит. */
19343
+ const AxisTitleSideSlot = ({ side, axisTitle, neutralColor, plotTop, plotBottom, }) => {
19344
+ if (!axisTitle)
19345
+ return null;
19346
+ 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 }) }));
19347
+ };
19348
+
18859
19349
  const Chart = React.memo(({ config, element, elementConfig, type, renderElement }) => {
18860
19350
  const theme = styled.useTheme();
18861
19351
  const { palette } = theme;
@@ -18876,12 +19366,18 @@ const Chart = React.memo(({ config, element, elementConfig, type, renderElement
18876
19366
  const radius = options?.radius || DEFAULT_BARCHART_RADIUS;
18877
19367
  const padding = options?.padding || DEFAULT_BAR_PADDING;
18878
19368
  const fontColor = options?.fontColor || palette.iconDisabled;
19369
+ // Нейтральная подпись оси — тем же цветом, что и значения шкалы (на макете у них один стиль).
19370
+ const axisTitleColor = options?.fontColor || palette.textDisabled;
18879
19371
  const angle = options?.angle || DEFAULT_CHART_ANGLE;
18880
19372
  const barWidth = options?.barWidth || DEFAULT_BAR_WIDTH;
18881
19373
  const cornerRadius = options?.cornerRadius || 0;
18882
19374
  const relatedAttributes = React.useMemo(() => relatedDataSources || [], [relatedDataSources]);
18883
- const axes = React.useMemo(() => relatedAttributes.filter(({ chartAxis }) => chartAxis === "y"), [relatedAttributes]);
19375
+ const axes = React.useMemo(() => relatedAttributes.filter(isValueAxis), [relatedAttributes]);
18884
19376
  const filterName = getChartFilterName(relatedDataSources);
19377
+ const { axisTitles, hasTopTitles, hasSideTitles, axisTitlesHeight, hideLeftAxis, hideRightAxis, } = useChartAxisTitles({ options, axes, isLineChart });
19378
+ // Ширина вертикальных подписей зависит от числа строк текста, поэтому тело между ними меряем
19379
+ // по факту, а не вычитаем константу.
19380
+ const [lineBodyRef, { width: measuredBodyWidth }] = useResizeBox(hasSideTitles);
18885
19381
  const isRelated = !!relatedAttributes?.length;
18886
19382
  const { data, loading } = useChartData({ element, type });
18887
19383
  const totalWidthFromBarWidth = (data[0]?.items?.length || 0) * (barWidth + padding);
@@ -18909,17 +19405,54 @@ const Chart = React.memo(({ config, element, elementConfig, type, renderElement
18909
19405
  const chartHeight = React.useMemo(() => (fitsHeight && measuredHeight ? measuredHeight : height), [fitsHeight, measuredHeight, height]);
18910
19406
  // Круглый график вписываем по короткой стороне (object-fit: contain), центрируя в ячейке.
18911
19407
  const pieSide = React.useMemo(() => (fitsHeight && measuredHeight ? Math.min(chartWidth, measuredHeight) : chartWidth), [fitsHeight, measuredHeight, chartWidth]);
19408
+ // Тело линейного графика — это ячейка за вычетом подписей осей. По высоте вычитаем только в
19409
+ // fill: вне fill chartHeight означает высоту самого тела, и обёртка честно растёт на подписи.
19410
+ const lineBodyWidth = React.useMemo(() => {
19411
+ if (!hasSideTitles)
19412
+ return chartWidth;
19413
+ if (measuredBodyWidth)
19414
+ return measuredBodyWidth;
19415
+ // До первого замера — оценка: одна строка текста и зазор на каждую подпись.
19416
+ const sideTitlesCount = Number(!!axisTitles.left) + Number(!!axisTitles.right);
19417
+ return Math.max(chartWidth - sideTitlesCount * (AXIS_TITLE_LINE_HEIGHT + AXIS_TITLE_GAP), 0);
19418
+ }, [hasSideTitles, measuredBodyWidth, chartWidth, axisTitles]);
19419
+ const lineBodyHeight = React.useMemo(() => (fitsHeight ? Math.max(chartHeight - axisTitlesHeight, 0) : chartHeight), [fitsHeight, chartHeight, axisTitlesHeight]);
19420
+ // Обёртка выше тела на строку горизонтальных подписей. В fill вся высота уже отмерена ячейкой,
19421
+ // поэтому прибавлять нечего — иначе тело вылезло бы за нижнюю границу.
19422
+ const lineWrapperHeight = React.useMemo(() => (fitsHeight ? chartHeight : chartHeight + axisTitlesHeight), [fitsHeight, chartHeight, axisTitlesHeight]);
18912
19423
  const [customize] = useChartChange({
18913
19424
  dataSources: config.dataSources,
18914
19425
  chartId: elementConfig?.id,
18915
- width: chartWidth,
18916
- height: chartHeight,
19426
+ width: lineBodyWidth,
19427
+ height: lineBodyHeight,
18917
19428
  fontColor,
18918
19429
  relatedAttributes,
18919
19430
  defaultColor: primaryColor,
18920
19431
  markers: configMarkers,
18921
19432
  showMarkers,
18922
19433
  });
19434
+ const { formatLeftTick, formatRightTick } = useChartAxisTickFormat({
19435
+ dataSources: config.dataSources,
19436
+ relatedAttributes,
19437
+ });
19438
+ const { reportPlotInsets } = React.useContext(ChartPlotContext);
19439
+ // Положение поля графика известно только после отрисовки: ширину шкал вычисляет d3. Смещение тела
19440
+ // берём из offset-размеров, а не из getBoundingClientRect: они не искажаются масштабом превью и
19441
+ // transform предков, а координаты шкалы всегда в пикселях SVG.
19442
+ const customizeLine = React.useCallback(args => {
19443
+ customize?.(args);
19444
+ const body = lineBodyRef.current;
19445
+ const layout = body?.parentElement?.parentElement;
19446
+ if (!reportPlotInsets || !body || !layout)
19447
+ return;
19448
+ const bodyLeft = body.offsetLeft - layout.offsetLeft;
19449
+ const [plotLeft, plotRight] = args.xScale.range();
19450
+ reportPlotInsets({
19451
+ left: Math.round(bodyLeft + plotLeft),
19452
+ right: Math.round(layout.offsetWidth - bodyLeft - plotRight),
19453
+ width: layout.offsetWidth,
19454
+ });
19455
+ }, [customize, reportPlotInsets, lineBodyRef]);
18923
19456
  const { formatFilterColor, onFilter } = useWidgetFilters(type, filterName, data?.[0]?.items);
18924
19457
  const formattedAttributes = React.useMemo(() => getFormattedAttributes(t, (data[0]?.items || []), attributes, element), [attributes, data, element, t]);
18925
19458
  const totalSum = React.useMemo(() => isRelated
@@ -18978,6 +19511,17 @@ const Chart = React.memo(({ config, element, elementConfig, type, renderElement
18978
19511
  left: markersMargin,
18979
19512
  };
18980
19513
  }, [showMarkers, markers?.length]);
19514
+ // Значения шкалы барчарта d3 центрирует на делении, и крайние деления наполовину выступают за поле.
19515
+ // При нулевом поле SVG выступ уходит за SVG, где его срезает BarChartContainer (overflow-x: hidden,
19516
+ // отчего и по вертикали содержимое не visible). Место под выступ резервируем внутри SVG — внешние
19517
+ // размеры обёрток не меняются. Без значений шкалы (showLabels) выступать нечему.
19518
+ const barMargin = React.useMemo(() => showLabels
19519
+ ? {
19520
+ ...margin,
19521
+ top: Math.max(margin.top, Y_AXIS_LABEL_OVERHANG),
19522
+ bottom: Math.max(margin.bottom, Y_AXIS_LABEL_OVERHANG),
19523
+ }
19524
+ : margin, [showLabels, margin]);
18981
19525
  const formatTooltipName = React.useCallback((name) => isRelated ? name : tooltipNameFromAttributes(name, formattedAttributes), [formattedAttributes, isRelated]);
18982
19526
  const formatTooltipValue = React.useCallback((value, name) => isRelated
18983
19527
  ? tooltipValueFromRelatedFeatures(t, value, relatedAttributes, data[0]?.layerInfo)
@@ -19004,26 +19548,42 @@ const Chart = React.memo(({ config, element, elementConfig, type, renderElement
19004
19548
  attributeUnits: item.attributeUnits,
19005
19549
  dataSourceName: item.dataSourceName,
19006
19550
  stroke: item.color || primaryColor,
19551
+ axis: item.axisSide ?? "left",
19007
19552
  values: item.items
19008
19553
  ?.filter(({ value }) => !isEmptyValue(value))
19009
19554
  ?.map(({ value }) => Number(value)),
19010
19555
  }));
19011
- const isHidedY = axes.every(({ hideAxis }) => hideAxis);
19012
- const chartValues = lineChartData
19013
- .flat()
19556
+ const isHidedY = hideLeftAxis && hideRightAxis;
19557
+ const valuesOfSide = (side) => lineChartData
19558
+ .filter(({ axis }) => axis === side)
19014
19559
  .map(({ values }) => values)
19015
19560
  .flat();
19561
+ const leftValues = valuesOfSide("left");
19562
+ const rightValues = valuesOfSide("right");
19016
19563
  // Резервируем нижнюю полосу под подписи оси X: без неё ось прижата к нижней границе,
19017
19564
  // подписи (сдвинутые на +16px) вылезают за тело и режутся overflow: hidden у ChartFillMeasure.
19565
+ // Сверху резервируем выступ верхнего деления шкалы: оно центрировано на линии сетки и при нулевом
19566
+ // поле наполовину уходит за SVG, где в fill его срезает overflow: hidden у ChartFillMeasure.
19567
+ const topOverhang = Math.max(Y_AXIS_LABEL_OVERHANG - margin.top, 0);
19018
19568
  const lineMargin = {
19019
19569
  ...margin,
19570
+ top: margin.top + topOverhang,
19020
19571
  bottom: Math.max(margin.bottom, LINE_CHART_LABEL_MARGIN),
19021
19572
  };
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 })] }));
19573
+ // draw.ts вычитает верхнее поле и из нижней границы поля графика — компенсируем добавленное,
19574
+ // чтобы низ поля и зазор до подписей оси X остались на месте.
19575
+ const lineXAxisPadding = LINE_CHART_X_AXIS_PADDING - topOverhang;
19576
+ // Колонки вертикальных подписей повторяют поля SVG, чтобы подпись центрировалась по полю
19577
+ // графика, а не по всей высоте вместе с подписями оси X.
19578
+ const plotTop = lineMargin.top;
19579
+ const plotBottom = lineMargin.top + lineMargin.bottom + lineXAxisPadding;
19580
+ 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 => {
19581
+ // Обе оси приходят в один колбэк, поэтому сторону определяем по классу.
19582
+ const isRight = yAxis.classed(charts.lineChartClassNames.lineChartYScaleRight);
19583
+ if (isRight ? hideRightAxis : hideLeftAxis) {
19584
+ yAxis.remove();
19585
+ }
19586
+ }, 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
19587
  }
19028
19588
  if (isStackBar) {
19029
19589
  // Вне fill chartHeight — высота самой полосы, и обёртка честно становится выше на строку
@@ -19053,7 +19613,7 @@ const Chart = React.memo(({ config, element, elementConfig, type, renderElement
19053
19613
  const barRadius = options?.cornerRadius ?? DEFAULT_BARCHART_RADIUS;
19054
19614
  bars.attr("rx", barRadius);
19055
19615
  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 }) }) }));
19616
+ }, margin: barMargin, xAxisPadding: 0, yAxisPadding: 0, formatTooltipValue: formatTooltipValue, formatTooltipName: formatTooltipName, hideTooltipGroupName: true, dynamicTooltipEnable: true, isBarTooltip: true, onBarClick: filterName ? item => onFilter(item.name) : undefined }) }) }));
19057
19617
  }, [
19058
19618
  customXAxisBottom,
19059
19619
  customYAxisLeft,
@@ -19069,6 +19629,15 @@ const Chart = React.memo(({ config, element, elementConfig, type, renderElement
19069
19629
  formatFilterColor,
19070
19630
  chartWidth,
19071
19631
  chartHeight,
19632
+ lineBodyWidth,
19633
+ lineBodyHeight,
19634
+ lineWrapperHeight,
19635
+ axisTitles,
19636
+ hasTopTitles,
19637
+ hasSideTitles,
19638
+ axisTitleColor,
19639
+ hideLeftAxis,
19640
+ hideRightAxis,
19072
19641
  fitsHeight,
19073
19642
  pieSide,
19074
19643
  fill,
@@ -19078,11 +19647,14 @@ const Chart = React.memo(({ config, element, elementConfig, type, renderElement
19078
19647
  formatTooltipValue,
19079
19648
  formatTooltipName,
19080
19649
  filterName,
19081
- axes,
19082
19650
  labels,
19083
19651
  margin,
19652
+ barMargin,
19084
19653
  renderLineChartTooltip,
19085
- customize,
19654
+ customizeLine,
19655
+ lineBodyRef,
19656
+ formatLeftTick,
19657
+ formatRightTick,
19086
19658
  primaryColor,
19087
19659
  type,
19088
19660
  elementConfig?.children,
@@ -19099,14 +19671,14 @@ const Chart = React.memo(({ config, element, elementConfig, type, renderElement
19099
19671
  ]);
19100
19672
  if (!isVisibleContainer(id, expandable, expanded, expandedContainers))
19101
19673
  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] }));
19674
+ 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
19675
  if (!fill)
19104
19676
  return chartBody;
19105
19677
  // fill: тело центрируется в измеряемой обёртке, ref которой питает ResizeObserver обеими осями.
19106
19678
  return jsxRuntime.jsx(ChartFillMeasure, { ref: chartRef, children: chartBody });
19107
19679
  });
19108
19680
 
19109
- const ChartLegend = ({ data, chartElement, fontSize, type, twoColumns, loading }) => {
19681
+ const ChartLegend = ({ data, chartElement, fontSize, type, twoColumns, column, loading }) => {
19110
19682
  const { t } = useGlobalContext();
19111
19683
  const { sliceItems, showMore, onShowMore } = useShownOtherItems(chartElement?.options);
19112
19684
  const axes = React.useMemo(() => getChartAxes(chartElement), [chartElement]);
@@ -19115,7 +19687,7 @@ const ChartLegend = ({ data, chartElement, fontSize, type, twoColumns, loading }
19115
19687
  const showMoreBtn = !!shownItems && data.length > shownItems;
19116
19688
  if (!loading && !data?.length && !!hideEmpty)
19117
19689
  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: "Другое" }) }))] }));
19690
+ 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
19691
  };
19120
19692
 
19121
19693
  const ContainerChildren = React.memo(({ type, items, isColumn, isMain, renderElement }) => {
@@ -19973,6 +20545,10 @@ const DEFAULT_HEATMAP_STYLE = {
19973
20545
  exports.ALIGNMENTS = ALIGNMENTS;
19974
20546
  exports.ALIGN_ITEMS = ALIGN_ITEMS;
19975
20547
  exports.ATTRIBUTE_ICON_ELEMENT_TYPES = ATTRIBUTE_ICON_ELEMENT_TYPES;
20548
+ exports.AXIS_TITLE_GAP = AXIS_TITLE_GAP;
20549
+ exports.AXIS_TITLE_LINE_HEIGHT = AXIS_TITLE_LINE_HEIGHT;
20550
+ exports.AXIS_TITLE_TOP_GRID_GAP = AXIS_TITLE_TOP_GRID_GAP;
20551
+ exports.AXIS_TITLE_TOP_HEIGHT = AXIS_TITLE_TOP_HEIGHT;
19976
20552
  exports.AddButtonRow = AddButtonRow;
19977
20553
  exports.AddFeatureButton = AddFeatureButton;
19978
20554
  exports.AddFeatureContainer = AddFeatureContainer;
@@ -19982,6 +20558,7 @@ exports.AttributeGalleryContainer = AttributeGalleryContainer;
19982
20558
  exports.AttributeLabel = AttributeLabel;
19983
20559
  exports.BASE_CONTAINER_STYLE = BASE_CONTAINER_STYLE;
19984
20560
  exports.BG_IMAGE_SLOT_ID = BG_IMAGE_SLOT_ID;
20561
+ exports.CHART_AXIS_ALIGN_JUSTIFY = CHART_AXIS_ALIGN_JUSTIFY;
19985
20562
  exports.CHART_TYPES = CHART_TYPES;
19986
20563
  exports.COMPACT_FRACTION_DIGITS = COMPACT_FRACTION_DIGITS;
19987
20564
  exports.CONFIG_PAGES_ID = CONFIG_PAGES_ID;
@@ -20135,6 +20712,8 @@ exports.HiddenTitleItems = HiddenTitleItems;
20135
20712
  exports.IconContainer = IconContainer;
20136
20713
  exports.ImageContainer = ImageContainer;
20137
20714
  exports.LEFT_PANEL_HEADER_HEIGHT = LEFT_PANEL_HEADER_HEIGHT;
20715
+ exports.LINE_CHART_X_LABELS_BLANK = LINE_CHART_X_LABELS_BLANK;
20716
+ exports.LINE_CHART_Y_TICKS = LINE_CHART_Y_TICKS;
20138
20717
  exports.Layer = Layer;
20139
20718
  exports.LayerDescription = LayerDescription;
20140
20719
  exports.LayerGroupList = LayerGroupList;
@@ -20214,6 +20793,9 @@ exports.TwoColumnContainer = TwoColumnContainer;
20214
20793
  exports.UploadContainer = UploadContainer;
20215
20794
  exports.VIEW_MODES = VIEW_MODES;
20216
20795
  exports.VoteContainer = VoteContainer;
20796
+ exports.X_AXIS_TITLE_GAP = X_AXIS_TITLE_GAP;
20797
+ exports.X_AXIS_TITLE_LEGEND_GAP = X_AXIS_TITLE_LEGEND_GAP;
20798
+ exports.Y_AXIS_LABEL_OVERHANG = Y_AXIS_LABEL_OVERHANG;
20217
20799
  exports.ZOOM_FILTER_NAME = ZOOM_FILTER_NAME;
20218
20800
  exports.addDataSource = addDataSource;
20219
20801
  exports.addDataSources = addDataSources;
@@ -20273,6 +20855,7 @@ exports.getAttributeIconUrl = getAttributeIconUrl;
20273
20855
  exports.getAttributeValue = getAttributeValue;
20274
20856
  exports.getAttributesConfiguration = getAttributesConfiguration;
20275
20857
  exports.getAverageTrackSize = getAverageTrackSize;
20858
+ exports.getAxisSide = getAxisSide;
20276
20859
  exports.getChartAxes = getChartAxes;
20277
20860
  exports.getChartFilterName = getChartFilterName;
20278
20861
  exports.getChartMarkers = getChartMarkers;
@@ -20324,6 +20907,7 @@ exports.getTrackSize = getTrackSize;
20324
20907
  exports.getTrackSizeKey = getTrackSizeKey;
20325
20908
  exports.getTrackSizes = getTrackSizes;
20326
20909
  exports.getWrapperSizeStyle = getWrapperSizeStyle;
20910
+ exports.hasAxisConfig = hasAxisConfig;
20327
20911
  exports.hasContainerBgImage = hasContainerBgImage;
20328
20912
  exports.hexToRgba = hexToRgba;
20329
20913
  exports.isCrossOriginUrl = isCrossOriginUrl;
@@ -20340,8 +20924,10 @@ exports.isNotValidSelectedTab = isNotValidSelectedTab;
20340
20924
  exports.isNumeric = isNumeric;
20341
20925
  exports.isObject = isObject;
20342
20926
  exports.isProxyService = isProxyService;
20927
+ exports.isRightAxis = isRightAxis;
20343
20928
  exports.isRootOwningContainer = isRootOwningContainer;
20344
20929
  exports.isTreeFilterValue = isTreeFilterValue;
20930
+ exports.isValueAxis = isValueAxis;
20345
20931
  exports.isVisibleContainer = isVisibleContainer;
20346
20932
  exports.mapNodeById = mapNodeById;
20347
20933
  exports.mergeAttributeConfigurations = mergeAttributeConfigurations;
@@ -20357,6 +20943,7 @@ exports.pointOptions = pointOptions;
20357
20943
  exports.removeDataSource = removeDataSource;
20358
20944
  exports.removeTracks = removeTracks;
20359
20945
  exports.replaceNodesByIds = replaceNodesByIds;
20946
+ exports.resolveAxis = resolveAxis;
20360
20947
  exports.rgbToHex = rgbToHex;
20361
20948
  exports.roundFr = roundFr;
20362
20949
  exports.roundTotalSum = roundTotalSum;
@@ -20384,6 +20971,9 @@ exports.useAttachmentsView = useAttachmentsView;
20384
20971
  exports.useAutoCompleteControl = useAutoCompleteControl;
20385
20972
  exports.useBeforeSave = useBeforeSave;
20386
20973
  exports.useBgImageHost = useBgImageHost;
20974
+ exports.useChartAxisAttributes = useChartAxisAttributes;
20975
+ exports.useChartAxisTickFormat = useChartAxisTickFormat;
20976
+ exports.useChartAxisTitles = useChartAxisTitles;
20387
20977
  exports.useChartChange = useChartChange;
20388
20978
  exports.useChartData = useChartData;
20389
20979
  exports.useConfigDataSources = useConfigDataSources;