@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
@@ -95,6 +95,148 @@ const validateXYSeries = (args) => {
95
95
  }
96
96
  });
97
97
  };
98
+ const validateAxisPlotValues = (args) => {
99
+ const { series, xAxis, yAxis = [] } = args;
100
+ const yAxisIndex = get(series, 'yAxis', 0);
101
+ const seriesYAxis = yAxis[yAxisIndex];
102
+ if (yAxisIndex !== 0 && typeof seriesYAxis === 'undefined') {
103
+ throw new ChartError({
104
+ code: CHART_ERROR_CODE.INVALID_DATA,
105
+ message: i18n('error', 'label_invalid-y-axis-index', {
106
+ index: yAxisIndex,
107
+ }),
108
+ });
109
+ }
110
+ const xPlotBands = get(xAxis, 'plotBands', []);
111
+ const yPlotBands = get(yAxis, 'plotBands', []);
112
+ if (!xPlotBands.length && !yPlotBands.length) {
113
+ return;
114
+ }
115
+ const xType = get(xAxis, 'type', DEFAULT_AXIS_TYPE);
116
+ const yType = get(seriesYAxis, 'type', DEFAULT_AXIS_TYPE);
117
+ xPlotBands.forEach(({ from = 0, to = 0 }) => {
118
+ const fromNotEqualTo = typeof to !== typeof from;
119
+ if (fromNotEqualTo) {
120
+ throw new ChartError({
121
+ code: CHART_ERROR_CODE.INVALID_OPTION_TYPE,
122
+ message: i18n('error', 'label_axis-plot-band-options-not-equal', {
123
+ axis: 'x',
124
+ option: 'from',
125
+ }),
126
+ });
127
+ }
128
+ switch (xType) {
129
+ case 'category': {
130
+ const invalidFrom = typeof from !== 'string' && typeof from !== 'number';
131
+ const invalidTo = typeof to !== 'string' && typeof to !== 'number';
132
+ if (invalidFrom) {
133
+ throw new ChartError({
134
+ code: CHART_ERROR_CODE.INVALID_OPTION_TYPE,
135
+ message: i18n('error', 'label_invalid-axis-plot-band-option', {
136
+ axis: 'x',
137
+ option: 'from',
138
+ }),
139
+ });
140
+ }
141
+ if (invalidTo) {
142
+ throw new ChartError({
143
+ code: CHART_ERROR_CODE.INVALID_OPTION_TYPE,
144
+ message: i18n('error', 'label_invalid-axis-plot-band-option', {
145
+ axis: 'x',
146
+ option: 'to',
147
+ }),
148
+ });
149
+ }
150
+ break;
151
+ }
152
+ case 'linear':
153
+ case 'datetime': {
154
+ const invalidFrom = typeof from !== 'number';
155
+ const invalidTo = typeof to !== 'number';
156
+ if (invalidFrom) {
157
+ throw new ChartError({
158
+ code: CHART_ERROR_CODE.INVALID_OPTION_TYPE,
159
+ message: i18n('error', 'label_invalid-axis-plot-band-option', {
160
+ axis: 'x',
161
+ option: 'from',
162
+ }),
163
+ });
164
+ }
165
+ if (invalidTo) {
166
+ throw new ChartError({
167
+ code: CHART_ERROR_CODE.INVALID_OPTION_TYPE,
168
+ message: i18n('error', 'label_invalid-axis-plot-band-option', {
169
+ axis: 'x',
170
+ option: 'to',
171
+ }),
172
+ });
173
+ }
174
+ break;
175
+ }
176
+ }
177
+ });
178
+ yPlotBands.forEach(({ from = 0, to = 0 }) => {
179
+ const fromNotEqualTo = typeof to !== typeof from;
180
+ if (fromNotEqualTo) {
181
+ throw new ChartError({
182
+ code: CHART_ERROR_CODE.INVALID_OPTION_TYPE,
183
+ message: i18n('error', 'label_axis-plot-band-options-not-equal', {
184
+ axis: 'x',
185
+ option: 'from',
186
+ }),
187
+ });
188
+ }
189
+ switch (yType) {
190
+ case 'category': {
191
+ const invalidFrom = typeof from !== 'string' && typeof from !== 'number';
192
+ const invalidTo = typeof to !== 'string' && typeof to !== 'number';
193
+ if (invalidFrom) {
194
+ throw new ChartError({
195
+ code: CHART_ERROR_CODE.INVALID_OPTION_TYPE,
196
+ message: i18n('error', 'label_invalid-axis-plot-band-option', {
197
+ axis: 'y',
198
+ option: 'from',
199
+ }),
200
+ });
201
+ }
202
+ if (invalidTo) {
203
+ throw new ChartError({
204
+ code: CHART_ERROR_CODE.INVALID_OPTION_TYPE,
205
+ message: i18n('error', 'label_invalid-axis-plot-band-option', {
206
+ axis: 'y',
207
+ option: 'to',
208
+ }),
209
+ });
210
+ }
211
+ break;
212
+ }
213
+ case 'linear':
214
+ case 'datetime': {
215
+ const invalidFrom = typeof from !== 'number';
216
+ const invalidTo = typeof to !== 'number';
217
+ if (invalidFrom) {
218
+ throw new ChartError({
219
+ code: CHART_ERROR_CODE.INVALID_OPTION_TYPE,
220
+ message: i18n('error', 'label_invalid-axis-plot-band-option', {
221
+ axis: 'y',
222
+ option: 'from',
223
+ }),
224
+ });
225
+ }
226
+ if (invalidTo) {
227
+ throw new ChartError({
228
+ code: CHART_ERROR_CODE.INVALID_OPTION_TYPE,
229
+ message: i18n('error', 'label_invalid-axis-plot-band-option', {
230
+ axis: 'y',
231
+ option: 'to',
232
+ }),
233
+ });
234
+ }
235
+ break;
236
+ }
237
+ }
238
+ });
239
+ };
98
240
  const validatePieSeries = ({ series }) => {
99
241
  series.data.forEach(({ value }) => {
100
242
  if (typeof value !== 'number') {
@@ -163,12 +305,14 @@ const validateSeries = (args) => {
163
305
  case 'area':
164
306
  case 'bar-y':
165
307
  case 'bar-x': {
308
+ validateAxisPlotValues({ series, xAxis, yAxis });
166
309
  validateXYSeries({ series, xAxis, yAxis });
167
310
  validateStacking({ series });
168
311
  break;
169
312
  }
170
313
  case 'line':
171
314
  case 'scatter': {
315
+ validateAxisPlotValues({ series, xAxis, yAxis });
172
316
  validateXYSeries({ series, xAxis, yAxis });
173
317
  break;
174
318
  }
@@ -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
  };