@gravity-ui/charts 1.0.1 → 1.2.0

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 (101) hide show
  1. package/dist/cjs/components/Axis/AxisX.d.ts +1 -0
  2. package/dist/cjs/components/Axis/AxisX.js +57 -12
  3. package/dist/cjs/components/Axis/AxisY.d.ts +1 -0
  4. package/dist/cjs/components/Axis/AxisY.js +83 -12
  5. package/dist/cjs/components/ChartInner/index.js +3 -3
  6. package/dist/cjs/components/ChartInner/styles.css +2 -0
  7. package/dist/cjs/components/ChartInner/useChartInnerProps.d.ts +3 -0
  8. package/dist/cjs/components/ChartInner/useChartInnerProps.js +14 -3
  9. package/dist/cjs/components/Legend/index.js +2 -2
  10. package/dist/cjs/components/Title/index.js +1 -1
  11. package/dist/cjs/components/Tooltip/DefaultContent.js +41 -23
  12. package/dist/cjs/hooks/useChartDimensions/index.js +3 -0
  13. package/dist/cjs/hooks/useChartDimensions/utils.js +3 -0
  14. package/dist/cjs/hooks/useChartOptions/types.d.ts +5 -3
  15. package/dist/cjs/hooks/useChartOptions/x-axis.js +8 -0
  16. package/dist/cjs/hooks/useChartOptions/y-axis.js +8 -0
  17. package/dist/cjs/hooks/useSeries/prepare-waterfall.js +40 -28
  18. package/dist/cjs/hooks/useSeries/types.d.ts +4 -3
  19. package/dist/cjs/hooks/useShapes/HtmlLayer.js +2 -1
  20. package/dist/cjs/hooks/useShapes/area/prepare-data.js +1 -0
  21. package/dist/cjs/hooks/useShapes/bar-x/prepare-data.js +1 -0
  22. package/dist/cjs/hooks/useShapes/bar-y/prepare-data.js +1 -0
  23. package/dist/cjs/hooks/useShapes/line/prepare-data.js +1 -0
  24. package/dist/cjs/hooks/useShapes/pie/index.js +1 -1
  25. package/dist/cjs/hooks/useShapes/pie/prepare-data.js +1 -0
  26. package/dist/cjs/hooks/useShapes/radar/prepare-data.js +1 -0
  27. package/dist/cjs/hooks/useShapes/treemap/index.js +1 -1
  28. package/dist/cjs/hooks/useShapes/treemap/prepare-data.js +3 -2
  29. package/dist/cjs/hooks/useShapes/waterfall/prepare-data.js +4 -2
  30. package/dist/cjs/i18n/keysets/en.json +3 -1
  31. package/dist/cjs/i18n/keysets/ru.json +3 -1
  32. package/dist/cjs/libs/chart-error/index.d.ts +1 -0
  33. package/dist/cjs/libs/chart-error/index.js +1 -0
  34. package/dist/cjs/types/chart/axis.d.ts +32 -7
  35. package/dist/cjs/types/chart/waterfall.d.ts +9 -0
  36. package/dist/cjs/types/chart-ui.d.ts +1 -0
  37. package/dist/cjs/utils/chart/axis-generators/bottom.d.ts +1 -0
  38. package/dist/cjs/utils/chart/axis-generators/bottom.js +16 -1
  39. package/dist/cjs/utils/chart/axis.d.ts +12 -1
  40. package/dist/cjs/utils/chart/axis.js +35 -0
  41. package/dist/cjs/utils/chart/get-closest-data.js +23 -13
  42. package/dist/cjs/utils/chart/index.d.ts +2 -1
  43. package/dist/cjs/utils/chart/index.js +5 -5
  44. package/dist/cjs/utils/chart/series/waterfall.d.ts +2 -2
  45. package/dist/cjs/utils/chart/series/waterfall.js +1 -7
  46. package/dist/cjs/utils/chart/types.d.ts +1 -0
  47. package/dist/cjs/utils/chart/types.js +1 -0
  48. package/dist/cjs/utils/chart-ui/pie-center-text.d.ts +1 -0
  49. package/dist/cjs/utils/chart-ui/pie-center-text.js +3 -1
  50. package/dist/cjs/validation/index.js +144 -0
  51. package/dist/esm/components/Axis/AxisX.d.ts +1 -0
  52. package/dist/esm/components/Axis/AxisX.js +57 -12
  53. package/dist/esm/components/Axis/AxisY.d.ts +1 -0
  54. package/dist/esm/components/Axis/AxisY.js +83 -12
  55. package/dist/esm/components/ChartInner/index.js +3 -3
  56. package/dist/esm/components/ChartInner/styles.css +2 -0
  57. package/dist/esm/components/ChartInner/useChartInnerProps.d.ts +3 -0
  58. package/dist/esm/components/ChartInner/useChartInnerProps.js +14 -3
  59. package/dist/esm/components/Legend/index.js +2 -2
  60. package/dist/esm/components/Title/index.js +1 -1
  61. package/dist/esm/components/Tooltip/DefaultContent.js +41 -23
  62. package/dist/esm/hooks/useChartDimensions/index.js +3 -0
  63. package/dist/esm/hooks/useChartDimensions/utils.js +3 -0
  64. package/dist/esm/hooks/useChartOptions/types.d.ts +5 -3
  65. package/dist/esm/hooks/useChartOptions/x-axis.js +8 -0
  66. package/dist/esm/hooks/useChartOptions/y-axis.js +8 -0
  67. package/dist/esm/hooks/useSeries/prepare-waterfall.js +40 -28
  68. package/dist/esm/hooks/useSeries/types.d.ts +4 -3
  69. package/dist/esm/hooks/useShapes/HtmlLayer.js +2 -1
  70. package/dist/esm/hooks/useShapes/area/prepare-data.js +1 -0
  71. package/dist/esm/hooks/useShapes/bar-x/prepare-data.js +1 -0
  72. package/dist/esm/hooks/useShapes/bar-y/prepare-data.js +1 -0
  73. package/dist/esm/hooks/useShapes/line/prepare-data.js +1 -0
  74. package/dist/esm/hooks/useShapes/pie/index.js +1 -1
  75. package/dist/esm/hooks/useShapes/pie/prepare-data.js +1 -0
  76. package/dist/esm/hooks/useShapes/radar/prepare-data.js +1 -0
  77. package/dist/esm/hooks/useShapes/treemap/index.js +1 -1
  78. package/dist/esm/hooks/useShapes/treemap/prepare-data.js +3 -2
  79. package/dist/esm/hooks/useShapes/waterfall/prepare-data.js +4 -2
  80. package/dist/esm/i18n/keysets/en.json +3 -1
  81. package/dist/esm/i18n/keysets/ru.json +3 -1
  82. package/dist/esm/libs/chart-error/index.d.ts +1 -0
  83. package/dist/esm/libs/chart-error/index.js +1 -0
  84. package/dist/esm/types/chart/axis.d.ts +32 -7
  85. package/dist/esm/types/chart/waterfall.d.ts +9 -0
  86. package/dist/esm/types/chart-ui.d.ts +1 -0
  87. package/dist/esm/utils/chart/axis-generators/bottom.d.ts +1 -0
  88. package/dist/esm/utils/chart/axis-generators/bottom.js +16 -1
  89. package/dist/esm/utils/chart/axis.d.ts +12 -1
  90. package/dist/esm/utils/chart/axis.js +35 -0
  91. package/dist/esm/utils/chart/get-closest-data.js +23 -13
  92. package/dist/esm/utils/chart/index.d.ts +2 -1
  93. package/dist/esm/utils/chart/index.js +5 -5
  94. package/dist/esm/utils/chart/series/waterfall.d.ts +2 -2
  95. package/dist/esm/utils/chart/series/waterfall.js +1 -7
  96. package/dist/esm/utils/chart/types.d.ts +1 -0
  97. package/dist/esm/utils/chart/types.js +1 -0
  98. package/dist/esm/utils/chart-ui/pie-center-text.d.ts +1 -0
  99. package/dist/esm/utils/chart-ui/pie-center-text.js +3 -1
  100. package/dist/esm/validation/index.js +144 -0
  101. package/package.json +1 -1
