@gravity-ui/charts 1.29.0 → 1.30.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.
Files changed (26) hide show
  1. package/README.md +19 -3
  2. package/dist/cjs/components/AxisX/prepare-axis-data.js +2 -2
  3. package/dist/cjs/components/ChartInner/useChartInnerHandlers.js +3 -1
  4. package/dist/cjs/components/Legend/index.js +13 -20
  5. package/dist/cjs/components/Tooltip/DefaultTooltipContent/Row.d.ts +1 -0
  6. package/dist/cjs/components/Tooltip/DefaultTooltipContent/Row.js +11 -2
  7. package/dist/cjs/components/Tooltip/DefaultTooltipContent/index.js +5 -3
  8. package/dist/cjs/components/Tooltip/DefaultTooltipContent/utils.d.ts +6 -0
  9. package/dist/cjs/components/Tooltip/DefaultTooltipContent/utils.js +18 -0
  10. package/dist/cjs/components/utils.d.ts +11 -0
  11. package/dist/cjs/components/utils.js +25 -1
  12. package/dist/cjs/hooks/useAxisScales/index.js +32 -8
  13. package/dist/cjs/types/chart/base.d.ts +6 -0
  14. package/dist/esm/components/AxisX/prepare-axis-data.js +2 -2
  15. package/dist/esm/components/ChartInner/useChartInnerHandlers.js +3 -1
  16. package/dist/esm/components/Legend/index.js +13 -20
  17. package/dist/esm/components/Tooltip/DefaultTooltipContent/Row.d.ts +1 -0
  18. package/dist/esm/components/Tooltip/DefaultTooltipContent/Row.js +11 -2
  19. package/dist/esm/components/Tooltip/DefaultTooltipContent/index.js +5 -3
  20. package/dist/esm/components/Tooltip/DefaultTooltipContent/utils.d.ts +6 -0
  21. package/dist/esm/components/Tooltip/DefaultTooltipContent/utils.js +18 -0
  22. package/dist/esm/components/utils.d.ts +11 -0
  23. package/dist/esm/components/utils.js +25 -1
  24. package/dist/esm/hooks/useAxisScales/index.js +32 -8
  25. package/dist/esm/types/chart/base.d.ts +6 -0
  26. package/package.json +2 -2
package/README.md CHANGED
@@ -1,15 +1,31 @@
1
- # Gravity UI Charts · [![npm package](https://img.shields.io/npm/v/@gravity-ui/charts)](https://www.npmjs.com/package/@gravity-ui/charts) [![CI](https://img.shields.io/github/actions/workflow/status/gravity-ui/charts/.github/workflows/ci.yml?label=CI&logo=github)](https://github.com/gravity-ui/charts/actions/workflows/ci.yml?query=branch:main) [![storybook](https://img.shields.io/badge/Storybook-deployed-ff4685)](https://preview.gravity-ui.com/charts/)
1
+ # Gravity UI Charts
2
2
 
