@gravity-ui/charts 1.48.1 → 1.48.3

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.
@@ -1,10 +1,12 @@
1
1
  import type { ChartScale, PreparedAxis, PreparedSeries, PreparedSplit } from '../../hooks';
2
2
  import type { AxisXData } from './types';
3
- export declare function prepareXAxisData({ axis, boundsOffsetLeft, boundsOffsetRight, boundsWidth, height, scale, series, split, yAxis, }: {
3
+ export declare function prepareXAxisData({ axis, boundsOffsetLeft, boundsOffsetRight, boundsWidth, chartMarginLeft, chartMarginRight, height, scale, series, split, yAxis, }: {
4
4
  axis: PreparedAxis;
5
5
  boundsOffsetLeft: number;
6
6
  boundsOffsetRight: number;
7
7
  boundsWidth: number;
8
+ chartMarginLeft: number;
9
+ chartMarginRight: number;
8
10
  height: number;
9
11
  scale: ChartScale;
10
12
  series: PreparedSeries[];
@@ -3,7 +3,7 @@ import { select } from 'd3-selection';
3
3
  import { calculateCos, calculateNumericProperty, calculateSin, formatAxisTickLabel, getBandsPosition, getLabelsSize, getMinSpaceBetween, getTextSizeFn, getTextWithElipsis, } from '../../core/utils';
4
4
  import { getXAxisTickValues } from '../../core/utils/axis/x-axis';
5
5
  import { getMultilineTitleContentRows } from '../utils/axis-title';
6
- async function getSvgAxisLabel({ getTextSize, text, axis, top, left, labelMaxWidth, axisWidth, boundsOffsetLeft, boundsOffsetRight, }) {
6
+ async function getSvgAxisLabel({ getTextSize, text, axis, top, left, labelMaxWidth, axisWidth, boundsOffsetLeft, boundsOffsetRight, chartMarginLeft, chartMarginRight, }) {
7
7
  var _a;
8
8
  const rotation = axis.labels.rotation;
9
9
  const content = [];
@@ -13,8 +13,10 @@ async function getSvgAxisLabel({ getTextSize, text, axis, top, left, labelMaxWid
13
13
  let textMaxWidth = Infinity;
14
14
  if (a === 0) {
15
15
  textMaxWidth = Math.min(labelMaxWidth,
16
- // rightmost label
17
- labelMaxWidth / 2 + axisWidth + boundsOffsetRight - left);
16
+ // rightmost label: may extend into the right Y-axis area but not into the right margin
17
+ labelMaxWidth / 2 + axisWidth + (boundsOffsetRight - chartMarginRight) - left,
18
+ // leftmost label: when clamped at the margin boundary, don't bleed into next-tick territory
19
+ left + (boundsOffsetLeft - chartMarginLeft) + labelMaxWidth / 2);
18
20
  }
19
21
  else if (rotation > 0) {
20
22
  textMaxWidth = Math.min(axis.labels.height / calculateSin(a) - textSize.height * calculateSin(90 - a),
@@ -23,8 +25,8 @@ async function getSvgAxisLabel({ getTextSize, text, axis, top, left, labelMaxWid
23
25
  }
24
26
  else {
25
27
  textMaxWidth = Math.min(axis.labels.height / calculateSin(a) - textSize.height * calculateSin(90 - a),
26
- // leftmostLabel
27
- (boundsOffsetLeft + left) / calculateSin(a));
28
+ // leftmost label: may extend into the left Y-axis area but not into the left margin
29
+ (boundsOffsetLeft - chartMarginLeft + left) / calculateSin(a));
28
30
  }
29
31
  if (textSize.width > textMaxWidth) {
30
32
  rowText = await getTextWithElipsis({
@@ -65,13 +67,13 @@ async function getSvgAxisLabel({ getTextSize, text, axis, top, left, labelMaxWid
65
67
  content,
66
68
  style: axis.labels.style,
67
69
  size: textSize,
68
- x: Math.max(-boundsOffsetLeft, x),
70
+ x: Math.max(-(boundsOffsetLeft - chartMarginLeft), x),
69
71
  y,
70
72
  angle: rotation,
71
73
  };
72
74
  return svgLabel;
73
75
  }
74
- export async function prepareXAxisData({ axis, boundsOffsetLeft, boundsOffsetRight, boundsWidth, height, scale, series, split, yAxis, }) {
76
+ export async function prepareXAxisData({ axis, boundsOffsetLeft, boundsOffsetRight, boundsWidth, chartMarginLeft, chartMarginRight, height, scale, series, split, yAxis, }) {
75
77
  var _a, _b, _c, _d, _e, _f, _g, _h, _j;
76
78
  const xAxisItems = [];
77
79
  const splitPlots = (_a = split === null || split === void 0 ? void 0 : split.plots) !== null && _a !== void 0 ? _a : [];
@@ -167,6 +169,8 @@ export async function prepareXAxisData({ axis, boundsOffsetLeft, boundsOffsetRig
167
169
  axisWidth,
168
170
  boundsOffsetLeft,
169
171
  boundsOffsetRight,
172
+ chartMarginLeft,
173
+ chartMarginRight,
170
174
  });
171
175
  }
172
176
  }
@@ -173,6 +173,7 @@ export const ChartInner = (props) => {
173
173
  const axisDataReady = !(preparedLegend === null || preparedLegend === void 0 ? void 0 : preparedLegend.enabled) || Boolean(legendConfig);
174
174
  const yAxisDataItems = useAsyncState([], setYAxisDataItems, axisDataReady);
175
175
  const setXAxisDataItems = React.useCallback(async () => {
176
+ var _a, _b;
176
177
  const items = [];
177
178
  const axis = xAxis;
178
179
  const scale = xScale;
@@ -182,6 +183,8 @@ export const ChartInner = (props) => {
182
183
  boundsOffsetLeft: boundsOffsetLeft,
183
184
  boundsOffsetRight: width - boundsWidth - boundsOffsetLeft,
184
185
  boundsWidth,
186
+ chartMarginLeft: (_a = preparedChart === null || preparedChart === void 0 ? void 0 : preparedChart.margin.left) !== null && _a !== void 0 ? _a : 0,
187
+ chartMarginRight: (_b = preparedChart === null || preparedChart === void 0 ? void 0 : preparedChart.margin.right) !== null && _b !== void 0 ? _b : 0,
185
188
  height: boundsHeight,
186
189
  scale,
187
190
  series: preparedSeries.filter((s) => s.visible),
@@ -204,6 +207,7 @@ export const ChartInner = (props) => {
204
207
  boundsHeight,
205
208
  boundsOffsetLeft,
206
209
  boundsWidth,
210
+ preparedChart,
207
211
  preparedSeries,
208
212
  preparedSplit,
209
213
  width,
@@ -18,10 +18,9 @@ export const Tooltip = (props) => {
18
18
  if (!svgContainer)
19
19
  return undefined;
20
20
  const updateRect = (e) => {
21
- // Skip synthetic events (e.g. the CustomEvent('scroll') dispatched by the
22
- // sibling useEffect below to reposition the Popup). Only native browser
23
- // scroll events have isTrusted === true, so those still update the rect.
24
- if (e && !e.isTrusted)
21
+ // Skip the synthetic CustomEvent('scroll') dispatched below to reposition the Popup.
22
+ // All other events including transitionend and animationend should update the rect.
23
+ if (e instanceof CustomEvent)
25
24
  return;
26
25
  containerRectRef.current = svgContainer.getBoundingClientRect();
27
26
  };
@@ -29,9 +28,13 @@ export const Tooltip = (props) => {
29
28
  const resizeObserver = new ResizeObserver(() => updateRect());
30
29
  resizeObserver.observe(svgContainer);
31
30
  window.addEventListener('scroll', updateRect, { passive: true, capture: true });
31
+ window.addEventListener('transitionend', updateRect, { capture: true });
32
+ window.addEventListener('animationend', updateRect, { capture: true });
32
33
  return () => {
33
34
  resizeObserver.disconnect();
34
35
  window.removeEventListener('scroll', updateRect, { capture: true });
36
+ window.removeEventListener('transitionend', updateRect, { capture: true });
37
+ window.removeEventListener('animationend', updateRect, { capture: true });
35
38
  };
36
39
  }, [svgContainer]);
37
40
  const containerRect = containerRectRef.current;
@@ -7,6 +7,7 @@ import { validateData } from '../core/validation';
7
7
  import { ChartInner } from './ChartInner';
8
8
  export * from './Tooltip/ChartTooltipContent';
9
9
  export const Chart = React.forwardRef(function Chart(props, forwardedRef) {
10
+ var _a, _b;
10
11
  const { data, lang, onResize, onReady } = props;
11
12
  const validatedData = React.useRef();
12
13
  const ref = React.useRef(null);
@@ -48,11 +49,15 @@ export const Chart = React.forwardRef(function Chart(props, forwardedRef) {
48
49
  // https://github.com/d3/d3-selection/blob/main/README.md#handling-events
49
50
  const eventName = `resize.${getUniqId()}`;
50
51
  selection.on(eventName, debuncedHandleResize);
52
+ window.addEventListener('transitionend', handleResize, { capture: true });
53
+ window.addEventListener('animationend', handleResize, { capture: true });
51
54
  return () => {
52
55
  // https://d3js.org/d3-selection/events#selection_on
53
56
  selection.on(eventName, null);
57
+ window.removeEventListener('transitionend', handleResize, { capture: true });
58
+ window.removeEventListener('animationend', handleResize, { capture: true });
54
59
  };
55
- }, [debuncedHandleResize]);
60
+ }, [debuncedHandleResize, handleResize]);
56
61
  React.useEffect(() => {
57
62
  if (typeof onResize === 'function') {
58
63
  onResize({ dimensions });
@@ -67,5 +72,5 @@ export const Chart = React.forwardRef(function Chart(props, forwardedRef) {
67
72
  width: (dimensions === null || dimensions === void 0 ? void 0 : dimensions.width) || '100%',
68
73
  height: (dimensions === null || dimensions === void 0 ? void 0 : dimensions.height) || '100%',
69
74
  position: 'relative',
70
- } }, (dimensions === null || dimensions === void 0 ? void 0 : dimensions.height) && (dimensions === null || dimensions === void 0 ? void 0 : dimensions.width) && (React.createElement(ChartInner, { height: dimensions === null || dimensions === void 0 ? void 0 : dimensions.height, width: dimensions === null || dimensions === void 0 ? void 0 : dimensions.width, data: data, onReady: onReady }))));
75
+ } }, Boolean((dimensions === null || dimensions === void 0 ? void 0 : dimensions.height) && (dimensions === null || dimensions === void 0 ? void 0 : dimensions.width)) && (React.createElement(ChartInner, { height: (_a = dimensions === null || dimensions === void 0 ? void 0 : dimensions.height) !== null && _a !== void 0 ? _a : 0, width: (_b = dimensions === null || dimensions === void 0 ? void 0 : dimensions.width) !== null && _b !== void 0 ? _b : 0, data: data, onReady: onReady }))));
71
76
  });
@@ -161,7 +161,7 @@ function getDomainMaxAlignedToEndTick(args) {
161
161
  else {
162
162
  step = tickStep(dMin, dMax, 1);
163
163
  }
164
- dNewMax = tickValues[tickValues.length - 1].value + step;
164
+ dNewMax = Math.floor(dMax / step + 1) * step;
165
165
  }
166
166
  }
167
167
  return dNewMax;
@@ -1,10 +1,12 @@
1
1
  import type { ChartScale, PreparedAxis, PreparedSeries, PreparedSplit } from '../../hooks';
2
2
  import type { AxisXData } from './types';
3
- export declare function prepareXAxisData({ axis, boundsOffsetLeft, boundsOffsetRight, boundsWidth, height, scale, series, split, yAxis, }: {
3
+ export declare function prepareXAxisData({ axis, boundsOffsetLeft, boundsOffsetRight, boundsWidth, chartMarginLeft, chartMarginRight, height, scale, series, split, yAxis, }: {
4
4
  axis: PreparedAxis;
5
5
  boundsOffsetLeft: number;
6
6
  boundsOffsetRight: number;
7
7
  boundsWidth: number;
8
+ chartMarginLeft: number;
9
+ chartMarginRight: number;
8
10
  height: number;
9
11
  scale: ChartScale;
10
12
  series: PreparedSeries[];
@@ -3,7 +3,7 @@ import { select } from 'd3-selection';
3
3
  import { calculateCos, calculateNumericProperty, calculateSin, formatAxisTickLabel, getBandsPosition, getLabelsSize, getMinSpaceBetween, getTextSizeFn, getTextWithElipsis, } from '../../core/utils';
4
4
  import { getXAxisTickValues } from '../../core/utils/axis/x-axis';
5
5
  import { getMultilineTitleContentRows } from '../utils/axis-title';
6
- async function getSvgAxisLabel({ getTextSize, text, axis, top, left, labelMaxWidth, axisWidth, boundsOffsetLeft, boundsOffsetRight, }) {
6
+ async function getSvgAxisLabel({ getTextSize, text, axis, top, left, labelMaxWidth, axisWidth, boundsOffsetLeft, boundsOffsetRight, chartMarginLeft, chartMarginRight, }) {
7
7
  var _a;
8
8
  const rotation = axis.labels.rotation;
9
9
  const content = [];
@@ -13,8 +13,10 @@ async function getSvgAxisLabel({ getTextSize, text, axis, top, left, labelMaxWid
13
13
  let textMaxWidth = Infinity;
14
14
  if (a === 0) {
15
15
  textMaxWidth = Math.min(labelMaxWidth,
16
- // rightmost label
17
- labelMaxWidth / 2 + axisWidth + boundsOffsetRight - left);
16
+ // rightmost label: may extend into the right Y-axis area but not into the right margin
17
+ labelMaxWidth / 2 + axisWidth + (boundsOffsetRight - chartMarginRight) - left,
18
+ // leftmost label: when clamped at the margin boundary, don't bleed into next-tick territory
19
+ left + (boundsOffsetLeft - chartMarginLeft) + labelMaxWidth / 2);
18
20
  }
19
21
  else if (rotation > 0) {
20
22
  textMaxWidth = Math.min(axis.labels.height / calculateSin(a) - textSize.height * calculateSin(90 - a),
@@ -23,8 +25,8 @@ async function getSvgAxisLabel({ getTextSize, text, axis, top, left, labelMaxWid
23
25
  }
24
26
  else {
25
27
  textMaxWidth = Math.min(axis.labels.height / calculateSin(a) - textSize.height * calculateSin(90 - a),
26
- // leftmostLabel
27
- (boundsOffsetLeft + left) / calculateSin(a));
28
+ // leftmost label: may extend into the left Y-axis area but not into the left margin
29
+ (boundsOffsetLeft - chartMarginLeft + left) / calculateSin(a));
28
30
  }
29
31
  if (textSize.width > textMaxWidth) {
30
32
  rowText = await getTextWithElipsis({
@@ -65,13 +67,13 @@ async function getSvgAxisLabel({ getTextSize, text, axis, top, left, labelMaxWid
65
67
  content,
66
68
  style: axis.labels.style,
67
69
  size: textSize,
68
- x: Math.max(-boundsOffsetLeft, x),
70
+ x: Math.max(-(boundsOffsetLeft - chartMarginLeft), x),
69
71
  y,
70
72
  angle: rotation,
71
73
  };
72
74
  return svgLabel;
73
75
  }
74
- export async function prepareXAxisData({ axis, boundsOffsetLeft, boundsOffsetRight, boundsWidth, height, scale, series, split, yAxis, }) {
76
+ export async function prepareXAxisData({ axis, boundsOffsetLeft, boundsOffsetRight, boundsWidth, chartMarginLeft, chartMarginRight, height, scale, series, split, yAxis, }) {
75
77
  var _a, _b, _c, _d, _e, _f, _g, _h, _j;
76
78
  const xAxisItems = [];
77
79
  const splitPlots = (_a = split === null || split === void 0 ? void 0 : split.plots) !== null && _a !== void 0 ? _a : [];
@@ -167,6 +169,8 @@ export async function prepareXAxisData({ axis, boundsOffsetLeft, boundsOffsetRig
167
169
  axisWidth,
168
170
  boundsOffsetLeft,
169
171
  boundsOffsetRight,
172
+ chartMarginLeft,
173
+ chartMarginRight,
170
174
  });
171
175
  }
172
176
  }
@@ -173,6 +173,7 @@ export const ChartInner = (props) => {
173
173
  const axisDataReady = !(preparedLegend === null || preparedLegend === void 0 ? void 0 : preparedLegend.enabled) || Boolean(legendConfig);
174
174
  const yAxisDataItems = useAsyncState([], setYAxisDataItems, axisDataReady);
175
175
  const setXAxisDataItems = React.useCallback(async () => {
176
+ var _a, _b;
176
177
  const items = [];
177
178
  const axis = xAxis;
178
179
  const scale = xScale;
@@ -182,6 +183,8 @@ export const ChartInner = (props) => {
182
183
  boundsOffsetLeft: boundsOffsetLeft,
183
184
  boundsOffsetRight: width - boundsWidth - boundsOffsetLeft,
184
185
  boundsWidth,
186
+ chartMarginLeft: (_a = preparedChart === null || preparedChart === void 0 ? void 0 : preparedChart.margin.left) !== null && _a !== void 0 ? _a : 0,
187
+ chartMarginRight: (_b = preparedChart === null || preparedChart === void 0 ? void 0 : preparedChart.margin.right) !== null && _b !== void 0 ? _b : 0,
185
188
  height: boundsHeight,
186
189
  scale,
187
190
  series: preparedSeries.filter((s) => s.visible),
@@ -204,6 +207,7 @@ export const ChartInner = (props) => {
204
207
  boundsHeight,
205
208
  boundsOffsetLeft,
206
209
  boundsWidth,
210
+ preparedChart,
207
211
  preparedSeries,
208
212
  preparedSplit,
209
213
  width,
@@ -18,10 +18,9 @@ export const Tooltip = (props) => {
18
18
  if (!svgContainer)
19
19
  return undefined;
20
20
  const updateRect = (e) => {
21
- // Skip synthetic events (e.g. the CustomEvent('scroll') dispatched by the
22
- // sibling useEffect below to reposition the Popup). Only native browser
23
- // scroll events have isTrusted === true, so those still update the rect.
24
- if (e && !e.isTrusted)
21
+ // Skip the synthetic CustomEvent('scroll') dispatched below to reposition the Popup.
22
+ // All other events including transitionend and animationend should update the rect.
23
+ if (e instanceof CustomEvent)
25
24
  return;
26
25
  containerRectRef.current = svgContainer.getBoundingClientRect();
27
26
  };
@@ -29,9 +28,13 @@ export const Tooltip = (props) => {
29
28
  const resizeObserver = new ResizeObserver(() => updateRect());
30
29
  resizeObserver.observe(svgContainer);
31
30
  window.addEventListener('scroll', updateRect, { passive: true, capture: true });
31
+ window.addEventListener('transitionend', updateRect, { capture: true });
32
+ window.addEventListener('animationend', updateRect, { capture: true });
32
33
  return () => {
33
34
  resizeObserver.disconnect();
34
35
  window.removeEventListener('scroll', updateRect, { capture: true });
36
+ window.removeEventListener('transitionend', updateRect, { capture: true });
37
+ window.removeEventListener('animationend', updateRect, { capture: true });
35
38
  };
36
39
  }, [svgContainer]);
37
40
  const containerRect = containerRectRef.current;
@@ -7,6 +7,7 @@ import { validateData } from '../core/validation';
7
7
  import { ChartInner } from './ChartInner';
8
8
  export * from './Tooltip/ChartTooltipContent';
9
9
  export const Chart = React.forwardRef(function Chart(props, forwardedRef) {
10
+ var _a, _b;
10
11
  const { data, lang, onResize, onReady } = props;
11
12
  const validatedData = React.useRef();
12
13
  const ref = React.useRef(null);
@@ -48,11 +49,15 @@ export const Chart = React.forwardRef(function Chart(props, forwardedRef) {
48
49
  // https://github.com/d3/d3-selection/blob/main/README.md#handling-events
49
50
  const eventName = `resize.${getUniqId()}`;
50
51
  selection.on(eventName, debuncedHandleResize);
52
+ window.addEventListener('transitionend', handleResize, { capture: true });
53
+ window.addEventListener('animationend', handleResize, { capture: true });
51
54
  return () => {
52
55
  // https://d3js.org/d3-selection/events#selection_on
53
56
  selection.on(eventName, null);
57
+ window.removeEventListener('transitionend', handleResize, { capture: true });
58
+ window.removeEventListener('animationend', handleResize, { capture: true });
54
59
  };
55
- }, [debuncedHandleResize]);
60
+ }, [debuncedHandleResize, handleResize]);
56
61
  React.useEffect(() => {
57
62
  if (typeof onResize === 'function') {
58
63
  onResize({ dimensions });
@@ -67,5 +72,5 @@ export const Chart = React.forwardRef(function Chart(props, forwardedRef) {
67
72
  width: (dimensions === null || dimensions === void 0 ? void 0 : dimensions.width) || '100%',
68
73
  height: (dimensions === null || dimensions === void 0 ? void 0 : dimensions.height) || '100%',
69
74
  position: 'relative',
70
- } }, (dimensions === null || dimensions === void 0 ? void 0 : dimensions.height) && (dimensions === null || dimensions === void 0 ? void 0 : dimensions.width) && (React.createElement(ChartInner, { height: dimensions === null || dimensions === void 0 ? void 0 : dimensions.height, width: dimensions === null || dimensions === void 0 ? void 0 : dimensions.width, data: data, onReady: onReady }))));
75
+ } }, Boolean((dimensions === null || dimensions === void 0 ? void 0 : dimensions.height) && (dimensions === null || dimensions === void 0 ? void 0 : dimensions.width)) && (React.createElement(ChartInner, { height: (_a = dimensions === null || dimensions === void 0 ? void 0 : dimensions.height) !== null && _a !== void 0 ? _a : 0, width: (_b = dimensions === null || dimensions === void 0 ? void 0 : dimensions.width) !== null && _b !== void 0 ? _b : 0, data: data, onReady: onReady }))));
71
76
  });
@@ -161,7 +161,7 @@ function getDomainMaxAlignedToEndTick(args) {
161
161
  else {
162
162
  step = tickStep(dMin, dMax, 1);
163
163
  }
164
- dNewMax = tickValues[tickValues.length - 1].value + step;
164
+ dNewMax = Math.floor(dMax / step + 1) * step;
165
165
  }
166
166
  }
167
167
  return dNewMax;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gravity-ui/charts",
3
- "version": "1.48.1",
3
+ "version": "1.48.3",
4
4
  "description": "A flexible JavaScript library for data visualization and chart rendering using React",
5
5
  "license": "MIT",
6
6
  "main": "dist/cjs/index.js",