@@ -8,6 +8,7 @@ type Props = {
8
8
  scale: ChartScale;
9
9
  split: PreparedSplit;
10
10
  plotRef?: React.MutableRefObject<SVGGElement | null>;
11
+ leftmostLimit?: number;
11
12
  };
12
13
  export declare function getTitlePosition(args: {
13
14
  axis: PreparedAxis;
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import { line, select } from 'd3';
3
- import { block, formatAxisTickLabel, getAxisTitleRows, getClosestPointsRange, getLineDashArray, getMaxTickCount, getScaleTicks, getTicksCount, handleOverflowingText, } from '../../utils';
3
+ import { block, formatAxisTickLabel, getAxisTitleRows, getBandsPosition, getClosestPointsRange, getLineDashArray, getMaxTickCount, getScaleTicks, getTicksCount, handleOverflowingText, } from '../../utils';
4
4
  import { axisBottom } from '../../utils/chart/axis-generators';
5
5
  import './styles.css';
6
6
  const b = block('axis');
@@ -42,12 +42,23 @@ export function getTitlePosition(args) {
42
42
  return { x, y };
43
43
  }
44
44
  export const AxisX = React.memo(function AxisX(props) {
45
- const { axis, width, height: totalHeight, scale, split, plotRef } = props;
45
+ const { axis, width, height: totalHeight, scale, split, plotRef, leftmostLimit } = props;
46
46
  const ref = React.useRef(null);
47
47
  React.useEffect(() => {
48
48
  if (!ref.current) {
49
49
  return;
50
50
  }
51
+ const svgElement = select(ref.current);
52
+ svgElement.selectAll('*').remove();
53
+ const plotClassName = b('plot-x');
54
+ let plotContainer = null;
55
+ if (plotRef === null || plotRef === void 0 ? void 0 : plotRef.current) {
56
+ plotContainer = select(plotRef.current);
57
+ plotContainer.selectAll(`.${plotClassName}`).remove();
58
+ }
59
+ if (!axis.visible) {
60
+ return;
61
+ }
51
62
  let tickItems = [];
52
63
  if (axis.grid.enabled) {
53
64
  tickItems = new Array(split.plots.length || 1).fill(null).map((_, index) => {
@@ -59,6 +70,7 @@ export const AxisX = React.memo(function AxisX(props) {
59
70
  }
60
71
  const axisScale = scale;
61
72
  const xAxisGenerator = axisBottom({
73
+ leftmostLimit,
62
74
  scale: axisScale,
63
75
  ticks: {
64
76
  items: tickItems,
@@ -77,8 +89,6 @@ export const AxisX = React.memo(function AxisX(props) {
77
89
  color: axis.lineColor,
78
90
  },
79
91
  });
80
- const svgElement = select(ref.current);
81
- svgElement.selectAll('*').remove();
82
92
  svgElement.call(xAxisGenerator).attr('class', b());
83
93
  // add an axis header if necessary
84
94
  if (axis.title.text) {
@@ -104,16 +114,51 @@ export const AxisX = React.memo(function AxisX(props) {
104
114
  }
105
115
  });
106
116
  }
117
+ // add plot bands
118
+ if (plotContainer && axis.plotBands.length > 0) {
119
+ const plotBandClassName = b('plot-x-band');
120
+ const plotBandsSelection = plotContainer
121
+ .selectAll(`.${plotBandClassName}-x`)
122
+ .data(axis.plotBands)
123
+ .join('g')
124
+ .attr('class', `${plotClassName} ${plotBandClassName}-x`);
125
+ plotBandsSelection
126
+ .append('rect')
127
+ .attr('x', (band) => {
128
+ var _a, _b;
129
+ const { from, to } = getBandsPosition({ band, axisScale, axis: 'x' });
130
+ const halfBandwidth = ((_b = (_a = axisScale.bandwidth) === null || _a === void 0 ? void 0 : _a.call(axisScale)) !== null && _b !== void 0 ? _b : 0) / 2;
131
+ const startPos = halfBandwidth + Math.min(from, to);
132
+ return Math.max(0, startPos);
133
+ })
134
+ .attr('width', (band) => {
135
+ const { from, to } = getBandsPosition({ band, axisScale, axis: 'x' });
136
+ const startPos = width - Math.min(from, to);
137
+ const endPos = Math.min(Math.abs(to - from), startPos);
138
+ return Math.min(endPos, width);
139
+ })
140
+ .attr('y', 0)
141
+ .attr('height', totalHeight)
142
+ .attr('fill', (band) => band.color)
143
+ .attr('opacity', (band) => band.opacity);
144
+ plotBandsSelection.each((plotBandData, i, nodes) => {
145
+ const plotLineSelection = select(nodes[i]);
146
+ if (plotBandData.layerPlacement === 'before') {
147
+ plotLineSelection.lower();
148
+ }
149
+ else {
150
+ plotLineSelection.raise();
151
+ }
152
+ });
153
+ }
107
154
  // add plot lines
108
- if (plotRef && axis.plotLines.length > 0) {
109
- const plotLineClassName = b('plotLine');
110
- const plotLineContainer = select(plotRef.current);
111
- plotLineContainer.selectAll(`.${plotLineClassName}-x`).remove();
112
- const plotLinesSelection = plotLineContainer
113
- .selectAll(`.${plotLineClassName}-x`)
155
+ if (plotContainer && axis.plotLines.length > 0) {
156
+ const plotLineClassName = b('plot-x-line');
157
+ const plotLinesSelection = plotContainer
158
+ .selectAll(`.${plotLineClassName}`)
114
159
  .data(axis.plotLines)
115
160
  .join('g')
116
- .attr('class', `${plotLineClassName}-x`);
161
+ .attr('class', `${plotClassName} ${plotLineClassName}`);
117
162
  const lineGenerator = line();
118
163
  plotLinesSelection
119
164
  .append('path')
@@ -140,6 +185,6 @@ export const AxisX = React.memo(function AxisX(props) {
140
185
  }
141
186
  });
142
187
  }
143
- }, [axis, width, totalHeight, scale, split, plotRef]);
188
+ }, [axis, width, totalHeight, scale, split, plotRef, leftmostLimit]);
144
189
  return React.createElement("g", { ref: ref });
145
190
  });
@@ -8,6 +8,7 @@ type Props = {
8
8
  height: number;
9
9
  split: PreparedSplit;
10
10
  plotRef?: React.MutableRefObject<SVGGElement | null>;
11
+ bottomLimit?: number;
11
12
  };
12
13
  export declare const AxisY: (props: Props) => React.JSX.Element;
13
14
  export {};
@@ -1,11 +1,14 @@
1
1
  import React from 'react';
2
2
  import { axisLeft, axisRight, line, select } from 'd3';
3
- import { block, calculateCos, calculateSin, formatAxisTickLabel, getAxisHeight, getAxisTitleRows, getClosestPointsRange, getLineDashArray, getScaleTicks, getTicksCount, handleOverflowingText, parseTransformStyle, setEllipsisForOverflowTexts, wrapText, } from '../../utils';
3
+ import { block, calculateCos, calculateSin, formatAxisTickLabel, getAxisHeight, getAxisTitleRows, getBandsPosition, getClosestPointsRange, getLineDashArray, getScaleTicks, getTicksCount, handleOverflowingText, parseTransformStyle, setEllipsisForOverflowTexts, wrapText, } from '../../utils';
4
4
  import './styles.css';
5
5
  const b = block('axis');
6
6
  function transformLabel(args) {
7
- const { node, axis } = args;
7
+ const { node, axis, isTopOffsetOverload = false } = args;
8
8
  let topOffset = axis.labels.lineHeight / 2;
9
+ if (isTopOffsetOverload) {
10
+ topOffset = 0;
11
+ }
9
12
  let leftOffset = axis.labels.margin;
10
13
  if (axis.position === 'left') {
11
14
  leftOffset = leftOffset * -1;
@@ -82,7 +85,7 @@ function getTitlePosition(args) {
82
85
  return { x, y };
83
86
  }
84
87
  export const AxisY = (props) => {
85
- const { axes, width, height: totalHeight, scale, split, plotRef } = props;
88
+ const { axes: allAxes, width, height: totalHeight, scale, split, plotRef, bottomLimit = 0, } = props;
86
89
  const height = getAxisHeight({ split, boundsHeight: totalHeight });
87
90
  const ref = React.useRef(null);
88
91
  const lineGenerator = line();
@@ -90,8 +93,15 @@ export const AxisY = (props) => {
90
93
  if (!ref.current) {
91
94
  return;
92
95
  }
96
+ const axes = allAxes.filter((a) => a.visible);
93
97
  const svgElement = select(ref.current);
94
98
  svgElement.selectAll('*').remove();
99
+ let plotContainer = null;
100
+ const plotClassName = b('plot-y');
101
+ if (plotRef === null || plotRef === void 0 ? void 0 : plotRef.current) {
102
+ plotContainer = select(plotRef.current);
103
+ plotContainer.selectAll(`.${plotClassName}`).remove();
104
+ }
95
105
  const getAxisPosition = (axis) => {
96
106
  var _a;
97
107
  const top = ((_a = split.plots[axis.plotIndex]) === null || _a === void 0 ? void 0 : _a.top) || 0;
@@ -108,6 +118,12 @@ export const AxisY = (props) => {
108
118
  }
109
119
  return acc;
110
120
  }, []);
121
+ const plotBands = axes.reduce((acc, axis) => {
122
+ if (axis.plotBands.length) {
123
+ acc.push(...axis.plotBands.map((plotBand) => (Object.assign(Object.assign({}, plotBand), { transform: getAxisPosition(axis) }))));
124
+ }
125
+ return acc;
126
+ }, []);
111
127
  const axisSelection = svgElement
112
128
  .selectAll('axis')
113
129
  .data(axes)
@@ -127,8 +143,8 @@ export const AxisY = (props) => {
127
143
  });
128
144
  yAxisGenerator(axisItem);
129
145
  if (d.labels.enabled) {
130
- const tickTexts = axisItem
131
- .selectAll('.tick text')
146
+ const labels = axisItem.selectAll('.tick text');
147
+ const tickTexts = labels
132
148
  // The offset must be applied before the labels are rotated.
133
149
  // Therefore, we reset the values and make an offset in transform attribute.
134
150
  // FIXME: give up axisLeft(d3) and switch to our own generation method
@@ -138,6 +154,26 @@ export const AxisY = (props) => {
138
154
  .style('transform', function () {
139
155
  return transformLabel({ node: this, axis: d });
140
156
  });
157
+ labels.each(function (_d, i) {
158
+ if (i === 0) {
159
+ const currentElement = this;
160
+ const currentElementPosition = currentElement.getBoundingClientRect();
161
+ const text = select(currentElement);
162
+ if (currentElementPosition.bottom > bottomLimit) {
163
+ const transform = transformLabel({
164
+ node: this,
165
+ axis: d,
166
+ isTopOffsetOverload: true,
167
+ });
168
+ text.style('transform', transform);
169
+ if (d.labels.rotation) {
170
+ text.attr('text-anchor', () => {
171
+ return d.labels.rotation < 0 ? 'start' : 'end';
172
+ });
173
+ }
174
+ }
175
+ }
176
+ });
141
177
  const textMaxWidth = !d.labels.rotation || Math.abs(d.labels.rotation) % 360 !== 90
142
178
  ? d.labels.maxWidth
143
179
  : (height - d.labels.padding * (tickTexts.size() - 1)) / tickTexts.size();
@@ -160,15 +196,50 @@ export const AxisY = (props) => {
160
196
  })
161
197
  .remove();
162
198
  }
163
- if (plotRef && d.plotLines.length > 0) {
164
- const plotLineClassName = b('plotLine');
165
- const plotLineContainer = select(plotRef.current);
166
- plotLineContainer.selectAll(`.${plotLineClassName}`).remove();
167
- const plotLinesSelection = plotLineContainer
199
+ if (plotContainer && d.plotBands.length > 0) {
200
+ const plotBandClassName = b('plot-y-band');
201
+ const plotBandsSelection = plotContainer
202
+ .selectAll(`.${plotBandClassName}`)
203
+ .data(plotBands)
204
+ .join('g')
205
+ .attr('class', `${plotClassName} ${plotBandClassName}`)
206
+ .style('transform', (plotBand) => plotBand.transform);
207
+ plotBandsSelection
208
+ .append('rect')
209
+ .attr('x', 0)
210
+ .attr('width', width)
211
+ .attr('y', (band) => {
212
+ var _a, _b;
213
+ const { from, to } = getBandsPosition({ band, axisScale, axis: 'y' });
214
+ const halfBandwidth = ((_b = (_a = axisScale.bandwidth) === null || _a === void 0 ? void 0 : _a.call(axisScale)) !== null && _b !== void 0 ? _b : 0) / 2;
215
+ const startPos = halfBandwidth + Math.min(from, to);
216
+ return Math.max(0, startPos);
217
+ })
218
+ .attr('height', (band) => {
219
+ const { from, to } = getBandsPosition({ band, axisScale, axis: 'y' });
220
+ const startPos = height - Math.min(from, to);
221
+ const endPos = Math.min(Math.abs(to - from), startPos);
222
+ return Math.min(endPos, height);
223
+ })
224
+ .attr('fill', (band) => band.color)
225
+ .attr('opacity', (band) => band.opacity);
226
+ plotBandsSelection.each((plotBandData, i, nodes) => {
227
+ const plotLineSelection = select(nodes[i]);
228
+ if (plotBandData.layerPlacement === 'before') {
229
+ plotLineSelection.lower();
230
+ }
231
+ else {
232
+ plotLineSelection.raise();
233
+ }
234
+ });
235
+ }
236
+ if (plotContainer && d.plotLines.length > 0) {
237
+ const plotLineClassName = b('plot-y-line');
238
+ const plotLinesSelection = plotContainer
168
239
  .selectAll(`.${plotLineClassName}`)
169
240
  .data(plotLines)
170
241
  .join('g')
171
- .attr('class', plotLineClassName)
242
+ .attr('class', `${plotClassName} ${plotLineClassName}`)
172
243
  .style('transform', (plotLine) => plotLine.transform);
173
244
  plotLinesSelection
174
245
  .append('path')
@@ -240,6 +311,6 @@ export const AxisY = (props) => {
240
311
  handleOverflowingText(nodes[index], height);
241
312
  }
242
313
  });
243
- }, [axes, width, height, scale, split]);
314
+ }, [allAxes, width, height, scale, split, bottomLimit]);
244
315
  return React.createElement("g", { ref: ref, className: b('container') });
245
316
  };
@@ -17,7 +17,7 @@ export const ChartInner = (props) => {
17
17
  const htmlLayerRef = React.useRef(null);
18
18
  const plotRef = React.useRef(null);
19
19
  const dispatcher = React.useMemo(() => getDispatcher(), []);
20
- const { boundsHeight, boundsOffsetLeft, boundsOffsetTop, boundsWidth, handleLegendItemClick, legendConfig, legendItems, preparedSeries, preparedSplit, preparedLegend, prevHeight, prevWidth, shapes, shapesData, title, tooltip, xAxis, xScale, yAxis, yScale, } = useChartInnerProps(Object.assign(Object.assign({}, props), { dispatcher, htmlLayout: htmlLayerRef.current }));
20
+ const { boundsHeight, boundsOffsetLeft, boundsOffsetTop, boundsWidth, handleLegendItemClick, legendConfig, legendItems, preparedSeries, preparedSplit, preparedLegend, prevHeight, prevWidth, shapes, shapesData, title, tooltip, xAxis, xScale, yAxis, yScale, svgXPos, svgBottomPos, } = useChartInnerProps(Object.assign(Object.assign({}, props), { dispatcher, htmlLayout: htmlLayerRef.current, svgContainer: svgRef.current }));
21
21
  const { tooltipPinned, togglePinTooltip, unpinTooltip } = useChartInnerState({
22
22
  dispatcher,
23
23
  tooltip,
@@ -67,9 +67,9 @@ export const ChartInner = (props) => {
67
67
  })),
68
68
  React.createElement("g", { width: boundsWidth, height: boundsHeight, transform: `translate(${[boundsOffsetLeft, boundsOffsetTop].join(',')})`, ref: plotRef },
69
69
  xScale && (yScale === null || yScale === void 0 ? void 0 : yScale.length) && (React.createElement(React.Fragment, null,
70
- React.createElement(AxisY, { axes: yAxis, width: boundsWidth, height: boundsHeight, scale: yScale, split: preparedSplit, plotRef: plotRef }),
70
+ React.createElement(AxisY, { bottomLimit: svgBottomPos, axes: yAxis, width: boundsWidth, height: boundsHeight, scale: yScale, split: preparedSplit, plotRef: plotRef }),
71
71
  React.createElement("g", { transform: `translate(0, ${boundsHeight})` },
72
- React.createElement(AxisX, { axis: xAxis, width: boundsWidth, height: boundsHeight, scale: xScale, split: preparedSplit, plotRef: plotRef })))),
72
+ React.createElement(AxisX, { leftmostLimit: svgXPos, axis: xAxis, width: boundsWidth, height: boundsHeight, scale: xScale, split: preparedSplit, plotRef: plotRef })))),
73
73
  shapes),
