@gravity-ui/charts 1.34.0 → 1.34.1

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.
@@ -6,6 +6,7 @@ import { getPreparedRangeSlider } from '../../hooks/useAxis/range-slider';
6
6
  import { getPreparedChart } from '../../hooks/useChartOptions/chart';
7
7
  import { getPreparedTitle } from '../../hooks/useChartOptions/title';
8
8
  import { getPreparedTooltip } from '../../hooks/useChartOptions/tooltip';
9
+ import { getClipPathIdByBounds } from '../../hooks/useShapes/utils';
9
10
  import { EventType, block, getDispatcher, isBandScale } from '../../utils';
10
11
  import { AxisX } from '../AxisX/AxisX';
11
12
  import { prepareXAxisData } from '../AxisX/prepare-axis-data';
@@ -214,8 +215,10 @@ export const ChartInner = (props) => {
214
215
  }, [height, areShapesReady, onReady, width]);
215
216
  const chartContent = (React.createElement(React.Fragment, null,
216
217
  React.createElement("defs", null,
217
- React.createElement("clipPath", { id: clipPathId },
218
- React.createElement("rect", { x: 0, y: 0, width: boundsWidth, height: boundsHeight }))),
218
+ React.createElement("clipPath", { id: getClipPathIdByBounds({ clipPathId }) },
219
+ React.createElement("rect", { x: 0, y: 0, width: boundsWidth, height: boundsHeight })),
220
+ React.createElement("clipPath", { id: getClipPathIdByBounds({ clipPathId, bounds: 'horizontal' }) },
221
+ React.createElement("rect", { x: 0, y: -boundsHeight, width: boundsWidth, height: boundsHeight * 3 }))),
219
222
  preparedTitle && React.createElement(Title, Object.assign({}, preparedTitle, { chartWidth: width })),
220
223
  React.createElement("g", { transform: `translate(0, ${boundsOffsetTop})` }, preparedSplit.plots.map((plot, index) => {
221
224
  return React.createElement(PlotTitle, { key: `plot-${index}`, title: plot.title });
@@ -126,6 +126,7 @@ export function useChartInnerProps(props) {
126
126
  htmlLayout,
127
127
  clipPathId,
128
128
  isOutsideBounds,
129
+ zoomState,
129
130
  });
130
131
  const handleAttemptToSetZoomState = React.useCallback((nextZoomState) => {
131
132
  const { preparedSeries: nextZoomedSeriesData } = getZoomedSeriesData({
@@ -59,6 +59,7 @@ function RangeSliderComponent(props, forwardedRef) {
59
59
  brushOptions: brush,
60
60
  node: ref.current,
61
61
  onBrushEnd,
62
+ preventNullSelection: true,
62
63
  selection,
63
64
  type: 'x',
64
65
  });
@@ -1,11 +1,11 @@
1
1
  import React from 'react';
2
- import { brush, brushX, brushY, select } from 'd3';
2
+ import { brush, brushX, brushY, pointer, select } from 'd3';
3
3
  import { block } from '../../utils';
4
- import { getNormalizedSelection, setBrushBorder, setBrushHandles } from './utils';
4
+ import { getDefaultSelection, getNormalizedSelection, setBrushBorder, setBrushHandles, } from './utils';
5
5
  import './styles.css';
6
6
  const b = block('brush');
7
7
  export function useBrush(props) {
8
- const { areas, brushOptions, disabled, node, selection, type, onBrushStart, onBrush, onBrushEnd, } = props;
8
+ const { areas, brushOptions, disabled, node, preventNullSelection = false, selection, type, onBrushStart, onBrush, onBrushEnd, } = props;
9
9
  React.useEffect(() => {
10
10
  if (!node || !areas.length || disabled) {
11
11
  return () => { };
@@ -103,7 +103,15 @@ export function useBrush(props) {
103
103
  });
104
104
  }
105
105
  if (event.sourceEvent) {
106
- onBrushEnd === null || onBrushEnd === void 0 ? void 0 : onBrushEnd.call(this, instance, event.selection);
106
+ let resultSelection = event.selection;
107
+ if (preventNullSelection && !resultSelection) {
108
+ const [pointerPositionX] = pointer(event, this);
109
+ resultSelection = getDefaultSelection({
110
+ brushWidth,
111
+ pointerPositionX,
112
+ });
113
+ }
114
+ onBrushEnd === null || onBrushEnd === void 0 ? void 0 : onBrushEnd.call(this, instance, resultSelection);
107
115
  }
108
116
  });
109
117
  groupSelection.call(instance);
@@ -129,5 +137,16 @@ export function useBrush(props) {
129
137
  groupSelection === null || groupSelection === void 0 ? void 0 : groupSelection.remove();
130
138
  });
131
139
  };
132
- }, [areas, brushOptions, disabled, node, selection, type, onBrushStart, onBrush, onBrushEnd]);
140
+ }, [
141
+ areas,
142
+ brushOptions,
143
+ disabled,
144
+ node,
145
+ preventNullSelection,
146
+ selection,
147
+ type,
148
+ onBrushStart,
149
+ onBrush,
150
+ onBrushEnd,
151
+ ]);
133
152
  }
