@gravity-ui/charts 1.34.8 → 1.34.9

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 (51) hide show
  1. package/dist/cjs/components/AxisX/AxisX.js +0 -1
  2. package/dist/cjs/components/AxisX/prepare-axis-data.js +4 -23
  3. package/dist/cjs/components/AxisX/types.d.ts +1 -9
  4. package/dist/cjs/components/AxisY/prepare-axis-title.js +3 -34
  5. package/dist/cjs/components/AxisY/types.d.ts +1 -9
  6. package/dist/cjs/components/ChartInner/index.js +6 -6
  7. package/dist/cjs/components/ChartInner/useChartInnerProps.d.ts +0 -1
  8. package/dist/cjs/components/ChartInner/useChartInnerProps.js +16 -6
  9. package/dist/cjs/components/ChartInner/useChartInnerState.d.ts +2 -3
  10. package/dist/cjs/components/ChartInner/useChartInnerState.js +14 -22
  11. package/dist/cjs/components/RangeSlider/index.d.ts +0 -1
  12. package/dist/cjs/components/RangeSlider/index.js +1 -8
  13. package/dist/cjs/components/types/index.d.ts +9 -0
  14. package/dist/cjs/components/types/index.js +1 -0
  15. package/dist/cjs/components/utils/axis-title.d.ts +6 -0
  16. package/dist/cjs/components/utils/axis-title.js +39 -0
  17. package/dist/cjs/components/{utils.d.ts → utils/index.d.ts} +3 -3
  18. package/dist/cjs/components/{utils.js → utils/index.js} +1 -1
  19. package/dist/cjs/hooks/useAxisScales/index.js +3 -1
  20. package/dist/cjs/hooks/useAxisScales/x-scale.d.ts +0 -6
  21. package/dist/cjs/hooks/useAxisScales/x-scale.js +12 -35
  22. package/dist/cjs/hooks/useRangeSlider/types.d.ts +1 -1
  23. package/dist/cjs/hooks/useSeries/prepare-legend.js +2 -1
  24. package/dist/cjs/utils/chart/zoom.d.ts +2 -1
  25. package/dist/cjs/utils/chart/zoom.js +9 -0
  26. package/dist/esm/components/AxisX/AxisX.js +0 -1
  27. package/dist/esm/components/AxisX/prepare-axis-data.js +4 -23
  28. package/dist/esm/components/AxisX/types.d.ts +1 -9
  29. package/dist/esm/components/AxisY/prepare-axis-title.js +3 -34
  30. package/dist/esm/components/AxisY/types.d.ts +1 -9
  31. package/dist/esm/components/ChartInner/index.js +6 -6
  32. package/dist/esm/components/ChartInner/useChartInnerProps.d.ts +0 -1
  33. package/dist/esm/components/ChartInner/useChartInnerProps.js +16 -6
  34. package/dist/esm/components/ChartInner/useChartInnerState.d.ts +2 -3
  35. package/dist/esm/components/ChartInner/useChartInnerState.js +14 -22
  36. package/dist/esm/components/RangeSlider/index.d.ts +0 -1
  37. package/dist/esm/components/RangeSlider/index.js +1 -8
  38. package/dist/esm/components/types/index.d.ts +9 -0
  39. package/dist/esm/components/types/index.js +1 -0
  40. package/dist/esm/components/utils/axis-title.d.ts +6 -0
  41. package/dist/esm/components/utils/axis-title.js +39 -0
  42. package/dist/esm/components/{utils.d.ts → utils/index.d.ts} +3 -3
  43. package/dist/esm/components/{utils.js → utils/index.js} +1 -1
  44. package/dist/esm/hooks/useAxisScales/index.js +3 -1
  45. package/dist/esm/hooks/useAxisScales/x-scale.d.ts +0 -6
  46. package/dist/esm/hooks/useAxisScales/x-scale.js +12 -35
  47. package/dist/esm/hooks/useRangeSlider/types.d.ts +1 -1
  48. package/dist/esm/hooks/useSeries/prepare-legend.js +2 -1
  49. package/dist/esm/utils/chart/zoom.d.ts +2 -1
  50. package/dist/esm/utils/chart/zoom.js +9 -0
  51. package/package.json +1 -1