3
- ## Install
3
+ A flexible JavaScript library for data visualization and chart rendering using React.
4
+
5
+ [![npm package](https://img.shields.io/npm/v/@gravity-ui/charts)](https://www.npmjs.com/package/@gravity-ui/charts) [![CI](https://img.shields.io/github/actions/workflow/status/gravity-ui/charts/.github/workflows/ci.yml?label=CI&logo=github)](https://github.com/gravity-ui/charts/actions/workflows/ci.yml?query=branch:main) [![storybook](https://img.shields.io/badge/Storybook-deployed-ff4685)](https://preview.gravity-ui.com/charts/)
6
+
7
+ ## Documentation
8
+
9
+ - [Overview](https://gravity-ui.github.io/charts/pages/overview.html)
10
+ - [API](https://gravity-ui.github.io/charts/pages/api/overview.html)
11
+ - [Guides](https://gravity-ui.github.io/charts/pages/guides/tooltip.html)
12
+
13
+ ## Get started
14
+
15
+ ### Install
4
16
 
5
17
  ```shell
6
18
  npm install @gravity-ui/uikit @gravity-ui/charts
7
19
  ```
8
20
 
9
- ## Development
21
+ ### Development
10
22
 
11
23
  To start the development server with storybook run the following:
12
24
 
13
25
  ```shell
14
26
  npm run start
15
27
  ```
28
+
29
+ ## Contributing
30
+
31
+ Please refer to the [contributing document](https://github.com/gravity-ui/charts/blob/main/CONTRIBUTING.md) if you wish to make pull requests.
@@ -251,7 +251,7 @@ export async function prepareXAxisData({ axis, yAxis, scale, boundsWidth, bounds
251
251
  plotBands.push({
252
252
  layerPlacement: plotBand.layerPlacement,
253
253
  x: Math.max(0, startPos),
254
- y: 0,
254
+ y: axisTop,
255
255
  width: plotBandWidth,
256
256
  height: axisHeight,
257
257
  color: plotBand.color,
@@ -294,7 +294,7 @@ export async function prepareXAxisData({ axis, yAxis, scale, boundsWidth, bounds
294
294
  plotLines.push({
295
295
  layerPlacement: plotLine.layerPlacement,
296
296
  x: 0,
297
- y: 0,
297
+ y: axisTop,
298
298
  width: axisWidth,
299
299
  color: plotLine.color,
300
300
  opacity: plotLine.opacity,
@@ -1,4 +1,5 @@
1
1
  import { pointer } from 'd3';
2
+ import get from 'lodash/get';
2
3
  import throttle from 'lodash/throttle';
3
4
  import { IS_TOUCH_ENABLED } from '../../constants';
4
5
  import { EventType } from '../../utils';
@@ -16,9 +17,10 @@ export function useChartInnerHandlers(props) {
16
17
  dispatcher.call(EventType.POINTERMOVE_CHART, {}, undefined, event);
17
18
  return;
18
19
  }
20
+ const shapesDataWithTooltipEnabled = shapesData.filter((d) => get(d, 'series.tooltip.enabled', true));
19
21
  const closest = getClosestPoints({
20
22
  position: [x, y],
21
- shapesData,
23
+ shapesData: shapesDataWithTooltipEnabled,
22
24
  boundsHeight,
23
25
  boundsWidth,
24
26
  });
@@ -1,9 +1,10 @@
1
1
  import React from 'react';
2
- import { line as lineGenerator, scaleLinear, select, symbol } from 'd3';
2
+ import { scaleLinear, select, symbol } from 'd3';
3
3
  import { CONTINUOUS_LEGEND_SIZE } from '../../constants';
4
4
  import { formatNumber } from '../../libs';
5
- import { block, createGradientRect, getContinuesColorFn, getLabelsSize, getLineDashArray, getSymbol, getUniqId, handleOverflowingText, } from '../../utils';
5
+ import { block, createGradientRect, getContinuesColorFn, getLabelsSize, getSymbol, getUniqId, handleOverflowingText, } from '../../utils';
6
6
  import { axisBottom } from '../../utils/chart/axis-generators';
7
+ import { appendLinePathElement } from '../utils';
7
8
  import './styles.css';
8
9
  const b = block('legend');
9
10
  const getLegendItemLeftPosition = (args) => {
@@ -64,9 +65,6 @@ const appendPaginator = (args) => {
64
65
  });
65
66
  paginationLine.attr('transform', transform);
66
67
  };
67
- const legendSymbolGenerator = lineGenerator()
68
- .x((d) => d.x)
69
- .y((d) => d.y);
70
68
  function renderLegendSymbol(args) {
71
69
  const { selection, legend, legendLineHeight } = args;
72
70
  const line = selection.data();
@@ -86,21 +84,16 @@ function renderLegendSymbol(args) {
86
84
  const color = d.visible ? d.color : '';
87
85
  switch (d.symbol.shape) {
88
86
  case 'path': {
89
- const y = legendLineHeight / 2;
90
- const points = [
91
- { x, y },
92
- { x: x + d.symbol.width, y },
93
- ];
94
- element
95
- .append('path')
96
- .attr('d', legendSymbolGenerator(points))
97
- .attr('fill', 'none')
98
- .attr('stroke-width', d.symbol.strokeWidth)
99
- .attr('class', className)
100
- .style('stroke', color);
101
- if (d.dashStyle) {
102
- element.attr('stroke-dasharray', getLineDashArray(d.dashStyle, d.symbol.strokeWidth));
103
- }
87
+ appendLinePathElement({
88
+ svgRootElement: element.node(),
89
+ x,
90
+ height: legendLineHeight,
91
+ width: d.symbol.width,
92
+ color,
93
+ className,
94
+ dashStyle: d.dashStyle,
95
+ lineWidth: d.symbol.strokeWidth,
96
+ });
104
97
  break;
105
98
  }
106
99
  case 'rect': {
@@ -4,6 +4,7 @@ export declare function Row(props: {
4
4
  active?: boolean;
5
5
  className?: string;
6
6
  color?: string;
7
+ colorSymbol?: React.ReactNode;
7
8
  striped?: boolean;
8
9
  style?: React.CSSProperties;
9
10
  value?: React.ReactNode;
@@ -2,9 +2,18 @@ import React from 'react';
2
2
  import { block } from '../../../utils';
3
3
  const b = block('tooltip');
4
4
  export function Row(props) {
5
- const { label, value, active, color, className, striped, style } = props;
5
+ const { label, value, active, color, colorSymbol, className, striped, style } = props;
6
+ const colorItem = React.useMemo(() => {
7
+ if (colorSymbol) {
8
+ return colorSymbol;
9
+ }
10
+ if (color) {
11
+ return React.createElement("div", { className: b('content-row-color'), style: { backgroundColor: color } });
12
+ }
13
+ return null;
14
+ }, [color, colorSymbol]);
6
15
  return (React.createElement("div", { className: b('content-row', { active, striped }, className), style: style },
7
- color && React.createElement("div", { className: b('content-row-color'), style: { backgroundColor: color } }),
16
+ colorItem,
8
17
  label,
9
18
  value && React.createElement("span", { className: b('content-row-value') }, value)));
10
19
  }
@@ -8,7 +8,7 @@ import { block, hasVerticalScrollbar } from '../../../utils';
8
8
  import { getFormattedValue } from '../../../utils/chart/format';
9
9
  import { Row } from './Row';
10
10
  import { RowWithAggregation } from './RowWithAggregation';
11
- import { getDefaultValueFormat, getHoveredValues, getMeasureValue, getPreparedAggregation, getXRowData, } from './utils';
11
+ import { getDefaultValueFormat, getHoveredValues, getMeasureValue, getPreparedAggregation, getTooltipRowColorSymbol, getXRowData, } from './utils';
12
12
  const b = block('tooltip');
13
13
  export const DefaultTooltipContent = ({ hovered, pinned, rowRenderer, totals, valueFormat, headerFormat, xAxis, yAxis, qa, }) => {
14
14
  var _a;
@@ -21,7 +21,7 @@ export const DefaultTooltipContent = ({ hovered, pinned, rowRenderer, totals, va
21
21
  const prevHoveredValues = usePrevious(hoveredValues);
22
22
  const visibleHovered = pinned || !visibleRows ? hovered : hovered.slice(0, visibleRows);
23
23
  const restHoveredValues = pinned || !visibleRows ? [] : hoveredValues.slice(visibleRows);
24
- const renderRow = ({ id, name, color, active, striped, value, formattedValue, }) => {
24
+ const renderRow = ({ id, name, color, active, striped, value, formattedValue, series, }) => {
25
25
  if (typeof rowRenderer === 'function') {
26
26
  return rowRenderer({
27
27
  id,
@@ -35,7 +35,8 @@ export const DefaultTooltipContent = ({ hovered, pinned, rowRenderer, totals, va
35
35
  hovered,
36
36
  });
37
37
  }
38
- return (React.createElement(Row, { key: id, active: active, color: color, label: React.createElement("span", { dangerouslySetInnerHTML: { __html: name } }), striped: striped, value: formattedValue }));
38
+ const colorSymbol = getTooltipRowColorSymbol({ series, color });
39
+ return (React.createElement(Row, { key: id, active: active, color: color, colorSymbol: colorSymbol ? (React.createElement("div", { dangerouslySetInnerHTML: { __html: colorSymbol.outerHTML } })) : undefined, label: React.createElement("span", { dangerouslySetInnerHTML: { __html: name } }), striped: striped, value: formattedValue }));
39
40
  };
40
41
  const formattedHeadValue = headerFormat
41
42
  ? getFormattedValue({
@@ -113,6 +114,7 @@ export const DefaultTooltipContent = ({ hovered, pinned, rowRenderer, totals, va
113
114
  striped,
114
115
  value: hoveredValues[i],
115
116
  formattedValue,
117
+ series,
116
118
  });
117
119
  }
118
120
  case 'waterfall': {
@@ -35,3 +35,9 @@ export declare function getPreparedAggregation(args: {
35
35
  xAxis?: ChartXAxis | null;
36
36
  yAxis?: ChartYAxis;
37
37
  }): ChartTooltipTotalsBuiltInAggregation | (() => ChartTooltipTotalsAggregationValue);
38
+ export declare function getTooltipRowColorSymbol({ series, color, height, width, }: {
39
+ color?: string;
40
+ series?: TooltipDataChunk['series'];
41
+ height?: number;
42
+ width?: number;
43
+ }): SVGSVGElement | null;
@@ -1,7 +1,9 @@
1
+ import { create } from 'd3-selection';
1
2
  import get from 'lodash/get';
2
3
  import { i18n } from '../../../i18n';
3
4
  import { getDataCategoryValue, getDefaultDateFormat } from '../../../utils';
4
5
  import { getFormattedValue } from '../../../utils/chart/format';
6
+ import { appendLinePathElement } from '../../utils';
5
7
  function getRowData(fieldName, data, axis) {
6
8
  switch (axis === null || axis === void 0 ? void 0 : axis.type) {
7
9
  case 'category': {
@@ -128,3 +130,19 @@ export function getPreparedAggregation(args) {
128
130
  }
129
131
  return 'sum';
130
132
  }
133
+ export function getTooltipRowColorSymbol({ series, color, height = 8, width = 16, }) {
134
+ if ((series === null || series === void 0 ? void 0 : series.type) === 'line') {
135
+ const colorSymbol = create('svg').attr('height', height).attr('width', width);
136
+ const g = colorSymbol.append('g');
137
+ appendLinePathElement({
138
+ svgRootElement: g.node(),
139
+ height,
140
+ width,
141
+ color,
142
+ dashStyle: get(series, 'dashStyle'),
143
+ lineWidth: get(series, 'lineWidth'),
144
+ });
145
+ return colorSymbol.node();
146
+ }
147
+ return null;
148
+ }
@@ -1,3 +1,4 @@
1
+ import type { DashStyle } from '../constants';
1
2
  import type { ChartScaleLinear, ChartScaleTime } from '../hooks';
2
3
  import type { ChartAxisRangeSlider } from '../types';
3
4
  export declare function getInitialRangeSliderState(args: {
@@ -7,3 +8,13 @@ export declare function getInitialRangeSliderState(args: {
7
8
  min: number;
8
9
  max: number;
9
10
  };
11
+ export declare function appendLinePathElement({ svgRootElement, height, width, x, lineWidth, dashStyle, className, color, }: {
12
+ svgRootElement: SVGGElement | null;
13
+ height: number;
14
+ width: number;
15
+ x?: number;
16
+ lineWidth?: number;
17
+ dashStyle?: DashStyle;
18
+ className?: string;
19
+ color?: string;
20
+ }): import("d3-selection").Selection<SVGGElement | null, unknown, null, undefined>;
@@ -1,5 +1,7 @@
1
1
  import { duration } from '@gravity-ui/date-utils';
2
- import { isTimeScale } from '../utils';
2
+ import { line as lineGenerator } from 'd3';
3
+ import { select } from 'd3-selection';
4
+ import { getLineDashArray, isTimeScale } from '../utils';
3
5
  export function getInitialRangeSliderState(args) {
4
6
  const { defaultRange, xScale } = args;
5
7
  let minRange;
@@ -32,3 +34,25 @@ export function getInitialRangeSliderState(args) {
32
34
  }
33
35
  return { min: minRange, max: maxRange };
34
36
  }
37
+ const legendSymbolGenerator = lineGenerator()
38
+ .x((d) => d.x)
39
+ .y((d) => d.y);
40
+ export function appendLinePathElement({ svgRootElement, height, width, x = 0, lineWidth = 1, dashStyle, className, color, }) {
41
+ const rootELementSelection = select(svgRootElement);
42
+ const y = height / 2;
43
+ const points = [
44
+ { x, y },
45
+ { x: x + width, y },
46
+ ];
47
+ const pathElement = rootELementSelection
48
+ .append('path')
49
+ .attr('d', legendSymbolGenerator(points))
50
+ .attr('fill', 'none')
51
+ .attr('stroke-width', lineWidth)
52
+ .attr('class', className !== null && className !== void 0 ? className : null)
53
+ .style('stroke', color !== null && color !== void 0 ? color : '');
54
+ if (dashStyle) {
55
+ pathElement.attr('stroke-dasharray', getLineDashArray(dashStyle, lineWidth));
56
+ }
57
+ return rootELementSelection;
58
+ }
@@ -116,8 +116,14 @@ export function createYScale(args) {
116
116
  offsetMax += bandWidth / 2;
117
117
  }
118
118
  }
119
- const domainOffsetMin = Math.abs(scale.invert(offsetMin) - scale.invert(0));
120
- const domainOffsetMax = Math.abs(scale.invert(offsetMax) - scale.invert(0));
119
+ const isMinSpecified = typeof get(axis, 'min') === 'number' && !zoomStateY;
120
+ const isMaxSpecified = typeof get(axis, 'max') === 'number' && !zoomStateY;
121
+ const domainOffsetMin = isMinSpecified
122
+ ? 0
123
+ : Math.abs(scale.invert(offsetMin) - scale.invert(0));
124
+ const domainOffsetMax = isMaxSpecified
125
+ ? 0
126
+ : Math.abs(scale.invert(offsetMax) - scale.invert(0));
121
127
  return scale.domain([yMin - domainOffsetMin, yMax + domainOffsetMax]);
122
128
  }
123
129
  break;
@@ -185,8 +191,14 @@ export function createYScale(args) {
185
191
  offsetMax += bandWidth / 2;
186
192
  }
187
193
  }
188
- const domainOffsetMin = Math.abs(scale.invert(offsetMin).getTime() - scale.invert(0).getTime());
189
- const domainOffsetMax = Math.abs(scale.invert(offsetMax).getTime() - scale.invert(0).getTime());
194
+ const isMinSpecified = typeof get(axis, 'min') === 'number' && !zoomStateY;
195
+ const isMaxSpecified = typeof get(axis, 'max') === 'number' && !zoomStateY;
196
+ const domainOffsetMin = isMinSpecified
197
+ ? 0
198
+ : Math.abs(scale.invert(offsetMin).getTime() - scale.invert(0).getTime());
199
+ const domainOffsetMax = isMaxSpecified
200
+ ? 0
201
+ : Math.abs(scale.invert(offsetMax).getTime() - scale.invert(0).getTime());
190
202
  return scale.domain([yMin - domainOffsetMin, yMax + domainOffsetMax]);
191
203
  }
192
204
  }
@@ -307,8 +319,14 @@ export function createXScale(args) {
307
319
  offsetMax += bandWidth / 2;
308
320
  }
309
321
  }
310
- const domainOffsetMin = Math.abs(scale.invert(offsetMin) - scale.invert(0));
311
- const domainOffsetMax = Math.abs(scale.invert(offsetMax) - scale.invert(0));
322
+ const isMinSpecified = typeof get(axis, 'min') === 'number' && !zoomStateX;
323
+ const isMaxSpecified = typeof get(axis, 'max') === 'number' && !zoomStateX;
324
+ const domainOffsetMin = isMinSpecified
325
+ ? 0
326
+ : Math.abs(scale.invert(offsetMin) - scale.invert(0));
327
+ const domainOffsetMax = isMaxSpecified
328
+ ? 0
329
+ : Math.abs(scale.invert(offsetMax) - scale.invert(0));
312
330
  // 10 is the default value for the number of ticks. Here, to preserve the appearance of a series with a small number of points
313
331
  const nicedDomain = scale.copy().nice(Math.max(10, domainData.length)).domain();
314
332
  scale.domain([xMin - domainOffsetMin, xMax + domainOffsetMax]);
@@ -374,8 +392,14 @@ export function createXScale(args) {
374
392
  offsetMax += bandWidth / 2;
375
393
  }
376
394
  }
377
- const domainOffsetMin = Math.abs(scale.invert(offsetMin).getTime() - scale.invert(0).getTime());
378
- const domainOffsetMax = Math.abs(scale.invert(offsetMax).getTime() - scale.invert(0).getTime());
395
+ const isMinSpecified = typeof get(axis, 'min') === 'number' && !zoomStateX;
396
+ const isMaxSpecified = typeof get(axis, 'max') === 'number' && !zoomStateX;
397
+ const domainOffsetMin = isMinSpecified
398
+ ? 0
399
+ : Math.abs(scale.invert(offsetMin).getTime() - scale.invert(0).getTime());
400
+ const domainOffsetMax = isMaxSpecified
401
+ ? 0
402
+ : Math.abs(scale.invert(offsetMax).getTime() - scale.invert(0).getTime());
379
403
  // 10 is the default value for the number of ticks. Here, to preserve the appearance of a series with a small number of points
380
404
  const nicedDomain = scale.copy().nice(Math.max(10, domainData.length)).domain();
381
405
  scale.domain([xMin - domainOffsetMin, xMax + domainOffsetMax]);
@@ -56,6 +56,12 @@ export interface BaseSeries {
56
56
  tooltip?: {
57
57
  /** Formatting settings for tooltip value. */
58
58
  valueFormat?: ValueFormat;
59
+ /**
60
+ * Enable or disable the visibility of this series in the tooltip.
61
+ *
62
+ * @default true
63
+ */
64
+ enabled?: boolean;
59
65
  };
60
66
  }
61
67
  export interface BaseSeriesData<T = MeaningfulAny> {
@@ -251,7 +251,7 @@ export async function prepareXAxisData({ axis, yAxis, scale, boundsWidth, bounds
251
251
  plotBands.push({
252
252
  layerPlacement: plotBand.layerPlacement,
253
253
  x: Math.max(0, startPos),
254
- y: 0,
254
+ y: axisTop,
255
255
  width: plotBandWidth,
256
256
  height: axisHeight,
257
257
  color: plotBand.color,
@@ -294,7 +294,7 @@ export async function prepareXAxisData({ axis, yAxis, scale, boundsWidth, bounds
294
294
  plotLines.push({
295
295
  layerPlacement: plotLine.layerPlacement,
296
296
  x: 0,
297
- y: 0,
297
+ y: axisTop,
298
298
  width: axisWidth,
299
299
  color: plotLine.color,
300
300
  opacity: plotLine.opacity,
@@ -1,4 +1,5 @@
1
1
  import { pointer } from 'd3';
2
+ import get from 'lodash/get';
2
3
  import throttle from 'lodash/throttle';
3
4
  import { IS_TOUCH_ENABLED } from '../../constants';
4
5
  import { EventType } from '../../utils';
@@ -16,9 +17,10 @@ export function useChartInnerHandlers(props) {
16
17
  dispatcher.call(EventType.POINTERMOVE_CHART, {}, undefined, event);
17
18
  return;
18
19
  }
20
+ const shapesDataWithTooltipEnabled = shapesData.filter((d) => get(d, 'series.tooltip.enabled', true));
19
21
  const closest = getClosestPoints({
20
22
  position: [x, y],
21
- shapesData,
23
+ shapesData: shapesDataWithTooltipEnabled,
22
24
  boundsHeight,
23
25
  boundsWidth,
24
26
  });
@@ -1,9 +1,10 @@
1
1
  import React from 'react';
2
- import { line as lineGenerator, scaleLinear, select, symbol } from 'd3';
2
+ import { scaleLinear, select, symbol } from 'd3';
3
3
  import { CONTINUOUS_LEGEND_SIZE } from '../../constants';
4
4
  import { formatNumber } from '../../libs';
5
- import { block, createGradientRect, getContinuesColorFn, getLabelsSize, getLineDashArray, getSymbol, getUniqId, handleOverflowingText, } from '../../utils';
5
+ import { block, createGradientRect, getContinuesColorFn, getLabelsSize, getSymbol, getUniqId, handleOverflowingText, } from '../../utils';
6
6
  import { axisBottom } from '../../utils/chart/axis-generators';
7
+ import { appendLinePathElement } from '../utils';
7
8
  import './styles.css';
8
9
  const b = block('legend');
9
10
  const getLegendItemLeftPosition = (args) => {
@@ -64,9 +65,6 @@ const appendPaginator = (args) => {
64
65
  });
65
66
  paginationLine.attr('transform', transform);
66
67
  };
67
- const legendSymbolGenerator = lineGenerator()
68
- .x((d) => d.x)
69
- .y((d) => d.y);
70
68
  function renderLegendSymbol(args) {
71
69
  const { selection, legend, legendLineHeight } = args;
72
70
  const line = selection.data();
@@ -86,21 +84,16 @@ function renderLegendSymbol(args) {
86
84
  const color = d.visible ? d.color : '';
87
85
  switch (d.symbol.shape) {
88
86
  case 'path': {
89
- const y = legendLineHeight / 2;
90
- const points = [
91
- { x, y },
92
- { x: x + d.symbol.width, y },
93
- ];
94
- element
95
- .append('path')
96
- .attr('d', legendSymbolGenerator(points))
97
- .attr('fill', 'none')
98
- .attr('stroke-width', d.symbol.strokeWidth)
99
- .attr('class', className)
100
- .style('stroke', color);
101
- if (d.dashStyle) {
102
- element.attr('stroke-dasharray', getLineDashArray(d.dashStyle, d.symbol.strokeWidth));
103
- }
87
+ appendLinePathElement({
88
+ svgRootElement: element.node(),
89
+ x,
90
+ height: legendLineHeight,
91
+ width: d.symbol.width,
92
+ color,
93
+ className,
94
+ dashStyle: d.dashStyle,
95
+ lineWidth: d.symbol.strokeWidth,
96
+ });
104
97
  break;
105
98
  }
106
99
  case 'rect': {
@@ -4,6 +4,7 @@ export declare function Row(props: {
4
4
  active?: boolean;
5
5
  className?: string;
6
6
  color?: string;
7
+ colorSymbol?: React.ReactNode;
7
8
  striped?: boolean;
8
9
  style?: React.CSSProperties;
9
10
  value?: React.ReactNode;
@@ -2,9 +2,18 @@ import React from 'react';
2
2
  import { block } from '../../../utils';
3
3
  const b = block('tooltip');
4
4
  export function Row(props) {
5
- const { label, value, active, color, className, striped, style } = props;
5
+ const { label, value, active, color, colorSymbol, className, striped, style } = props;
6
+ const colorItem = React.useMemo(() => {
7
+ if (colorSymbol) {
8
+ return colorSymbol;
9
+ }
10
+ if (color) {
11
+ return React.createElement("div", { className: b('content-row-color'), style: { backgroundColor: color } });
12
+ }
13
+ return null;
14
+ }, [color, colorSymbol]);
6
15
  return (React.createElement("div", { className: b('content-row', { active, striped }, className), style: style },
7
- color && React.createElement("div", { className: b('content-row-color'), style: { backgroundColor: color } }),
16
+ colorItem,
8
17
  label,
9
18
  value && React.createElement("span", { className: b('content-row-value') }, value)));
10
19
  }
@@ -8,7 +8,7 @@ import { block, hasVerticalScrollbar } from '../../../utils';
8
8
  import { getFormattedValue } from '../../../utils/chart/format';
9
9
  import { Row } from './Row';
10
10
  import { RowWithAggregation } from './RowWithAggregation';
11
- import { getDefaultValueFormat, getHoveredValues, getMeasureValue, getPreparedAggregation, getXRowData, } from './utils';
11
+ import { getDefaultValueFormat, getHoveredValues, getMeasureValue, getPreparedAggregation, getTooltipRowColorSymbol, getXRowData, } from './utils';
12
12
  const b = block('tooltip');
13
13
  export const DefaultTooltipContent = ({ hovered, pinned, rowRenderer, totals, valueFormat, headerFormat, xAxis, yAxis, qa, }) => {
14
14
  var _a;
@@ -21,7 +21,7 @@ export const DefaultTooltipContent = ({ hovered, pinned, rowRenderer, totals, va
21
21
  const prevHoveredValues = usePrevious(hoveredValues);
22
22
  const visibleHovered = pinned || !visibleRows ? hovered : hovered.slice(0, visibleRows);
23
23
  const restHoveredValues = pinned || !visibleRows ? [] : hoveredValues.slice(visibleRows);
24
- const renderRow = ({ id, name, color, active, striped, value, formattedValue, }) => {
24
+ const renderRow = ({ id, name, color, active, striped, value, formattedValue, series, }) => {
25
25
  if (typeof rowRenderer === 'function') {
26
26
  return rowRenderer({
27
27
  id,
@@ -35,7 +35,8 @@ export const DefaultTooltipContent = ({ hovered, pinned, rowRenderer, totals, va
35
35
  hovered,
36
36
  });
37
37
  }
38
- return (React.createElement(Row, { key: id, active: active, color: color, label: React.createElement("span", { dangerouslySetInnerHTML: { __html: name } }), striped: striped, value: formattedValue }));
38
+ const colorSymbol = getTooltipRowColorSymbol({ series, color });
39
+ return (React.createElement(Row, { key: id, active: active, color: color, colorSymbol: colorSymbol ? (React.createElement("div", { dangerouslySetInnerHTML: { __html: colorSymbol.outerHTML } })) : undefined, label: React.createElement("span", { dangerouslySetInnerHTML: { __html: name } }), striped: striped, value: formattedValue }));
39
40
  };
40
41
  const formattedHeadValue = headerFormat
41
42
  ? getFormattedValue({
@@ -113,6 +114,7 @@ export const DefaultTooltipContent = ({ hovered, pinned, rowRenderer, totals, va
113
114
  striped,
114
115
  value: hoveredValues[i],
115
116
  formattedValue,
117
+ series,
116
118
  });
117
119
  }
118
120
  case 'waterfall': {
@@ -35,3 +35,9 @@ export declare function getPreparedAggregation(args: {
35
35
  xAxis?: ChartXAxis | null;
36
36
  yAxis?: ChartYAxis;
37
37
  }): ChartTooltipTotalsBuiltInAggregation | (() => ChartTooltipTotalsAggregationValue);
38
+ export declare function getTooltipRowColorSymbol({ series, color, height, width, }: {
39
+ color?: string;
40
+ series?: TooltipDataChunk['series'];
41
+ height?: number;
42
+ width?: number;
43
+ }): SVGSVGElement | null;
@@ -1,7 +1,9 @@
1
+ import { create } from 'd3-selection';
1
2
  import get from 'lodash/get';
2
3
  import { i18n } from '../../../i18n';
3
4
  import { getDataCategoryValue, getDefaultDateFormat } from '../../../utils';
4
5
  import { getFormattedValue } from '../../../utils/chart/format';
6
+ import { appendLinePathElement } from '../../utils';
5
7
  function getRowData(fieldName, data, axis) {
6
8
  switch (axis === null || axis === void 0 ? void 0 : axis.type) {
7
9
  case 'category': {
@@ -128,3 +130,19 @@ export function getPreparedAggregation(args) {
128
130
  }
129
131
  return 'sum';
130
132
  }
133
+ export function getTooltipRowColorSymbol({ series, color, height = 8, width = 16, }) {
134
+ if ((series === null || series === void 0 ? void 0 : series.type) === 'line') {
135
+ const colorSymbol = create('svg').attr('height', height).attr('width', width);
136
+ const g = colorSymbol.append('g');
137
+ appendLinePathElement({
138
+ svgRootElement: g.node(),
139
+ height,
140
+ width,
141
+ color,
142
+ dashStyle: get(series, 'dashStyle'),
143
+ lineWidth: get(series, 'lineWidth'),
144
+ });
145
+ return colorSymbol.node();
146
+ }
147
+ return null;
148
+ }
@@ -1,3 +1,4 @@
1
+ import type { DashStyle } from '../constants';
1
2
  import type { ChartScaleLinear, ChartScaleTime } from '../hooks';
2
3
  import type { ChartAxisRangeSlider } from '../types';
3
4
  export declare function getInitialRangeSliderState(args: {
@@ -7,3 +8,13 @@ export declare function getInitialRangeSliderState(args: {
7
8
  min: number;
8
9
  max: number;
9
10
  };
11
+ export declare function appendLinePathElement({ svgRootElement, height, width, x, lineWidth, dashStyle, className, color, }: {
12
+ svgRootElement: SVGGElement | null;
13
+ height: number;
14
+ width: number;
15
+ x?: number;
16
+ lineWidth?: number;
17
+ dashStyle?: DashStyle;
18
+ className?: string;
19
+ color?: string;
20
+ }): import("d3-selection").Selection<SVGGElement | null, unknown, null, undefined>;
@@ -1,5 +1,7 @@
1
1
  import { duration } from '@gravity-ui/date-utils';
2
- import { isTimeScale } from '../utils';
2
+ import { line as lineGenerator } from 'd3';
3
+ import { select } from 'd3-selection';
4
+ import { getLineDashArray, isTimeScale } from '../utils';
3
5
  export function getInitialRangeSliderState(args) {
4
6
  const { defaultRange, xScale } = args;
5
7
  let minRange;
@@ -32,3 +34,25 @@ export function getInitialRangeSliderState(args) {
32
34
  }
33
35
  return { min: minRange, max: maxRange };
34
36
  }
37
+ const legendSymbolGenerator = lineGenerator()
38
+ .x((d) => d.x)
39
+ .y((d) => d.y);
40
+ export function appendLinePathElement({ svgRootElement, height, width, x = 0, lineWidth = 1, dashStyle, className, color, }) {
41
+ const rootELementSelection = select(svgRootElement);
42
+ const y = height / 2;
43
+ const points = [
44
+ { x, y },
45
+ { x: x + width, y },
46
+ ];
47
+ const pathElement = rootELementSelection
48
+ .append('path')
49
+ .attr('d', legendSymbolGenerator(points))
50
+ .attr('fill', 'none')
51
+ .attr('stroke-width', lineWidth)
52
+ .attr('class', className !== null && className !== void 0 ? className : null)
53
+ .style('stroke', color !== null && color !== void 0 ? color : '');
54
+ if (dashStyle) {
55
+ pathElement.attr('stroke-dasharray', getLineDashArray(dashStyle, lineWidth));
56
+ }
57
+ return rootELementSelection;
58
+ }
@@ -116,8 +116,14 @@ export function createYScale(args) {
116
116
  offsetMax += bandWidth / 2;
117
117
  }
118
118
  }
119
- const domainOffsetMin = Math.abs(scale.invert(offsetMin) - scale.invert(0));
120
- const domainOffsetMax = Math.abs(scale.invert(offsetMax) - scale.invert(0));
119
+ const isMinSpecified = typeof get(axis, 'min') === 'number' && !zoomStateY;
120
+ const isMaxSpecified = typeof get(axis, 'max') === 'number' && !zoomStateY;
121
+ const domainOffsetMin = isMinSpecified
122
+ ? 0
123
+ : Math.abs(scale.invert(offsetMin) - scale.invert(0));
124
+ const domainOffsetMax = isMaxSpecified
125
+ ? 0
126
+ : Math.abs(scale.invert(offsetMax) - scale.invert(0));
121
127
  return scale.domain([yMin - domainOffsetMin, yMax + domainOffsetMax]);
122
128
  }
123
129
  break;
@@ -185,8 +191,14 @@ export function createYScale(args) {
185
191
  offsetMax += bandWidth / 2;
186
192
  }
187
193
  }
188
- const domainOffsetMin = Math.abs(scale.invert(offsetMin).getTime() - scale.invert(0).getTime());
189
- const domainOffsetMax = Math.abs(scale.invert(offsetMax).getTime() - scale.invert(0).getTime());
194
+ const isMinSpecified = typeof get(axis, 'min') === 'number' && !zoomStateY;
195
+ const isMaxSpecified = typeof get(axis, 'max') === 'number' && !zoomStateY;
196
+ const domainOffsetMin = isMinSpecified
197
+ ? 0
198
+ : Math.abs(scale.invert(offsetMin).getTime() - scale.invert(0).getTime());
199
+ const domainOffsetMax = isMaxSpecified
200
+ ? 0
201
+ : Math.abs(scale.invert(offsetMax).getTime() - scale.invert(0).getTime());
190
202
  return scale.domain([yMin - domainOffsetMin, yMax + domainOffsetMax]);
191
203
  }
192
204
  }
@@ -307,8 +319,14 @@ export function createXScale(args) {
307
319
  offsetMax += bandWidth / 2;
308
320
  }
309
321
  }
310
- const domainOffsetMin = Math.abs(scale.invert(offsetMin) - scale.invert(0));
311
- const domainOffsetMax = Math.abs(scale.invert(offsetMax) - scale.invert(0));
322
+ const isMinSpecified = typeof get(axis, 'min') === 'number' && !zoomStateX;
323
+ const isMaxSpecified = typeof get(axis, 'max') === 'number' && !zoomStateX;
324
+ const domainOffsetMin = isMinSpecified
325
+ ? 0
326
+ : Math.abs(scale.invert(offsetMin) - scale.invert(0));
327
+ const domainOffsetMax = isMaxSpecified
328
+ ? 0
329
+ : Math.abs(scale.invert(offsetMax) - scale.invert(0));
312
330
  // 10 is the default value for the number of ticks. Here, to preserve the appearance of a series with a small number of points
313
331
  const nicedDomain = scale.copy().nice(Math.max(10, domainData.length)).domain();
314
332
  scale.domain([xMin - domainOffsetMin, xMax + domainOffsetMax]);
@@ -374,8 +392,14 @@ export function createXScale(args) {
374
392
  offsetMax += bandWidth / 2;
375
393
  }
376
394
  }
377
- const domainOffsetMin = Math.abs(scale.invert(offsetMin).getTime() - scale.invert(0).getTime());
378
- const domainOffsetMax = Math.abs(scale.invert(offsetMax).getTime() - scale.invert(0).getTime());
395
+ const isMinSpecified = typeof get(axis, 'min') === 'number' && !zoomStateX;
396
+ const isMaxSpecified = typeof get(axis, 'max') === 'number' && !zoomStateX;
397
+ const domainOffsetMin = isMinSpecified
398
+ ? 0
399
+ : Math.abs(scale.invert(offsetMin).getTime() - scale.invert(0).getTime());
400
+ const domainOffsetMax = isMaxSpecified
401
+ ? 0
402
+ : Math.abs(scale.invert(offsetMax).getTime() - scale.invert(0).getTime());
379
403
  // 10 is the default value for the number of ticks. Here, to preserve the appearance of a series with a small number of points
380
404
  const nicedDomain = scale.copy().nice(Math.max(10, domainData.length)).domain();
381
405
  scale.domain([xMin - domainOffsetMin, xMax + domainOffsetMax]);
@@ -56,6 +56,12 @@ export interface BaseSeries {
56
56
  tooltip?: {
57
57
  /** Formatting settings for tooltip value. */
58
58
  valueFormat?: ValueFormat;
59
+ /**
60
+ * Enable or disable the visibility of this series in the tooltip.
61
+ *
62
+ * @default true
63
+ */
64
+ enabled?: boolean;
59
65
  };
60
66
  }
61
67
  export interface BaseSeriesData<T = MeaningfulAny> {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gravity-ui/charts",
3
- "version": "1.29.0",
4
- "description": "React component used to render charts",
3
+ "version": "1.30.1",
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",
7
7
  "module": "./dist/esm/index.js",