@@ -15,6 +15,7 @@ export interface BrushArea {
15
15
  export interface UseBrushProps {
16
16
  areas: BrushArea[];
17
17
  node: SVGGElement | null;
18
+ preventNullSelection?: boolean;
18
19
  brushOptions?: DeepRequired<ChartBrush>;
19
20
  disabled?: boolean;
20
21
  onBrush?: (this: SVGGElement, brushInstance: BrushBehavior<unknown>, selection: BrushSelection) => void;
@@ -17,3 +17,7 @@ export declare function getNormalizedSelection(args: {
17
17
  selection: BrushSelection;
18
18
  width: number;
19
19
  }): [number, number] | [[number, number], [number, number]];
20
+ export declare function getDefaultSelection(args: {
21
+ brushWidth: number;
22
+ pointerPositionX: number;
23
+ }): BrushSelection;
@@ -170,3 +170,7 @@ export function getNormalizedSelection(args) {
170
170
  }
171
171
  return resultSelection;
172
172
  }
173
+ export function getDefaultSelection(args) {
174
+ const { brushWidth, pointerPositionX } = args;
175
+ return pointerPositionX < 0 ? [0, 1] : [brushWidth - 1, brushWidth];
176
+ }
@@ -5,6 +5,7 @@ import type { PreparedXAxis, PreparedYAxis } from '../useAxis/types';
5
5
  import type { ChartScale } from '../useAxisScales/types';
6
6
  import type { PreparedSeries, PreparedSeriesOptions } from '../useSeries/types';
7
7
  import type { PreparedSplit } from '../useSplit/types';
8
+ import type { ZoomState } from '../useZoom/types';
8
9
  import type { PreparedAreaData } from './area/types';
9
10
  import type { PreparedBarXData } from './bar-x';
10
11
  import type { PreparedBarYData } from './bar-y/types';
@@ -37,6 +38,7 @@ type Args = {
37
38
  isRangeSlider?: boolean;
38
39
  xScale?: ChartScale;
39
40
  yScale?: (ChartScale | undefined)[];
41
+ zoomState?: Partial<ZoomState>;
40
42
  };
41
43
  export declare const useShapes: (args: Args) => {
42
44
  shapes: React.ReactElement<any, string | React.JSXElementConstructor<any>>[];
@@ -20,6 +20,7 @@ import { prepareSankeyData } from './sankey/prepare-data';
20
20
  import { ScatterSeriesShape, prepareScatterData } from './scatter';
21
21
  import { TreemapSeriesShape } from './treemap';
22
22
  import { prepareTreemapData } from './treemap/prepare-data';
23
+ import { getSeriesClipPathId } from './utils';
23
24
  import { WaterfallSeriesShapes, prepareWaterfallData } from './waterfall';
24
25
  import './styles.css';
25
26
  function IS_OUTSIDE_BOUNDS() {
@@ -30,7 +31,7 @@ function shouldUseClipPathId(seriesType, clipPathBySeriesType) {
30
31
  return (_a = clipPathBySeriesType === null || clipPathBySeriesType === void 0 ? void 0 : clipPathBySeriesType[seriesType]) !== null && _a !== void 0 ? _a : true;
31
32
  }
32
33
  export const useShapes = (args) => {
33
- const { boundsWidth, boundsHeight, clipPathId, clipPathBySeriesType, dispatcher, htmlLayout, isOutsideBounds = IS_OUTSIDE_BOUNDS, isRangeSlider, series, seriesOptions, split, xAxis, xScale, yAxis, yScale, } = args;
34
+ const { boundsWidth, boundsHeight, clipPathId, clipPathBySeriesType, dispatcher, htmlLayout, isOutsideBounds = IS_OUTSIDE_BOUNDS, isRangeSlider, series, seriesOptions, split, xAxis, xScale, yAxis, yScale, zoomState, } = args;
34
35
  const [shapesElemens, setShapesElements] = React.useState([]);
35
36
  const [shapesElemensData, setShapesElemensData] = React.useState([]);
36
37
  const countedRef = React.useRef(0);
@@ -109,7 +110,12 @@ export const useShapes = (args) => {
109
110
  isOutsideBounds,
110
111
  isRangeSlider,
111
112
  });
112
- shapes.push(React.createElement(LineSeriesShapes, { key: SERIES_TYPE.Line, dispatcher: dispatcher, seriesOptions: seriesOptions, preparedData: preparedData, htmlLayout: htmlLayout, clipPathId: clipPathId }));
113
+ const resultClipPathId = getSeriesClipPathId({
114
+ clipPathId,
115
+ yAxis,
116
+ zoomState,
117
+ });
118
+ shapes.push(React.createElement(LineSeriesShapes, { key: SERIES_TYPE.Line, dispatcher: dispatcher, seriesOptions: seriesOptions, preparedData: preparedData, htmlLayout: htmlLayout, clipPathId: resultClipPathId }));
113
119
  shapesData.push(...preparedData);
114
120
  }
115
121
  break;
@@ -243,6 +249,7 @@ export const useShapes = (args) => {
243
249
  xScale,
244
250
  yAxis,
245
251
  yScale,
252
+ zoomState,
246
253
  ]);
247
254
  return { shapes: shapesElemens, shapesData: shapesElemensData };
248
255
  };