@@ -32,7 +32,6 @@ export const AxisX = (props) => {
32
32
  .attr('class', b('title'))
33
33
  .append('text')
34
34
  .attr('text-anchor', 'start')
35
- .style('dominant-baseline', 'text-after-edge')
36
35
  .style('transform', `translate(${preparedAxisData.title.x}px, ${preparedAxisData.title.y}px) rotate(${preparedAxisData.title.rotate}deg) translate(0px, ${preparedAxisData.title.offset}px)`)
37
36
  .attr('font-size', preparedAxisData.title.style.fontSize)
38
37
  .selectAll('tspan')
@@ -1,6 +1,7 @@
1
1
  import { getUniqId } from '@gravity-ui/uikit';
2
- import { calculateSin, formatAxisTickLabel, getBandsPosition, getLabelsSize, getMinSpaceBetween, getTextSizeFn, getTextWithElipsis, wrapText, } from '../../utils';
2
+ import { calculateSin, formatAxisTickLabel, getBandsPosition, getLabelsSize, getMinSpaceBetween, getTextSizeFn, getTextWithElipsis, } from '../../utils';
3
3
  import { getXAxisTickValues } from '../../utils/chart/axis/x-axis';
4
+ import { getMultilineTitleContentRows } from '../utils/axis-title';
4
5
  async function getSvgAxisLabel({ getTextSize, text, axis, top, left, labelMaxWidth, axisWidth, boundsOffsetLeft, boundsOffsetRight, }) {
5
6
  var _a;
6
7
  const rotation = axis.labels.rotation;
@@ -189,23 +190,7 @@ export async function prepareXAxisData({ axis, boundsOffsetLeft, boundsOffsetRig
189
190
  const titleContent = [];
190
191
  const titleMaxWidth = axisWidth;
191
192
  if (axis.title.maxRowCount > 1) {
192
- const titleTextRows = await wrapText({
193
- text: axis.title.text,
194
- style: axis.title.style,
195
- width: titleMaxWidth,
196
- getTextSize: getTitleTextSize,
197
- });
198
- for (let i = 0; i < axis.title.maxRowCount && i < titleTextRows.length; i++) {
199
- const textRow = titleTextRows[i];
200
- const textRowContent = textRow.text.trim();
201
- const textRowSize = await getTitleTextSize(textRowContent);
202
- titleContent.push({
203
- text: textRowContent,
204
- x: 0,
205
- y: textRow.y,
206
- size: textRowSize,
207
- });
208
- }
193
+ titleContent.push(...(await getMultilineTitleContentRows({ axis, titleMaxWidth })));
209
194
  }
210
195
  else {
211
196
  const text = await getTextWithElipsis({
@@ -245,11 +230,7 @@ export async function prepareXAxisData({ axis, boundsOffsetLeft, boundsOffsetRig
245
230
  style: axis.title.style,
246
231
  size: titleTextSize,
247
232
  x,
248
- y: height +
249
- axis.labels.margin +
250
- axis.labels.height +
251
- axis.title.margin +
252
- titleTextSize.height,
233
+ y: height + axis.labels.margin + axis.labels.height + axis.title.margin,
253
234
  rotate: 0,
254
235
  offset: 0,
255
236
  };
@@ -1,14 +1,6 @@
1
1
  import type { DashStyle } from 'src/constants';
2
2
  import type { BaseTextStyle, HtmlItem, PlotLayerPlacement, PointPosition } from '../../types';
3
- export type TextRowData = {
4
- text: string;
5
- x: number;
6
- y: number;
7
- size: {
8
- width: number;
9
- height: number;
10
- };
11
- };
3
+ import type { TextRowData } from '../types';
12
4
  export type AxisSvgLabelData = {
13
5
  x: number;
14
6
  y: number;
@@ -1,4 +1,5 @@
1
- import { calculateCos, calculateSin, getLabelsSize, getTextSizeFn, getTextWithElipsis, wrapText, } from '../../utils';
1
+ import { calculateCos, calculateSin, getLabelsSize, getTextSizeFn, getTextWithElipsis, } from '../../utils';
2
+ import { getMultilineTitleContentRows } from '../utils/axis-title';
2
3
  export async function prepareSvgYAxisTitle({ axis, axisTop, axisHeight, axisWidth, axisLabelsWidth, }) {
3
4
  if (!axis.title.text || axis.title.html) {
4
5
  return null;
@@ -10,39 +11,7 @@ export async function prepareSvgYAxisTitle({ axis, axisTop, axisHeight, axisWidt
10
11
  const titleContent = [];
11
12
  const titleMaxWidth = rotateAngle === 0 ? axis.title.maxWidth : sin * axisHeight;
12
13
  if (axis.title.maxRowCount > 1) {
13
- let titleTextRows = await wrapText({
14
- text: axis.title.text,
15
- style: axis.title.style,
16
- width: titleMaxWidth,
17
- getTextSize: getTitleTextSize,
18
- });
19
- titleTextRows = titleTextRows.reduce((acc, row, index) => {
20
- if (index < axis.title.maxRowCount) {
21
- acc.push(row);
22
- }
23
- else {
24
- acc[axis.title.maxRowCount - 1].text += row.text;
25
- }
26
- return acc;
27
- }, []);
28
- for (let i = 0; i < titleTextRows.length; i++) {
29
- const textRow = titleTextRows[i];
30
- let textRowContent = textRow.text.trim();
31
- if (i === titleTextRows.length - 1) {
32
- textRowContent = await getTextWithElipsis({
33
- text: textRowContent,
34
- maxWidth: titleMaxWidth,
35
- getTextWidth: async (s) => (await getTitleTextSize(s)).width,
36
- });
37
- }
38
- const textRowSize = await getTitleTextSize(textRowContent);
39
- titleContent.push({
40
- text: textRowContent,
41
- x: 0,
42
- y: textRow.y,
43
- size: textRowSize,
44
- });
45
- }
14
+ titleContent.push(...(await getMultilineTitleContentRows({ axis, titleMaxWidth })));
46
15
  }
47
16
  else {
48
17
  const text = await getTextWithElipsis({
@@ -1,14 +1,6 @@
1
1
  import type { DashStyle } from 'src/constants';
2
2
  import type { BaseTextStyle, HtmlItem, PlotLayerPlacement, PointPosition } from '../../types';
3
- export type TextRowData = {
4
- text: string;
5
- x: number;
6
- y: number;
7
- size: {
8
- width: number;
9
- height: number;
10
- };
11
- };
3
+ import type { TextRowData } from '../types';
12
4
  export type AxisSvgLabelData = {
13
5
  x: number;
14
6
  y: number;
@@ -26,7 +26,7 @@ import './styles.css';
26
26
  const b = block('chart');
27
27
  const DEBOUNCED_VALUE_DELAY = 10;
28
28
  export const ChartInner = (props) => {
29
- var _a, _b, _c, _d, _e, _f;
29
+ var _a, _b, _c, _d, _e;
30
30
  const { width, height, data, onReady } = props;
31
31
  const svgRef = React.useRef(null);
32
32
  const resetZoomButtonRef = React.useRef(null);
@@ -60,13 +60,13 @@ export const ChartInner = (props) => {
60
60
  }, [data.xAxis]);
61
61
  const { initialized, setInitialized, tooltipPinned, togglePinTooltip, unpinTooltip, rangeSliderState, updateRangeSliderState, updateZoomState, zoomState, } = useChartInnerState({
62
62
  dispatcher,
63
- preparedChart,
64
63
  preparedRangeSlider,
65
64
  tooltip: preparedTooltip,
66
65
  });
67
66
  const { allPreparedSeries, boundsHeight, boundsOffsetLeft, boundsOffsetTop, boundsWidth, handleLegendItemClick, isOutsideBounds, legendConfig, legendItems, preparedLegend, preparedSeries, preparedSeriesOptions, preparedSplit, prevHeight, prevWidth, shapes, shapesData, shapesReady, xAxis, xScale, yAxis, yScale, } = useChartInnerProps(Object.assign(Object.assign({}, props), { clipPathId,
68
67
  dispatcher,
69
- htmlLayout, plotNode: plotRef.current, preparedChart, rangeSliderDomain: (_a = rangeSliderRef.current) === null || _a === void 0 ? void 0 : _a.getDomain(), rangeSliderState, svgContainer: svgRef.current, updateZoomState,
68
+ htmlLayout, plotNode: plotRef.current, preparedChart,
69
+ rangeSliderState, svgContainer: svgRef.current, updateZoomState,
70
70
  zoomState }));
71
71
  const debouncedBoundsWidth = useDebouncedValue({
72
72
  value: boundsWidth,
@@ -96,8 +96,8 @@ export const ChartInner = (props) => {
96
96
  tooltipThrottle: preparedTooltip.throttle,
97
97
  isOutsideBounds,
98
98
  });
99
- const clickHandler = (_c = (_b = data.chart) === null || _b === void 0 ? void 0 : _b.events) === null || _c === void 0 ? void 0 : _c.click;
100
- const pointerMoveHandler = (_e = (_d = data.chart) === null || _d === void 0 ? void 0 : _d.events) === null || _e === void 0 ? void 0 : _e.pointermove;
99
+ const clickHandler = (_b = (_a = data.chart) === null || _a === void 0 ? void 0 : _a.events) === null || _b === void 0 ? void 0 : _b.click;
100
+ const pointerMoveHandler = (_d = (_c = data.chart) === null || _c === void 0 ? void 0 : _c.events) === null || _d === void 0 ? void 0 : _d.pointermove;
101
101
  const prevRangeSliderDefaultRange = usePrevious(preparedRangeSlider.defaultRange);
102
102
  useCrosshair({
103
103
  split: preparedSplit,
@@ -246,7 +246,7 @@ export const ChartInner = (props) => {
246
246
  React.createElement("g", { ref: plotBeforeRef }),
247
247
  shapes,
248
248
  React.createElement("g", { ref: plotAfterRef })),
249
- ((_f = xAxis === null || xAxis === void 0 ? void 0 : xAxis.rangeSlider) === null || _f === void 0 ? void 0 : _f.enabled) && (React.createElement(RangeSlider, { boundsOffsetLeft: debouncedOffsetLeft, boundsWidth: debouncedBoundsWidth, height: height, htmlLayout: htmlLayout, onUpdate: updateRangeSliderState, preparedChart: preparedChart, preparedLegend: preparedLegend, preparedSeries: debouncedAllPreparedSeries, preparedSeriesOptions: preparedSeriesOptions, preparedRangeSlider: xAxis.rangeSlider, rangeSliderState: rangeSliderState, ref: rangeSliderRef, width: width, xAxis: data.xAxis, yAxis: data.yAxis })),
249
+ ((_e = xAxis === null || xAxis === void 0 ? void 0 : xAxis.rangeSlider) === null || _e === void 0 ? void 0 : _e.enabled) && (React.createElement(RangeSlider, { boundsOffsetLeft: debouncedOffsetLeft, boundsWidth: debouncedBoundsWidth, height: height, htmlLayout: htmlLayout, onUpdate: updateRangeSliderState, preparedChart: preparedChart, preparedLegend: preparedLegend, preparedSeries: debouncedAllPreparedSeries, preparedSeriesOptions: preparedSeriesOptions, preparedRangeSlider: xAxis.rangeSlider, rangeSliderState: rangeSliderState, ref: rangeSliderRef, width: width, xAxis: data.xAxis, yAxis: data.yAxis })),
250
250
  (preparedLegend === null || preparedLegend === void 0 ? void 0 : preparedLegend.enabled) && legendConfig && (React.createElement(Legend, { chartSeries: preparedSeries, legend: preparedLegend, items: legendItems, config: legendConfig, onItemClick: handleLegendItemClick, onUpdate: unpinTooltip, htmlLayout: htmlLayout }))));
251
251
  return (React.createElement("div", { className: b() },
252
252
  React.createElement("svg", { ref: svgRef, width: width, height: height,
@@ -11,7 +11,6 @@ type Props = ChartInnerProps & {
11
11
  svgContainer: SVGGElement | null;
12
12
  updateZoomState: (nextZoomState: Partial<ZoomState>) => void;
13
13
  zoomState: Partial<ZoomState>;
14
- rangeSliderDomain?: [number, number];
15
14
  rangeSliderState?: RangeSliderState;
16
15
  };
17
16
  export declare function useChartInnerProps(props: Props): {
@@ -4,7 +4,7 @@ import { useAxis, useAxisScales, useChartDimensions, useNormalizedOriginalData,
4
4
  import { getYAxisWidth } from '../../hooks/useChartDimensions/utils';
5
5
  import { getLegendComponents } from '../../hooks/useSeries/prepare-legend';
6
6
  import { getPreparedOptions } from '../../hooks/useSeries/prepare-options';
7
- import { getZoomedSeriesData } from '../../utils';
7
+ import { getEffectiveXRange, getZoomedSeriesData } from '../../utils';
8
8
  import { hasAtLeastOneSeriesDataPerPlot } from './utils';
9
9
  const CLIP_PATH_BY_SERIES_TYPE = {
10
10
  [SERIES_TYPE.Scatter]: false,
@@ -35,7 +35,7 @@ function getBoundsOffsetLeft(args) {
35
35
  }
36
36
  export function useChartInnerProps(props) {
37
37
  var _a;
38
- const { clipPathId, data, dispatcher, height, htmlLayout, plotNode, preparedChart, rangeSliderDomain, rangeSliderState, svgContainer, width, updateZoomState, zoomState, } = props;
38
+ const { clipPathId, data, dispatcher, height, htmlLayout, plotNode, preparedChart, rangeSliderState, svgContainer, width, updateZoomState, zoomState, } = props;
39
39
  const prevWidth = usePrevious(width);
40
40
  const prevHeight = usePrevious(height);
41
41
  const colors = React.useMemo(() => {
@@ -57,14 +57,25 @@ export function useChartInnerProps(props) {
57
57
  seriesData: normalizedSeriesData,
58
58
  seriesOptions: data.series.options,
59
59
  });
60
+ const effectiveZoomState = React.useMemo(() => {
61
+ const result = {};
62
+ const effectiveX = getEffectiveXRange(zoomState.x, rangeSliderState);
63
+ if (effectiveX !== undefined) {
64
+ result.x = effectiveX;
65
+ }
66
+ if (zoomState.y !== undefined) {
67
+ result.y = zoomState.y;
68
+ }
69
+ return result;
70
+ }, [zoomState, rangeSliderState]);
60
71
  const { preparedSeries, preparedShapesSeries } = React.useMemo(() => {
61
72
  return getZoomedSeriesData({
62
73
  seriesData: allPreparedSeries,
63
74
  xAxis: normalizedXAxis,
64
75
  yAxis: normalizedYAxis,
65
- zoomState,
76
+ zoomState: effectiveZoomState,
66
77
  });
67
- }, [allPreparedSeries, normalizedXAxis, normalizedYAxis, zoomState]);
78
+ }, [allPreparedSeries, normalizedXAxis, normalizedYAxis, effectiveZoomState]);
68
79
  const { legendConfig, legendItems } = React.useMemo(() => {
69
80
  if (!preparedLegend) {
70
81
  return { legendConfig: undefined, legendItems: [] };
@@ -126,7 +137,7 @@ export function useChartInnerProps(props) {
126
137
  htmlLayout,
127
138
  clipPathId,
128
139
  isOutsideBounds,
129
- zoomState,
140
+ zoomState: effectiveZoomState,
130
141
  });
131
142
  const handleAttemptToSetZoomState = React.useCallback((nextZoomState) => {
132
143
  const { preparedSeries: nextZoomedSeriesData } = getZoomedSeriesData({
@@ -147,7 +158,6 @@ export function useChartInnerProps(props) {
147
158
  plotContainerWidth: boundsWidth,
148
159
  preparedSplit,
149
160
  preparedZoom: preparedChart.zoom,
150
- rangeSliderDomain,
151
161
  xAxis,
152
162
  xScale,
153
163
  yAxis,
@@ -1,9 +1,8 @@
1
1
  import React from 'react';
2
2
  import type { Dispatch } from 'd3';
3
- import type { PreparedChart, PreparedRangeSlider, PreparedTooltip, RangeSliderState, ZoomState } from '../../hooks';
3
+ import type { PreparedRangeSlider, PreparedTooltip, RangeSliderState, ZoomState } from '../../hooks';
4
4
  type Props = {
5
5
  dispatcher: Dispatch<object>;
6
- preparedChart: PreparedChart;
7
6
  preparedRangeSlider: PreparedRangeSlider;
8
7
  tooltip?: PreparedTooltip;
9
8
  };
@@ -14,7 +13,7 @@ export declare function useChartInnerState(props: Props): {
14
13
  tooltipPinned: boolean;
15
14
  togglePinTooltip: ((value: boolean, event: React.MouseEvent) => void) | undefined;
16
15
  unpinTooltip: (() => void) | undefined;
17
- updateRangeSliderState: (nextRangeSliderState?: RangeSliderState, syncZoom?: boolean) => void;
16
+ updateRangeSliderState: (nextRangeSliderState?: RangeSliderState) => void;
18
17
  updateZoomState: (nextZoomState: Partial<ZoomState>) => void;
19
18
  zoomState: Partial<ZoomState>;
20
19
  };
@@ -1,11 +1,9 @@
1
1
  import React from 'react';
2
2
  import isEqual from 'lodash/isEqual';
3
- import { ZOOM_TYPE } from '../../constants';
4
3
  import { EventType, isMacintosh } from '../../utils';
5
- const RANGE_SLIDER_SYNC_ZOOM_TYPES = [ZOOM_TYPE.X, ZOOM_TYPE.XY];
6
4
  export function useChartInnerState(props) {
7
5
  var _a, _b;
8
- const { dispatcher, preparedChart, preparedRangeSlider, tooltip } = props;
6
+ const { dispatcher, preparedRangeSlider, tooltip } = props;
9
7
  const [tooltipPinned, setTooltipPinned] = React.useState(false);
10
8
  const [zoomState, setZoomState] = React.useState({});
11
9
  const [rangeSliderState, setRangeSliderState] = React.useState();
@@ -13,7 +11,6 @@ export function useChartInnerState(props) {
13
11
  const tooltipEnabled = tooltip === null || tooltip === void 0 ? void 0 : tooltip.enabled;
14
12
  const tooltipPinEnabled = (_a = tooltip === null || tooltip === void 0 ? void 0 : tooltip.pin) === null || _a === void 0 ? void 0 : _a.enabled;
15
13
  const modifierKey = (_b = tooltip === null || tooltip === void 0 ? void 0 : tooltip.pin) === null || _b === void 0 ? void 0 : _b.modifierKey;
16
- const rangeSliderEnabled = preparedRangeSlider.enabled;
17
14
  const togglePinTooltip = React.useCallback((value, event) => {
18
15
  let resultValue = value;
19
16
  if (value && modifierKey) {
@@ -36,16 +33,17 @@ export function useChartInnerState(props) {
36
33
  const updateZoomState = React.useCallback((nextZoomState) => {
37
34
  if (!isEqual(zoomState, nextZoomState)) {
38
35
  setZoomState(nextZoomState);
39
- if (rangeSliderEnabled && (nextZoomState === null || nextZoomState === void 0 ? void 0 : nextZoomState.x)) {
40
- const [xMin, xMax] = nextZoomState.x;
41
- setRangeSliderState({
42
- max: xMax,
43
- min: xMin,
44
- });
36
+ // One-way sync: zoom range slider. On full reset clear the slider state;
37
+ // on x-zoom change sync slider to the zoomed x range.
38
+ if (Object.keys(nextZoomState).length === 0) {
39
+ setRangeSliderState(undefined);
40
+ }
41
+ else if (nextZoomState.x !== undefined) {
42
+ setRangeSliderState({ min: nextZoomState.x[0], max: nextZoomState.x[1] });
45
43
  }
46
44
  }
47
- }, [rangeSliderEnabled, zoomState]);
48
- const updateRangeSliderState = React.useCallback((nextRangeSliderState, syncZoom = true) => {
45
+ }, [zoomState]);
46
+ const updateRangeSliderState = React.useCallback((nextRangeSliderState) => {
49
47
  if (!isEqual(rangeSliderState, nextRangeSliderState)) {
50
48
  setRangeSliderState(nextRangeSliderState
51
49
  ? {
@@ -53,17 +51,11 @@ export function useChartInnerState(props) {
53
51
  min: nextRangeSliderState.min,
54
52
  }
55
53
  : undefined);
56
- if (syncZoom &&
57
- nextRangeSliderState &&
58
- preparedChart.zoom &&
59
- Object.keys(zoomState || {}).length > 0 &&
60
- RANGE_SLIDER_SYNC_ZOOM_TYPES.includes(preparedChart.zoom.type)) {
61
- setZoomState({
62
- x: [nextRangeSliderState.min, nextRangeSliderState.max],
63
- });
64
- }
54
+ // Moving the slider clears zoom so the slider is always the sole source of truth.
55
+ // Zoom stays active only until the user explicitly touches the slider.
56
+ setZoomState({});
65
57
  }
66
- }, [preparedChart.zoom, rangeSliderState, zoomState]);
58
+ }, [rangeSliderState]);
67
59
  return {
68
60
  initialized,
69
61
  rangeSliderState,
@@ -2,7 +2,6 @@ import React from 'react';
2
2
  import type { RangeSliderProps } from '../../hooks';
3
3
  import './styles.css';
4
4
  export interface RangeSliderHandle {
5
- getDomain: () => [number, number] | undefined;
6
5
  resetState: () => void;
7
6
  }
8
7
  export declare const RangeSlider: React.MemoExoticComponent<React.ForwardRefExoticComponent<RangeSliderProps & React.RefAttributes<RangeSliderHandle>>>;
@@ -20,24 +20,17 @@ function RangeSliderComponent(props, forwardedRef) {
20
20
  * in that case, the extra computations simply don't happen.
21
21
  *
22
22
  * Methods:
23
- * - getDomain: returns the current X-axis domain (for synchronization with zoom)
24
23
  * - resetState: resets the slider state to its initial value (these calculations
25
24
  * are only possible within the component since it has access to the prepared series data)
26
25
  */
27
26
  React.useImperativeHandle(forwardedRef, () => ({
28
- getDomain: () => {
29
- if (!xScale || isBandScale(xScale)) {
30
- return undefined;
31
- }
32
- return xScale.domain().map(Number);
33
- },
34
27
  resetState: () => {
35
28
  if (xScale && !isBandScale(xScale)) {
36
29
  const initialRangeSliderState = getInitialRangeSliderState({
37
30
  xScale,
38
31
  defaultRange,
39
32
  });
40
- onUpdate(initialRangeSliderState, false);
33
+ onUpdate(initialRangeSliderState);
41
34
  }
42
35
  },
43
36
  }), [defaultRange, onUpdate, xScale]);
@@ -0,0 +1,9 @@
1
+ export type TextRowData = {
2
+ text: string;
3
+ x: number;
4
+ y: number;
5
+ size: {
6
+ width: number;
7
+ height: number;
8
+ };
9
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ import type { PreparedAxis } from '../../hooks';
2
+ import type { TextRowData } from '../types';
3
+ export declare function getMultilineTitleContentRows({ axis, titleMaxWidth, }: {
4
+ axis: PreparedAxis;
5
+ titleMaxWidth: number;
6
+ }): Promise<TextRowData[]>;
@@ -0,0 +1,39 @@
1
+ import { getTextSizeFn, getTextWithElipsis, wrapText } from '../../utils';
2
+ export async function getMultilineTitleContentRows({ axis, titleMaxWidth, }) {
3
+ const titleContent = [];
4
+ const getTitleTextSize = getTextSizeFn({ style: axis.title.style });
5
+ let titleTextRows = await wrapText({
6
+ text: axis.title.text,
7
+ style: axis.title.style,
8
+ width: titleMaxWidth,
9
+ getTextSize: getTitleTextSize,
10
+ });
11
+ titleTextRows = titleTextRows.reduce((acc, row, index) => {
12
+ if (index < axis.title.maxRowCount) {
13
+ acc.push(row);
14
+ }
15
+ else {
16
+ acc[axis.title.maxRowCount - 1].text += row.text;
17
+ }
18
+ return acc;
19
+ }, []);
20
+ for (let i = 0; i < titleTextRows.length; i++) {
21
+ const textRow = titleTextRows[i];
22
+ let textRowContent = textRow.text.trim();
23
+ if (i === titleTextRows.length - 1) {
24
+ textRowContent = await getTextWithElipsis({
25
+ text: textRowContent,
26
+ maxWidth: titleMaxWidth,
27
+ getTextWidth: async (s) => (await getTitleTextSize(s)).width,
28
+ });
29
+ }
30
+ const textRowSize = await getTitleTextSize(textRowContent);
31
+ titleContent.push({
32
+ text: textRowContent,
33
+ x: 0,
34
+ y: textRow.y,
35
+ size: textRowSize,
36
+ });
37
+ }
38
+ return titleContent;
39
+ }
@@ -1,6 +1,6 @@
1
- import type { DashStyle } from '../constants';
2
- import type { ChartScaleLinear, ChartScaleTime } from '../hooks';
3
- import type { ChartAxisRangeSlider } from '../types';
1
+ import type { DashStyle } from '../../constants';
2
+ import type { ChartScaleLinear, ChartScaleTime } from '../../hooks';
3
+ import type { ChartAxisRangeSlider } from '../../types';
4
4
  export declare function getInitialRangeSliderState(args: {
5
5
  xScale: ChartScaleLinear | ChartScaleTime;
6
6
  defaultRange?: ChartAxisRangeSlider['defaultRange'];
@@ -1,7 +1,7 @@
1
1
  import { duration } from '@gravity-ui/date-utils';
2
2
  import { line as lineGenerator } from 'd3';
3
3
  import { select } from 'd3-selection';
4
- import { getLineDashArray, isTimeScale } from '../utils';
4
+ import { getLineDashArray, isTimeScale } from '../../utils';
5
5
  export function getInitialRangeSliderState(args) {
6
6
  const { defaultRange, xScale } = args;
7
7
  let minRange;
@@ -9,7 +9,9 @@ export { createXScale } from './x-scale';
9
9
  export { createYScale } from './y-scale';
10
10
  const createScales = (args) => {
11
11
  const { boundsWidth, boundsHeight, isRangeSlider, rangeSliderState, series, split, xAxis, yAxis, zoomState, } = args;
12
- let visibleSeries = getOnlyVisibleSeries(series);
12
+ // For range slider: always use all series regardless of visibility so the slider domain
13
+ // stays stable when the user hides/shows series via the legend.
14
+ let visibleSeries = isRangeSlider ? series : getOnlyVisibleSeries(series);
13
15
  // Reassign to all series in case of all series unselected,
14
16
  // otherwise we will get an empty space without grid
15
17
  visibleSeries = visibleSeries.length === 0 ? series : visibleSeries;
@@ -1,11 +1,5 @@
1
1
  import type { PreparedAxis, PreparedSeries, RangeSliderState } from '../../hooks';
2
2
  import type { ChartAxis, ChartSeries } from '../../types';
3
- export declare function getXMaxDomainResult(args: {
4
- xMaxDomain: number;
5
- xMaxProps?: number;
6
- xMaxRangeSlider?: number;
7
- xMaxZoom?: number;
8
- }): number;
9
3
  export declare function createXScale(args: {
10
4
  axis: PreparedAxis | ChartAxis;
11
5
  boundsWidth: number;
@@ -1,7 +1,7 @@
1
1
  import { extent, scaleBand, scaleLinear, scaleLog, scaleUtc } from 'd3';
2
2
  import get from 'lodash/get';
3
3
  import { DEFAULT_AXIS_TYPE, SERIES_TYPE } from '../../constants';
4
- import { getAxisCategories, getDefaultMaxXAxisValue, getDefaultMinXAxisValue, getDomainDataXBySeries, } from '../../utils';
4
+ import { getDefaultMaxXAxisValue, getDefaultMinXAxisValue, getDomainDataXBySeries, getEffectiveXRange, } from '../../utils';
5
5
  import { getBandSize } from '../utils/get-band-size';
6
6
  import { checkIsPointDomain, filterCategoriesByVisibleSeries, getMinMaxPropsOrState, hasOnlyMarkerSeries, validateArrayData, } from './utils';
7
7
  const X_AXIS_ZOOM_PADDING = 0.02;
@@ -27,23 +27,6 @@ function isSeriesWithXAxisOffset(series) {
27
27
  const types = [SERIES_TYPE.Heatmap, SERIES_TYPE.BarX];
28
28
  return series.some((s) => types.includes(s.type));
29
29
  }
30
- export function getXMaxDomainResult(args) {
31
- const { xMaxDomain, xMaxProps, xMaxRangeSlider, xMaxZoom } = args;
32
- let xMaxDomainResult = xMaxDomain;
33
- // When xMaxRangeSlider is provided, we use it directly without considering xMaxDomain.
34
- // This is intentional: the range slider needs to display the chart's maxPadding area,
35
- // which would be clipped if we constrained it to xMaxDomain.
36
- if (typeof xMaxRangeSlider === 'number') {
37
- xMaxDomainResult = xMaxRangeSlider;
38
- }
39
- else if (typeof xMaxZoom === 'number' && xMaxZoom < xMaxDomain) {
40
- xMaxDomainResult = xMaxZoom;
41
- }
42
- else if (typeof xMaxProps === 'number' && xMaxProps < xMaxDomain) {
43
- xMaxDomainResult = xMaxProps;
44
- }
45
- return xMaxDomainResult;
46
- }
47
30
  function getXScaleRange({ boundsWidth, hasZoomX }) {
48
31
  const xAxisZoomPadding = boundsWidth * X_AXIS_ZOOM_PADDING;
49
32
  const xRange = [0, boundsWidth];
@@ -54,22 +37,17 @@ function getXScaleRange({ boundsWidth, hasZoomX }) {
54
37
  // eslint-disable-next-line complexity
55
38
  export function createXScale(args) {
56
39
  const { axis, boundsWidth, series, rangeSliderState, zoomStateX } = args;
40
+ const effectiveX = getEffectiveXRange(zoomStateX, rangeSliderState);
41
+ const effectiveXMin = effectiveX === null || effectiveX === void 0 ? void 0 : effectiveX[0];
42
+ const effectiveXMax = effectiveX === null || effectiveX === void 0 ? void 0 : effectiveX[1];
57
43
  const [xMinPropsOrState, xMaxPropsOrState] = getMinMaxPropsOrState({
58
44
  axis,
59
- maxValues: [zoomStateX === null || zoomStateX === void 0 ? void 0 : zoomStateX[1], rangeSliderState === null || rangeSliderState === void 0 ? void 0 : rangeSliderState.max],
60
- minValues: [zoomStateX === null || zoomStateX === void 0 ? void 0 : zoomStateX[0], rangeSliderState === null || rangeSliderState === void 0 ? void 0 : rangeSliderState.min],
45
+ maxValues: [effectiveXMax],
46
+ minValues: [effectiveXMin],
61
47
  });
62
48
  const xType = get(axis, 'type', DEFAULT_AXIS_TYPE);
63
49
  const hasZoomX = Boolean(zoomStateX);
64
- let xCategories = get(axis, 'categories');
65
- if (rangeSliderState && xCategories) {
66
- xCategories = getAxisCategories({
67
- categories: xCategories,
68
- min: rangeSliderState.min,
69
- max: rangeSliderState.max,
70
- order: axis.order,
71
- });
72
- }
50
+ const xCategories = get(axis, 'categories');
73
51
  const maxPadding = get(axis, 'maxPadding', 0);
74
52
  const xAxisMaxPadding = boundsWidth * maxPadding + calculateXAxisPadding(series);
75
53
  const range = getXScaleRange({
@@ -193,12 +171,11 @@ export function createXScale(args) {
193
171
  !isPointDomain
194
172
  ? xMinPropsOrState
195
173
  : xMinTimestamp;
196
- const xMax = getXMaxDomainResult({
197
- xMaxDomain: xMaxTimestamp,
198
- xMaxProps: get(axis, 'max'),
199
- xMaxRangeSlider: rangeSliderState === null || rangeSliderState === void 0 ? void 0 : rangeSliderState.max,
200
- xMaxZoom: zoomStateX === null || zoomStateX === void 0 ? void 0 : zoomStateX[1],
201
- });
174
+ const xMax = typeof xMaxPropsOrState === 'number' &&
175
+ xMaxPropsOrState < xMaxTimestamp &&
176
+ !isPointDomain
177
+ ? xMaxPropsOrState
178
+ : xMaxTimestamp;
202
179
  domain = [xMin, xMax];
203
180
  const scale = scaleUtc().domain(domain).range(range);
204
181
  let offsetMin = 0;
@@ -13,7 +13,7 @@ export interface RangeSliderProps {
13
13
  boundsWidth: number;
14
14
  height: number;
15
15
  htmlLayout: HTMLElement | null;
16
- onUpdate: (nextRangeSliderState?: RangeSliderState, syncZoom?: boolean) => void;
16
+ onUpdate: (nextRangeSliderState?: RangeSliderState) => void;
17
17
  preparedChart: PreparedChart;
18
18
  preparedLegend: PreparedLegend | null;
19
19
  preparedRangeSlider: PreparedRangeSlider;
@@ -8,7 +8,8 @@ import { getDefaultColorStops, getDomainForContinuousColorScale, getLabelsSize }
8
8
  export async function getPreparedLegend(args) {
9
9
  var _a, _b, _c, _d, _e, _f, _g;
10
10
  const { legend, series } = args;
11
- const enabled = Boolean(typeof (legend === null || legend === void 0 ? void 0 : legend.enabled) === 'boolean' ? legend === null || legend === void 0 ? void 0 : legend.enabled : series.length > 1);
11
+ const seriesWithEnabledLegend = series.filter((s) => { var _a; return ((_a = s.legend) === null || _a === void 0 ? void 0 : _a.enabled) !== false; });
12
+ const enabled = Boolean(typeof (legend === null || legend === void 0 ? void 0 : legend.enabled) === 'boolean' ? legend === null || legend === void 0 ? void 0 : legend.enabled : seriesWithEnabledLegend.length > 1);
12
13
  const defaultItemStyle = clone(legendDefaults.itemStyle);
13
14
  const itemStyle = get(legend, 'itemStyle');
14
15
  const computedItemStyle = merge(defaultItemStyle, itemStyle);
@@ -1,4 +1,4 @@
1
- import type { PreparedSeries, PreparedXAxis, PreparedYAxis } from '../../hooks';
1
+ import type { PreparedSeries, PreparedXAxis, PreparedYAxis, RangeSliderState } from '../../hooks';
2
2
  import type { ZoomState } from '../../hooks/useZoom/types';
3
3
  import type { ChartXAxis, ChartYAxis } from '../../types';
4
4
  export declare function getZoomedSeriesData(args: {
@@ -10,3 +10,4 @@ export declare function getZoomedSeriesData(args: {
10
10
  preparedSeries: PreparedSeries[];
11
11
  preparedShapesSeries: PreparedSeries[];
12
12
  };
13
+ export declare function getEffectiveXRange(zoomStateX: [number, number] | undefined, rangeSliderState: RangeSliderState | undefined): [number, number] | undefined;
@@ -123,3 +123,12 @@ export function getZoomedSeriesData(args) {
123
123
  preparedShapesSeries: zoomedShapesSeriesData,
124
124
  };
125
125
  }
126
+ export function getEffectiveXRange(zoomStateX, rangeSliderState) {
127
+ if (zoomStateX && rangeSliderState) {
128
+ return [
129
+ Math.max(zoomStateX[0], rangeSliderState.min),
130
+ Math.min(zoomStateX[1], rangeSliderState.max),
131
+ ];
132
+ }
133
+ return (zoomStateX !== null && zoomStateX !== void 0 ? zoomStateX : (rangeSliderState ? [rangeSliderState.min, rangeSliderState.max] : undefined));
134
+ }
@@ -32,7 +32,6 @@ export const AxisX = (props) => {
32
32
  .attr('class', b('title'))
33
33
  .append('text')
34
34
  .attr('text-anchor', 'start')
35
- .style('dominant-baseline', 'text-after-edge')
36
35
  .style('transform', `translate(${preparedAxisData.title.x}px, ${preparedAxisData.title.y}px) rotate(${preparedAxisData.title.rotate}deg) translate(0px, ${preparedAxisData.title.offset}px)`)
37
36
  .attr('font-size', preparedAxisData.title.style.fontSize)
38
37
  .selectAll('tspan')