74
74
  preparedLegend.enabled && (React.createElement(Legend, { chartSeries: preparedSeries, boundsWidth: boundsWidth, legend: preparedLegend, items: legendItems, config: legendConfig, onItemClick: handleLegendItemClick, onUpdate: unpinTooltip }))),
75
75
  React.createElement("div", { className: b('html-layer'), ref: htmlLayerRef, style: {
@@ -1,6 +1,8 @@
1
1
  .gcharts-chart {
2
2
  --gcharts-data-labels: var(--g-color-text-secondary);
3
3
  position: absolute;
4
+ inset-block-start: 0;
5
+ inset-inline-start: 0;
4
6
  }
5
7
  .gcharts-chart__html-layer {
6
8
  display: contents;
@@ -4,8 +4,11 @@ import type { ChartInnerProps } from './types';
4
4
  type Props = ChartInnerProps & {
5
5
  dispatcher: Dispatch<object>;
6
6
  htmlLayout: HTMLElement | null;
7
+ svgContainer: SVGGElement | null;
7
8
  };
8
9
  export declare function useChartInnerProps(props: Props): {
10
+ svgBottomPos: number | undefined;
11
+ svgXPos: number | undefined;
9
12
  boundsHeight: number;
10
13
  boundsOffsetLeft: number;
11
14
  boundsOffsetTop: number;
@@ -4,7 +4,8 @@ import { getYAxisWidth } from '../../hooks/useChartDimensions/utils';
4
4
  import { getPreparedXAxis } from '../../hooks/useChartOptions/x-axis';
5
5
  import { getPreparedYAxis } from '../../hooks/useChartOptions/y-axis';
6
6
  export function useChartInnerProps(props) {
7
- const { width, height, data, dispatcher, htmlLayout } = props;
7
+ var _a;
8
+ const { width, height, data, dispatcher, htmlLayout, svgContainer } = props;
8
9
  const prevWidth = usePrevious(width);
9
10
  const prevHeight = usePrevious(height);
10
11
  const { chart, title, tooltip } = useChartOptions({ data });
@@ -54,9 +55,19 @@ export function useChartInnerProps(props) {
54
55
  htmlLayout,
55
56
  });
56
57
  const boundsOffsetTop = chart.margin.top;
57
- // We only need to consider the width of the first left axis
58
- const boundsOffsetLeft = chart.margin.left + getYAxisWidth(yAxis[0]);
58
+ // We need to calculate the width of each axis because the first axis can be hidden
59
+ const boundsOffsetLeft = chart.margin.left +
60
+ yAxis.reduce((acc, axis) => {
61
+ const axisWidth = getYAxisWidth(axis);
62
+ if (acc < axisWidth) {
63
+ acc = axisWidth;
64
+ }
65
+ return acc;
66
+ }, 0);
67
+ const { x, bottom } = (_a = svgContainer === null || svgContainer === void 0 ? void 0 : svgContainer.getBoundingClientRect()) !== null && _a !== void 0 ? _a : {};
59
68
  return {
69
+ svgBottomPos: bottom,
70
+ svgXPos: x,
60
71
  boundsHeight,
61
72
  boundsOffsetLeft,
62
73
  boundsOffsetTop,
@@ -185,7 +185,7 @@ export const Legend = (props) => {
185
185
  const mods = { selected: d.visible, unselected: !d.visible };
186
186
  return b('item-text', mods);
187
187
  })
188
- .text(function (d) {
188
+ .html(function (d) {
189
189
  return ('name' in d && d.name);
190
190
  })
191
191
  .style('font-size', legend.itemStyle.fontSize);
@@ -283,7 +283,7 @@ export const Legend = (props) => {
283
283
  .attr('font-size', (_d = legend.title.style.fontSize) !== null && _d !== void 0 ? _d : null)
284
284
  .attr('fill', (_e = legend.title.style.fontColor) !== null && _e !== void 0 ? _e : null)
285
285
  .style('alignment-baseline', 'before-edge')
286
- .text(legend.title.text);
286
+ .html(legend.title.text);
287
287
  }
288
288
  const { left } = getLegendPosition({
289
289
  align: legend.align,
@@ -5,5 +5,5 @@ const b = block('title');
5
5
  export const Title = (props) => {
6
6
  const { chartWidth, text, height, style } = props;
7
7
  return (React.createElement("text", { className: b(), dx: chartWidth / 2, dy: height / 2, dominantBaseline: "middle", textAnchor: "middle", style: Object.assign({ lineHeight: `${height}px` }, style) },
8
- React.createElement("tspan", null, text)));
8
+ React.createElement("tspan", { dangerouslySetInnerHTML: { __html: text } })));
9
9
  };
@@ -1,7 +1,5 @@
1
1
  import React from 'react';
2
- import { dateTime } from '@gravity-ui/date-utils';
3
2
  import get from 'lodash/get';
4
- import { formatNumber } from '../../libs';
5
3
  import { block, getDataCategoryValue, getWaterfallPointSubtotal } from '../../utils';
6
4
  import { getFormattedValue } from '../../utils/chart/format';
7
5
  const b = block('tooltip');
@@ -12,23 +10,14 @@ const getRowData = (fieldName, data, axis) => {
12
10
  const categories = get(axis, 'categories', []);
13
11
  return getDataCategoryValue({ axisDirection: fieldName, categories, data });
14
12
  }
15
- case 'datetime': {
16
- const value = get(data, fieldName);
17
- if (!value) {
18
- return undefined;
19
- }
20
- return dateTime({ input: value }).format(DEFAULT_DATE_FORMAT);
21
- }
22
- case 'linear':
23
13
  default: {
24
- const value = get(data, fieldName);
25
- return formatNumber(value);
14
+ return get(data, fieldName);
26
15
  }
27
16
  }
28
17
  };
29
18
  const getXRowData = (data, xAxis) => getRowData('x', data, xAxis);
30
19
  const getYRowData = (data, yAxis) => getRowData('y', data, yAxis);
31
- const getMeasureValue = (data, xAxis, yAxis) => {
20
+ const getMeasureValue = ({ data, xAxis, yAxis, valueFormat, }) => {
32
21
  var _a, _b, _c, _d;
33
22
  if (data.every((item) => ['pie', 'treemap', 'waterfall', 'sankey'].includes(item.series.type))) {
34
23
  return null;
@@ -37,12 +26,38 @@ const getMeasureValue = (data, xAxis, yAxis) => {
37
26
  return (_b = (_a = data[0].category) === null || _a === void 0 ? void 0 : _a.key) !== null && _b !== void 0 ? _b : null;
38
27
  }
39
28
  if (data.some((item) => item.series.type === 'bar-y')) {
40
- return getYRowData((_c = data[0]) === null || _c === void 0 ? void 0 : _c.data, yAxis);
29
+ const format = valueFormat !== null && valueFormat !== void 0 ? valueFormat : getDefaultValueFormat({ axis: yAxis });
30
+ return getFormattedValue({
31
+ value: getYRowData((_c = data[0]) === null || _c === void 0 ? void 0 : _c.data, yAxis),
32
+ format,
33
+ });
41
34
  }
42
- return getXRowData((_d = data[0]) === null || _d === void 0 ? void 0 : _d.data, xAxis);
35
+ const format = valueFormat !== null && valueFormat !== void 0 ? valueFormat : getDefaultValueFormat({ axis: xAxis });
36
+ return getFormattedValue({
37
+ value: getXRowData((_d = data[0]) === null || _d === void 0 ? void 0 : _d.data, xAxis),
38
+ format,
39
+ });
43
40
  };
41
+ function getDefaultValueFormat({ axis }) {
42
+ switch (axis === null || axis === void 0 ? void 0 : axis.type) {
43
+ case 'linear':
44
+ case 'logarithmic': {
45
+ return {
46
+ type: 'number',
47
+ };
48
+ }
49
+ case 'datetime': {
50
+ return {
51
+ type: 'date',
52
+ format: DEFAULT_DATE_FORMAT,
53
+ };
54
+ }
55
+ default:
56
+ return undefined;
57
+ }
58
+ }
44
59
  export const DefaultContent = ({ hovered, xAxis, yAxis, valueFormat }) => {
45
- const measureValue = getMeasureValue(hovered, xAxis, yAxis);
60
+ const measureValue = getMeasureValue({ data: hovered, xAxis, yAxis });
46
61
  return (React.createElement(React.Fragment, null,
47
62
  measureValue && React.createElement("div", null, measureValue),
48
63
  hovered.map((seriesItem, i) => {
@@ -55,9 +70,10 @@ export const DefaultContent = ({ hovered, xAxis, yAxis, valueFormat }) => {
55
70
  case 'line':
56
71
  case 'area':
57
72
  case 'bar-x': {
73
+ const format = valueFormat !== null && valueFormat !== void 0 ? valueFormat : getDefaultValueFormat({ axis: yAxis });
58
74
  const formattedValue = getFormattedValue({
59
75
  value: getYRowData(data, yAxis),
60
- format: valueFormat,
76
+ format,
61
77
  });
62
78
  const value = (React.createElement(React.Fragment, null,
63
79
  series.name,
@@ -70,13 +86,14 @@ export const DefaultContent = ({ hovered, xAxis, yAxis, valueFormat }) => {
70
86
  case 'waterfall': {
71
87
  const isTotal = get(data, 'total', false);
72
88
  const subTotalValue = getWaterfallPointSubtotal(data, series);
89
+ const format = valueFormat !== null && valueFormat !== void 0 ? valueFormat : getDefaultValueFormat({ axis: yAxis });
73
90
  const subTotal = getFormattedValue({
74
91
  value: subTotalValue,
75
- format: valueFormat,
92
+ format,
76
93
  });
77
94
  const formattedValue = getFormattedValue({
78
95
  value: getYRowData(data, yAxis),
79
- format: valueFormat,
96
+ format,
80
97
  });
81
98
  return (React.createElement("div", { key: `${id}_${get(data, 'x')}` },
82
99
  !isTotal && (React.createElement(React.Fragment, null,
@@ -93,9 +110,10 @@ export const DefaultContent = ({ hovered, xAxis, yAxis, valueFormat }) => {
93
110
  subTotal)));
94
111
  }
95
112
  case 'bar-y': {
113
+ const format = valueFormat !== null && valueFormat !== void 0 ? valueFormat : getDefaultValueFormat({ axis: xAxis });
96
114
  const formattedValue = getFormattedValue({
97
115
  value: getXRowData(data, xAxis),
98
- format: valueFormat,
116
+ format,
99
117
  });
100
118
  const value = (React.createElement(React.Fragment, null,
101
119
  series.name,
@@ -110,7 +128,7 @@ export const DefaultContent = ({ hovered, xAxis, yAxis, valueFormat }) => {
110
128
  const seriesData = data;
111
129
  const formattedValue = getFormattedValue({
112
130
  value: seriesData.value,
113
- format: valueFormat,
131
+ format: valueFormat !== null && valueFormat !== void 0 ? valueFormat : { type: 'number' },
114
132
  });
115
133
  return (React.createElement("div", { key: id, className: b('content-row') },
116
134
  React.createElement("div", { className: b('color'), style: { backgroundColor: color } }),
@@ -124,7 +142,7 @@ export const DefaultContent = ({ hovered, xAxis, yAxis, valueFormat }) => {
124
142
  const value = (_a = source.links.find((d) => d.name === (target === null || target === void 0 ? void 0 : target.name))) === null || _a === void 0 ? void 0 : _a.value;
125
143
  const formattedValue = getFormattedValue({
126
144
  value,
127
- format: valueFormat,
145
+ format: valueFormat !== null && valueFormat !== void 0 ? valueFormat : { type: 'number' },
128
146
  });
129
147
  return (React.createElement("div", { key: id, className: b('content-row') },
130
148
  React.createElement("div", { className: b('color'), style: { backgroundColor: source.color } }),
@@ -142,7 +160,7 @@ export const DefaultContent = ({ hovered, xAxis, yAxis, valueFormat }) => {
142
160
  const seriesData = data;
143
161
  const formattedValue = getFormattedValue({
144
162
  value: seriesData.value,
145
- format: valueFormat,
163
+ format: valueFormat !== null && valueFormat !== void 0 ? valueFormat : { type: 'number' },
146
164
  });
147
165
  const value = (React.createElement(React.Fragment, null,
148
166
  React.createElement("span", null,
@@ -8,6 +8,9 @@ const getBottomOffset = (args) => {
8
8
  if (preparedLegend.enabled) {
9
9
  result += preparedLegend.height + preparedLegend.margin;
10
10
  }
11
+ if (!preparedXAxis.visible) {
12
+ return result;
13
+ }
11
14
  if (hasAxisRelatedSeries) {
12
15
  if (preparedXAxis.title.text) {
13
16
  result += preparedXAxis.title.height + preparedXAxis.title.margin;
@@ -6,6 +6,9 @@ export const getBoundsWidth = (args) => {
6
6
  getWidthOccupiedByYAxis({ preparedAxis: preparedYAxis }));
7
7
  };
8
8
  export function getYAxisWidth(axis) {
9
+ if (!(axis === null || axis === void 0 ? void 0 : axis.visible)) {
10
+ return 0;
11
+ }
9
12
  let result = 0;
10
13
  if (axis === null || axis === void 0 ? void 0 : axis.title.text) {
11
14
  result += axis.title.height + axis.title.margin;
@@ -1,5 +1,5 @@
1
1
  import type { DashStyle } from 'src/constants';
2
- import type { AxisPlotLine, BaseTextStyle, ChartAxis, ChartAxisLabels, ChartAxisTitleAlignment, ChartAxisType, ChartData, ChartMargin } from '../../types';
2
+ import type { AxisPlotBand, BaseTextStyle, ChartAxis, ChartAxisLabels, ChartAxisTitleAlignment, ChartAxisType, ChartData, ChartMargin, PlotLayerPlacement } from '../../types';
3
3
  type PreparedAxisLabels = Omit<ChartAxisLabels, 'enabled' | 'padding' | 'style' | 'autoRotation'> & Required<Pick<ChartAxisLabels, 'enabled' | 'padding' | 'margin' | 'rotation'>> & {
4
4
  style: BaseTextStyle;
5
5
  rotation: number;
@@ -11,15 +11,16 @@ type PreparedAxisLabels = Omit<ChartAxisLabels, 'enabled' | 'padding' | 'style'
11
11
  export type PreparedChart = {
12
12
  margin: ChartMargin;
13
13
  };
14
+ export type PreparedAxisPlotBand = Required<AxisPlotBand>;
14
15
  export type PreparedAxisPlotLine = {
15
16
  value: number;
16
17
  color: string;
17
18
  width: number;
18
19
  dashStyle: DashStyle;
19
20
  opacity: number;
20
- layerPlacement: AxisPlotLine['layerPlacement'];
21
+ layerPlacement: PlotLayerPlacement;
21
22
  };
22
- export type PreparedAxis = Omit<ChartAxis, 'type' | 'labels' | 'plotLines'> & {
23
+ export type PreparedAxis = Omit<ChartAxis, 'type' | 'labels' | 'plotLines' | 'plotBands'> & {
23
24
  type: ChartAxisType;
24
25
  labels: PreparedAxisLabels;
25
26
  title: {
@@ -42,6 +43,7 @@ export type PreparedAxis = Omit<ChartAxis, 'type' | 'labels' | 'plotLines'> & {
42
43
  position: 'left' | 'right' | 'top' | 'bottom';
43
44
  plotIndex: number;
44
45
  plotLines: PreparedAxisPlotLine[];
46
+ plotBands: PreparedAxisPlotBand[];
45
47
  };
46
48
  export type PreparedTitle = ChartData['title'] & {
47
49
  height: number;
@@ -110,6 +110,14 @@ export const getPreparedXAxis = ({ xAxis, series, width, }) => {
110
110
  opacity: get(d, 'opacity', 1),
111
111
  layerPlacement: get(d, 'layerPlacement', 'before'),
112
112
  })),
113
+ plotBands: get(xAxis, 'plotBands', []).map((d) => ({
114
+ color: get(d, 'color', 'var(--g-color-base-brand)'),
115
+ opacity: get(d, 'opacity', 1),
116
+ from: get(d, 'from', 0),
117
+ to: get(d, 'to', 0),
118
+ layerPlacement: get(d, 'layerPlacement', 'before'),
119
+ })),
120
+ visible: get(xAxis, 'visible', true),
113
121
  };
114
122
  const { height, rotation } = getLabelSettings({
115
123
  axis: preparedXAxis,
@@ -116,6 +116,14 @@ export const getPreparedYAxis = ({ series, yAxis, height, }) => {
116
116
  opacity: get(d, 'opacity', 1),
117
117
  layerPlacement: get(d, 'layerPlacement', 'before'),
118
118
  })),
119
+ plotBands: get(axisItem, 'plotBands', []).map((d) => ({
120
+ color: get(d, 'color', 'var(--g-color-base-brand)'),
121
+ opacity: get(d, 'opacity', 1),
122
+ from: get(d, 'from', 0),
123
+ to: get(d, 'to', 0),
124
+ layerPlacement: get(d, 'layerPlacement', 'before'),
125
+ })),
126
+ visible: get(axisItem, 'visible', true),
119
127
  };
120
128
  if (labelsEnabled) {
121
129
  preparedAxis.labels.width = getAxisLabelMaxWidth({ axis: preparedAxis, series });