@@ -2,6 +2,7 @@ import type { BaseType } from 'd3';
2
2
  import type { BasicInactiveState } from '../../types';
3
3
  import type { PreparedXAxis, PreparedYAxis } from '../useAxis/types';
4
4
  import type { ChartScale } from '../useAxisScales/types';
5
+ import type { ZoomState } from '../useZoom/types';
5
6
  export declare function getXValue(args: {
6
7
  point: {
7
8
  x?: number | string | null;
@@ -46,3 +47,12 @@ export declare function getRectBorderPath(args: {
46
47
  borderWidth: number;
47
48
  borderRadius?: number | number[];
48
49
  }): string;
50
+ export declare function getClipPathIdByBounds(args: {
51
+ clipPathId: string;
52
+ bounds?: 'horizontal';
53
+ }): string;
54
+ export declare function getSeriesClipPathId(args: {
55
+ clipPathId: string;
56
+ yAxis: PreparedYAxis[];
57
+ zoomState?: Partial<ZoomState>;
58
+ }): string;
@@ -131,3 +131,18 @@ export function getRectBorderPath(args) {
131
131
  }).toString();
132
132
  return `${outerPath} ${innerPath}`;
133
133
  }
134
+ export function getClipPathIdByBounds(args) {
135
+ const { bounds, clipPathId } = args;
136
+ return bounds ? `${clipPathId}-${bounds}` : clipPathId;
137
+ }
138
+ export function getSeriesClipPathId(args) {
139
+ const { clipPathId, yAxis, zoomState } = args;
140
+ const hasMinOrMax = yAxis.some((axis) => {
141
+ return typeof (axis === null || axis === void 0 ? void 0 : axis.min) === 'number' || typeof (axis === null || axis === void 0 ? void 0 : axis.max) === 'number';
142
+ });
143
+ const hasZoom = zoomState && Object.keys(zoomState).length > 0;
144
+ if (!hasZoom && !hasMinOrMax) {
145
+ return getClipPathIdByBounds({ clipPathId, bounds: 'horizontal' });
146
+ }
147
+ return getClipPathIdByBounds({ clipPathId });
148
+ }
@@ -6,6 +6,7 @@ import { getPreparedRangeSlider } from '../../hooks/useAxis/range-slider';
6
6
  import { getPreparedChart } from '../../hooks/useChartOptions/chart';
7
7
  import { getPreparedTitle } from '../../hooks/useChartOptions/title';
8
8
  import { getPreparedTooltip } from '../../hooks/useChartOptions/tooltip';
9
+ import { getClipPathIdByBounds } from '../../hooks/useShapes/utils';
9
10
  import { EventType, block, getDispatcher, isBandScale } from '../../utils';
10
11
  import { AxisX } from '../AxisX/AxisX';
11
12
  import { prepareXAxisData } from '../AxisX/prepare-axis-data';
@@ -214,8 +215,10 @@ export const ChartInner = (props) => {
214
215
  }, [height, areShapesReady, onReady, width]);
215
216
  const chartContent = (React.createElement(React.Fragment, null,
216
217
  React.createElement("defs", null,
217
- React.createElement("clipPath", { id: clipPathId },
218
- React.createElement("rect", { x: 0, y: 0, width: boundsWidth, height: boundsHeight }))),
218
+ React.createElement("clipPath", { id: getClipPathIdByBounds({ clipPathId }) },
219
+ React.createElement("rect", { x: 0, y: 0, width: boundsWidth, height: boundsHeight })),
220
+ React.createElement("clipPath", { id: getClipPathIdByBounds({ clipPathId, bounds: 'horizontal' }) },
221
+ React.createElement("rect", { x: 0, y: -boundsHeight, width: boundsWidth, height: boundsHeight * 3 }))),
219
222
  preparedTitle && React.createElement(Title, Object.assign({}, preparedTitle, { chartWidth: width })),
220
223
  React.createElement("g", { transform: `translate(0, ${boundsOffsetTop})` }, preparedSplit.plots.map((plot, index) => {
221
224
  return React.createElement(PlotTitle, { key: `plot-${index}`, title: plot.title });
@@ -126,6 +126,7 @@ export function useChartInnerProps(props) {
126
126
  htmlLayout,
127
127
  clipPathId,
128
128
  isOutsideBounds,
129
+ zoomState,
129
130
  });
130
131
  const handleAttemptToSetZoomState = React.useCallback((nextZoomState) => {
131
132
  const { preparedSeries: nextZoomedSeriesData } = getZoomedSeriesData({
@@ -59,6 +59,7 @@ function RangeSliderComponent(props, forwardedRef) {
59
59
  brushOptions: brush,
60
60
  node: ref.current,
61
61
  onBrushEnd,
62
+ preventNullSelection: true,
62
63
  selection,
63
64
  type: 'x',
64
65
  });
@@ -1,11 +1,11 @@
1
1
  import React from 'react';
2
- import { brush, brushX, brushY, select } from 'd3';
2
+ import { brush, brushX, brushY, pointer, select } from 'd3';
3
3
  import { block } from '../../utils';
4
- import { getNormalizedSelection, setBrushBorder, setBrushHandles } from './utils';
4
+ import { getDefaultSelection, getNormalizedSelection, setBrushBorder, setBrushHandles, } from './utils';
5
5
  import './styles.css';
6
6
  const b = block('brush');
7
7
  export function useBrush(props) {
8
- const { areas, brushOptions, disabled, node, selection, type, onBrushStart, onBrush, onBrushEnd, } = props;
8
+ const { areas, brushOptions, disabled, node, preventNullSelection = false, selection, type, onBrushStart, onBrush, onBrushEnd, } = props;
9
9
  React.useEffect(() => {
10
10
  if (!node || !areas.length || disabled) {
11
11
  return () => { };
@@ -103,7 +103,15 @@ export function useBrush(props) {
103
103
  });
104
104
  }
105
105
  if (event.sourceEvent) {
106
- onBrushEnd === null || onBrushEnd === void 0 ? void 0 : onBrushEnd.call(this, instance, event.selection);
106
+ let resultSelection = event.selection;
107
+ if (preventNullSelection && !resultSelection) {
108
+ const [pointerPositionX] = pointer(event, this);
109
+ resultSelection = getDefaultSelection({
110
+ brushWidth,
111
+ pointerPositionX,
112
+ });
113
+ }
114
+ onBrushEnd === null || onBrushEnd === void 0 ? void 0 : onBrushEnd.call(this, instance, resultSelection);
107
115
  }
108
116
  });
109
117
  groupSelection.call(instance);
@@ -129,5 +137,16 @@ export function useBrush(props) {
129
137
  groupSelection === null || groupSelection === void 0 ? void 0 : groupSelection.remove();
130
138
  });
131
139
  };
132
- }, [areas, brushOptions, disabled, node, selection, type, onBrushStart, onBrush, onBrushEnd]);
140
+ }, [
141
+ areas,
142
+ brushOptions,
143
+ disabled,
144
+ node,
145
+ preventNullSelection,
146
+ selection,
147
+ type,
148
+ onBrushStart,
149
+ onBrush,
150
+ onBrushEnd,
151
+ ]);
133
152
  }
@@ -15,6 +15,7 @@ export interface BrushArea {
15
15
  export interface UseBrushProps {
16
16
  areas: BrushArea[];
17
17
  node: SVGGElement | null;
18
+ preventNullSelection?: boolean;
18
19
  brushOptions?: DeepRequired<ChartBrush>;
19
20
  disabled?: boolean;
20
21
  onBrush?: (this: SVGGElement, brushInstance: BrushBehavior<unknown>, selection: BrushSelection) => void;
@@ -17,3 +17,7 @@ export declare function getNormalizedSelection(args: {
17
17
  selection: BrushSelection;
18
18
  width: number;
19
19
  }): [number, number] | [[number, number], [number, number]];
20
+ export declare function getDefaultSelection(args: {
21
+ brushWidth: number;
22
+ pointerPositionX: number;
23
+ }): BrushSelection;
@@ -170,3 +170,7 @@ export function getNormalizedSelection(args) {
170
170
  }
171
171
  return resultSelection;
172
172
  }
173
+ export function getDefaultSelection(args) {
174
+ const { brushWidth, pointerPositionX } = args;
175
+ return pointerPositionX < 0 ? [0, 1] : [brushWidth - 1, brushWidth];
176
+ }
@@ -5,6 +5,7 @@ import type { PreparedXAxis, PreparedYAxis } from '../useAxis/types';
5
5
  import type { ChartScale } from '../useAxisScales/types';
6
6
  import type { PreparedSeries, PreparedSeriesOptions } from '../useSeries/types';
7
7
  import type { PreparedSplit } from '../useSplit/types';
8
+ import type { ZoomState } from '../useZoom/types';
8
9
  import type { PreparedAreaData } from './area/types';
9
10
  import type { PreparedBarXData } from './bar-x';
10
11
  import type { PreparedBarYData } from './bar-y/types';
@@ -37,6 +38,7 @@ type Args = {
37
38
  isRangeSlider?: boolean;
38
39
  xScale?: ChartScale;
39
40
  yScale?: (ChartScale | undefined)[];
41
+ zoomState?: Partial<ZoomState>;
40
42
  };
41
43
  export declare const useShapes: (args: Args) => {
42
44
  shapes: React.ReactElement<any, string | React.JSXElementConstructor<any>>[];
@@ -20,6 +20,7 @@ import { prepareSankeyData } from './sankey/prepare-data';
20
20
  import { ScatterSeriesShape, prepareScatterData } from './scatter';
21
21
  import { TreemapSeriesShape } from './treemap';
22
22
  import { prepareTreemapData } from './treemap/prepare-data';
23
+ import { getSeriesClipPathId } from './utils';
23
24
  import { WaterfallSeriesShapes, prepareWaterfallData } from './waterfall';
24
25
  import './styles.css';
25
26
  function IS_OUTSIDE_BOUNDS() {
@@ -30,7 +31,7 @@ function shouldUseClipPathId(seriesType, clipPathBySeriesType) {
30
31
  return (_a = clipPathBySeriesType === null || clipPathBySeriesType === void 0 ? void 0 : clipPathBySeriesType[seriesType]) !== null && _a !== void 0 ? _a : true;
31
32
  }
32
33
  export const useShapes = (args) => {
33
- const { boundsWidth, boundsHeight, clipPathId, clipPathBySeriesType, dispatcher, htmlLayout, isOutsideBounds = IS_OUTSIDE_BOUNDS, isRangeSlider, series, seriesOptions, split, xAxis, xScale, yAxis, yScale, } = args;
34
+ const { boundsWidth, boundsHeight, clipPathId, clipPathBySeriesType, dispatcher, htmlLayout, isOutsideBounds = IS_OUTSIDE_BOUNDS, isRangeSlider, series, seriesOptions, split, xAxis, xScale, yAxis, yScale, zoomState, } = args;
34
35
  const [shapesElemens, setShapesElements] = React.useState([]);
35
36
  const [shapesElemensData, setShapesElemensData] = React.useState([]);
36
37
  const countedRef = React.useRef(0);
@@ -109,7 +110,12 @@ export const useShapes = (args) => {
109
110
  isOutsideBounds,
110
111
  isRangeSlider,
111
112
  });
112
- shapes.push(React.createElement(LineSeriesShapes, { key: SERIES_TYPE.Line, dispatcher: dispatcher, seriesOptions: seriesOptions, preparedData: preparedData, htmlLayout: htmlLayout, clipPathId: clipPathId }));
113
+ const resultClipPathId = getSeriesClipPathId({
114
+ clipPathId,
115
+ yAxis,
116
+ zoomState,
117
+ });
118
+ shapes.push(React.createElement(LineSeriesShapes, { key: SERIES_TYPE.Line, dispatcher: dispatcher, seriesOptions: seriesOptions, preparedData: preparedData, htmlLayout: htmlLayout, clipPathId: resultClipPathId }));
113
119
  shapesData.push(...preparedData);
114
120
  }
115
121
  break;
@@ -243,6 +249,7 @@ export const useShapes = (args) => {
243
249
  xScale,
244
250
  yAxis,
245
251
  yScale,
252
+ zoomState,
246
253
  ]);
247
254
  return { shapes: shapesElemens, shapesData: shapesElemensData };
248
255
  };
@@ -2,6 +2,7 @@ import type { BaseType } from 'd3';
2
2
  import type { BasicInactiveState } from '../../types';
3
3
  import type { PreparedXAxis, PreparedYAxis } from '../useAxis/types';
4
4
  import type { ChartScale } from '../useAxisScales/types';
5
+ import type { ZoomState } from '../useZoom/types';
5
6
  export declare function getXValue(args: {
6
7
  point: {
7
8
  x?: number | string | null;
@@ -46,3 +47,12 @@ export declare function getRectBorderPath(args: {
46
47
  borderWidth: number;
47
48
  borderRadius?: number | number[];
48
49
  }): string;
50
+ export declare function getClipPathIdByBounds(args: {
51
+ clipPathId: string;
52
+ bounds?: 'horizontal';
53
+ }): string;
54
+ export declare function getSeriesClipPathId(args: {
55
+ clipPathId: string;
56
+ yAxis: PreparedYAxis[];
57
+ zoomState?: Partial<ZoomState>;
58
+ }): string;
@@ -131,3 +131,18 @@ export function getRectBorderPath(args) {
131
131
  }).toString();
132
132
  return `${outerPath} ${innerPath}`;
133
133
  }
134
+ export function getClipPathIdByBounds(args) {
135
+ const { bounds, clipPathId } = args;
136
+ return bounds ? `${clipPathId}-${bounds}` : clipPathId;
137
+ }
138
+ export function getSeriesClipPathId(args) {
139
+ const { clipPathId, yAxis, zoomState } = args;
140
+ const hasMinOrMax = yAxis.some((axis) => {
141
+ return typeof (axis === null || axis === void 0 ? void 0 : axis.min) === 'number' || typeof (axis === null || axis === void 0 ? void 0 : axis.max) === 'number';
142
+ });
143
+ const hasZoom = zoomState && Object.keys(zoomState).length > 0;
144
+ if (!hasZoom && !hasMinOrMax) {
145
+ return getClipPathIdByBounds({ clipPathId, bounds: 'horizontal' });
146
+ }
147
+ return getClipPathIdByBounds({ clipPathId });
148
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gravity-ui/charts",
3
- "version": "1.34.0",
3
+ "version": "1.34.1",
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",