@gravity-ui/chartkit 4.0.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,7 +19,8 @@ import '@gravity-ui/uikit/styles/styles.scss';
19
19
  ```typescript
20
20
  import {ThemeProvider} from '@gravity-ui/uikit';
21
21
  import ChartKit, {settings} from '@gravity-ui/chartkit';
22
- import {YagrPlugin, YagrWidgetData} from '@gravity-ui/chartkit/build/plugins';
22
+ import {YagrPlugin} from '@gravity-ui/chartkit/yagr';
23
+ import type {YagrWidgetData} from '@gravity-ui/chartkit/yagr';
23
24
 
24
25
  import '@gravity-ui/uikit/styles/styles.scss';
25
26
 
@@ -1,14 +1,17 @@
1
1
  import React from 'react';
2
2
  import { select } from 'd3';
3
3
  import debounce from 'lodash/debounce';
4
+ import { getRandomCKId } from '../../../utils';
4
5
  import { Chart } from './components';
5
6
  const D3Widget = React.forwardRef(function D3Widget(props, forwardedRef) {
6
7
  const ref = React.useRef(null);
7
8
  const debounced = React.useRef();
8
9
  const [dimensions, setDimensions] = React.useState();
9
10
  const handleResize = React.useCallback(() => {
10
- if (ref.current) {
11
- const { top, left, width, height } = ref.current.getBoundingClientRect();
11
+ var _a;
12
+ const parentElement = (_a = ref.current) === null || _a === void 0 ? void 0 : _a.parentElement;
13
+ if (parentElement) {
14
+ const { top, left, width, height } = parentElement.getBoundingClientRect();
12
15
  setDimensions({ top, left, width, height });
13
16
  }
14
17
  }, []);
@@ -25,16 +28,22 @@ const D3Widget = React.forwardRef(function D3Widget(props, forwardedRef) {
25
28
  }), [handleResize]);
26
29
  React.useEffect(() => {
27
30
  const selection = select(window);
28
- selection.on('resize', debuncedHandleResize);
31
+ // https://github.com/d3/d3-selection/blob/main/README.md#handling-events
32
+ const eventName = `resize.${getRandomCKId()}`;
33
+ selection.on(eventName, debuncedHandleResize);
29
34
  return () => {
30
35
  // https://d3js.org/d3-selection/events#selection_on
31
- selection.on('resize', null);
36
+ selection.on(eventName, null);
32
37
  };
33
38
  }, [debuncedHandleResize]);
34
39
  React.useEffect(() => {
35
40
  // dimensions initialize
36
41
  handleResize();
37
42
  }, [handleResize]);
38
- return (React.createElement("div", { ref: ref, style: { width: '100%', height: '100%', position: 'relative' } }, (dimensions === null || dimensions === void 0 ? void 0 : dimensions.width) && (dimensions === null || dimensions === void 0 ? void 0 : dimensions.height) && (React.createElement(Chart, { top: (dimensions === null || dimensions === void 0 ? void 0 : dimensions.top) || 0, left: dimensions.left || 0, width: dimensions.width, height: dimensions.height, data: props.data }))));
43
+ return (React.createElement("div", { ref: ref, style: {
44
+ width: (dimensions === null || dimensions === void 0 ? void 0 : dimensions.width) || '100%',
45
+ height: (dimensions === null || dimensions === void 0 ? void 0 : dimensions.height) || '100%',
46
+ position: 'relative',
47
+ } }, (dimensions === null || dimensions === void 0 ? void 0 : dimensions.width) && (dimensions === null || dimensions === void 0 ? void 0 : dimensions.height) && (React.createElement(Chart, { top: (dimensions === null || dimensions === void 0 ? void 0 : dimensions.top) || 0, left: dimensions.left || 0, width: dimensions.width, height: dimensions.height, data: props.data }))));
39
48
  });
40
49
  export default D3Widget;
@@ -18,9 +18,6 @@ export const Chart = (props) => {
18
18
  width,
19
19
  height,
20
20
  margin: chart.margin,
21
- legend,
22
- title,
23
- xAxis,
24
21
  yAxis,
25
22
  });
26
23
  const { preparedSeries, handleLegendItemClick } = useSeries({ series: data.series, legend });
@@ -51,10 +48,7 @@ export const Chart = (props) => {
51
48
  return (React.createElement(React.Fragment, null,
52
49
  React.createElement("svg", { ref: svgRef, className: b({ hovered: chartHovered }), width: width, height: height, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave },
53
50
  title && React.createElement(Title, Object.assign({}, title, { chartWidth: width })),
54
- React.createElement("g", { width: boundsWidth, height: boundsHeight, transform: `translate(${[
55
- chart.margin.left,
56
- chart.margin.top + ((title === null || title === void 0 ? void 0 : title.height) || 0),
57
- ].join(',')})` },
51
+ React.createElement("g", { width: boundsWidth, height: boundsHeight, transform: `translate(${[chart.margin.left, chart.margin.top].join(',')})` },
58
52
  xScale && yScale && (React.createElement(React.Fragment, null,
59
53
  React.createElement(AxisY, { axises: yAxis, width: boundsWidth, height: boundsHeight, scale: yScale }),
60
54
  React.createElement("g", { transform: `translate(0, ${boundsHeight})` },
@@ -1,17 +1,13 @@
1
1
  import type { ChartMargin } from '../../../../../types/widget-data';
2
- import type { PreparedAxis, PreparedLegend, PreparedTitle } from '../useChartOptions/types';
2
+ import type { PreparedAxis } from '../useChartOptions/types';
3
3
  type Args = {
4
4
  width: number;
5
5
  height: number;
6
6
  margin: ChartMargin;
7
- legend: PreparedLegend;
8
- title?: PreparedTitle;
9
- xAxis?: PreparedAxis;
10
7
  yAxis?: PreparedAxis[];
11
8
  };
12
9
  export declare const useChartDimensions: (args: Args) => {
13
10
  boundsWidth: number;
14
11
  boundsHeight: number;
15
- legendHeight: number;
16
12
  };
17
13
  export {};
@@ -1,11 +1,9 @@
1
1
  export const useChartDimensions = (args) => {
2
- const { margin, legend, title, width, height, xAxis, yAxis } = args;
3
- const titleHeight = (title === null || title === void 0 ? void 0 : title.height) || 0;
4
- const xAxisTitleHeight = (xAxis === null || xAxis === void 0 ? void 0 : xAxis.title.height) || 0;
2
+ const { margin, width, height, yAxis } = args;
5
3
  const yAxisTitleHeight = (yAxis === null || yAxis === void 0 ? void 0 : yAxis.reduce((acc, axis) => {
6
4
  return acc + (axis.title.height || 0);
7
5
  }, 0)) || 0;
8
6
  const boundsWidth = width - margin.right - margin.left - yAxisTitleHeight;
9
- const boundsHeight = height - margin.top - margin.bottom - legend.height - titleHeight - xAxisTitleHeight;
10
- return { boundsWidth, boundsHeight, legendHeight: legend.height };
7
+ const boundsHeight = height - margin.top - margin.bottom;
8
+ return { boundsWidth, boundsHeight };
11
9
  };
@@ -1,8 +1,10 @@
1
1
  import type { ChartKitWidgetData } from '../../../../../types/widget-data';
2
- import type { PreparedAxis, PreparedChart } from './types';
2
+ import type { PreparedAxis, PreparedChart, PreparedTitle, PreparedLegend } from './types';
3
3
  export declare const getPreparedChart: (args: {
4
4
  chart: ChartKitWidgetData['chart'];
5
5
  series: ChartKitWidgetData['series'];
6
+ preparedLegend: PreparedLegend;
6
7
  preparedXAxis: PreparedAxis;
7
8
  preparedY1Axis: PreparedAxis;
9
+ preparedTitle?: PreparedTitle;
8
10
  }) => PreparedChart;
@@ -42,26 +42,63 @@ const getAxisLabelMaxWidth = (args) => {
42
42
  .remove();
43
43
  return width;
44
44
  };
45
- export const getPreparedChart = (args) => {
46
- const { chart, series, preparedXAxis, preparedY1Axis } = args;
47
- const hasAxisRelatedSeries = series.data.some(isAxisRelatedSeries);
48
- let marginBottom = get(chart, 'margin.bottom', 0);
49
- let marginLeft = get(chart, 'margin.left', 0);
45
+ const getMarginTop = (args) => {
46
+ const { chart, hasAxisRelatedSeries, preparedY1Axis, preparedTitle } = args;
50
47
  let marginTop = get(chart, 'margin.top', 0);
51
- let marginRight = get(chart, 'margin.right', 0);
48
+ if (hasAxisRelatedSeries) {
49
+ marginTop +=
50
+ getHorisontalSvgTextHeight({ text: 'Tmp', style: preparedY1Axis.labels.style }) / 2;
51
+ }
52
+ if (preparedTitle === null || preparedTitle === void 0 ? void 0 : preparedTitle.height) {
53
+ marginTop += preparedTitle.height;
54
+ }
55
+ return marginTop;
56
+ };
57
+ const getMarginBottom = (args) => {
58
+ const { chart, hasAxisRelatedSeries, preparedLegend, preparedXAxis } = args;
59
+ let marginBottom = get(chart, 'margin.bottom', 0) + preparedLegend.height;
52
60
  if (hasAxisRelatedSeries) {
53
61
  marginBottom +=
54
- preparedXAxis.labels.padding +
62
+ preparedXAxis.title.height +
55
63
  getHorisontalSvgTextHeight({ text: 'Tmp', style: preparedXAxis.labels.style });
64
+ if (preparedXAxis.labels.enabled) {
65
+ marginBottom += preparedXAxis.labels.padding;
66
+ }
67
+ }
68
+ return marginBottom;
69
+ };
70
+ const getMarginLeft = (args) => {
71
+ const { chart, hasAxisRelatedSeries, series, preparedY1Axis } = args;
72
+ let marginLeft = get(chart, 'margin.left', 0);
73
+ if (hasAxisRelatedSeries) {
56
74
  marginLeft +=
57
75
  AXIS_WIDTH +
58
76
  preparedY1Axis.labels.padding +
59
77
  getAxisLabelMaxWidth({ axis: preparedY1Axis, series: series.data }) +
60
- (preparedY1Axis.title.height || 0);
61
- marginTop +=
62
- getHorisontalSvgTextHeight({ text: 'Tmp', style: preparedY1Axis.labels.style }) / 2;
78
+ preparedY1Axis.title.height;
79
+ }
80
+ return marginLeft;
81
+ };
82
+ const getMarginRight = (args) => {
83
+ const { chart, hasAxisRelatedSeries, series, preparedXAxis } = args;
84
+ let marginRight = get(chart, 'margin.right', 0);
85
+ if (hasAxisRelatedSeries) {
63
86
  marginRight += getAxisLabelMaxWidth({ axis: preparedXAxis, series: series.data }) / 2;
64
87
  }
88
+ return marginRight;
89
+ };
90
+ export const getPreparedChart = (args) => {
91
+ const { chart, series, preparedLegend, preparedXAxis, preparedY1Axis, preparedTitle } = args;
92
+ const hasAxisRelatedSeries = series.data.some(isAxisRelatedSeries);
93
+ const marginTop = getMarginTop({ chart, hasAxisRelatedSeries, preparedY1Axis, preparedTitle });
94
+ const marginBottom = getMarginBottom({
95
+ chart,
96
+ hasAxisRelatedSeries,
97
+ preparedLegend,
98
+ preparedXAxis,
99
+ });
100
+ const marginLeft = getMarginLeft({ chart, hasAxisRelatedSeries, series, preparedY1Axis });
101
+ const marginRight = getMarginRight({ chart, hasAxisRelatedSeries, series, preparedXAxis });
65
102
  return {
66
103
  margin: {
67
104
  top: marginTop,
@@ -16,6 +16,8 @@ export const useChartOptions = (args) => {
16
16
  const preparedChart = getPreparedChart({
17
17
  chart,
18
18
  series,
19
+ preparedTitle,
20
+ preparedLegend,
19
21
  preparedXAxis,
20
22
  preparedY1Axis: preparedYAxis[0],
21
23
  });
@@ -93,7 +93,7 @@ export function BarXSeriesShapes(args) {
93
93
  .attr('y', (d) => d.y)
94
94
  .attr('height', (d) => d.height)
95
95
  .attr('width', (d) => d.width)
96
- .attr('fill', item.color)
96
+ .attr('fill', (d) => d.data.color || item.color)
97
97
  .on('mousemove', (e, point) => {
98
98
  const [x, y] = pointer(e, svgContainer);
99
99
  onSeriesMouseMove === null || onSeriesMouseMove === void 0 ? void 0 : onSeriesMouseMove({
@@ -1,8 +1,9 @@
1
1
  import React from 'react';
2
2
  import { group } from 'd3';
3
+ import { getRandomCKId } from '../../../../../utils';
3
4
  import { getOnlyVisibleSeries } from '../../utils';
4
5
  import { BarXSeriesShapes } from './bar-x';
5
- import { prepareScatterSeries } from './scatter';
6
+ import { ScatterSeriesShape } from './scatter';
6
7
  import { PieSeriesComponent } from './pie';
7
8
  import './styles.css';
8
9
  export const useShapes = (args) => {
@@ -15,29 +16,22 @@ export const useShapes = (args) => {
15
16
  switch (seriesType) {
16
17
  case 'bar-x': {
17
18
  if (xScale && yScale) {
18
- acc.push(React.createElement(BarXSeriesShapes, Object.assign({}, args, { key: "bar-x", series: chartSeries, xScale: xScale, yScale: yScale })));
19
+ acc.push(React.createElement(BarXSeriesShapes, { key: "bar-x", series: chartSeries, xAxis: xAxis, xScale: xScale, yAxis: yAxis, yScale: yScale, top: top, left: left, svgContainer: svgContainer, onSeriesMouseMove: onSeriesMouseMove, onSeriesMouseLeave: onSeriesMouseLeave }));
19
20
  }
20
21
  break;
21
22
  }
22
23
  case 'scatter': {
23
24
  if (xScale && yScale) {
24
- acc.push(...prepareScatterSeries({
25
- top,
26
- left,
27
- series: chartSeries,
28
- xAxis,
29
- xScale,
30
- yAxis,
31
- yScale,
32
- onSeriesMouseMove,
33
- onSeriesMouseLeave,
34
- svgContainer,
35
- }));
25
+ const scatterShapes = chartSeries.map((scatterSeries, i) => {
26
+ const id = getRandomCKId();
27
+ return (React.createElement(ScatterSeriesShape, { key: `${i}-${id}`, top: top, left: left, series: scatterSeries, xAxis: xAxis, xScale: xScale, yAxis: yAxis, yScale: yScale, onSeriesMouseMove: onSeriesMouseMove, onSeriesMouseLeave: onSeriesMouseLeave, svgContainer: svgContainer }));
28
+ });
29
+ acc.push(...scatterShapes);
36
30
  }
37
31
  break;
38
32
  }
39
33
  case 'pie': {
40
- const groupedPieSeries = group(chartSeries, (item) => item.stackId);
34
+ const groupedPieSeries = group(chartSeries, (pieSeries) => pieSeries.stackId);
41
35
  acc.push(...Array.from(groupedPieSeries).map(([key, pieSeries]) => {
42
36
  return (React.createElement(PieSeriesComponent, { key: `pie-${key}`, boundsWidth: boundsWidth, boundsHeight: boundsHeight, series: pieSeries, onSeriesMouseMove: onSeriesMouseMove, onSeriesMouseLeave: onSeriesMouseLeave, svgContainer: svgContainer }));
43
37
  }));
@@ -54,6 +48,8 @@ export const useShapes = (args) => {
54
48
  yAxis,
55
49
  yScale,
56
50
  svgContainer,
51
+ left,
52
+ top,
57
53
  onSeriesMouseMove,
58
54
  onSeriesMouseLeave,
59
55
  ]);
@@ -3,10 +3,10 @@ import { ChartOptions } from '../useChartOptions/types';
3
3
  import { ChartScale } from '../useAxisScales';
4
4
  import { OnSeriesMouseLeave, OnSeriesMouseMove } from '../useTooltip/types';
5
5
  import { ScatterSeries } from '../../../../../types/widget-data';
6
- type PrepareScatterSeriesArgs = {
6
+ type ScatterSeriesShapeProps = {
7
7
  top: number;
8
8
  left: number;
9
- series: ScatterSeries[];
9
+ series: ScatterSeries;
10
10
  xAxis: ChartOptions['xAxis'];
11
11
  xScale: ChartScale;
12
12
  yAxis: ChartOptions['yAxis'];
@@ -15,5 +15,5 @@ type PrepareScatterSeriesArgs = {
15
15
  onSeriesMouseMove?: OnSeriesMouseMove;
16
16
  onSeriesMouseLeave?: OnSeriesMouseLeave;
17
17
  };
18
- export declare function prepareScatterSeries(args: PrepareScatterSeriesArgs): React.ReactElement<any, string | React.JSXElementConstructor<any>>[];
18
+ export declare function ScatterSeriesShape(props: ScatterSeriesShapeProps): React.JSX.Element;
19
19
  export {};
@@ -1,7 +1,6 @@
1
- import { pointer } from 'd3';
1
+ import { pointer, select } from 'd3';
2
2
  import React from 'react';
3
3
  import { block } from '../../../../../utils/cn';
4
- import { getRandomCKId } from '../../../../../utils';
5
4
  const b = block('d3-scatter');
6
5
  const DEFAULT_SCATTER_POINT_RADIUS = 4;
7
6
  const prepareCategoricalScatterData = (data) => {
@@ -10,11 +9,9 @@ const prepareCategoricalScatterData = (data) => {
10
9
  const prepareLinearScatterData = (data) => {
11
10
  return data.filter((d) => typeof d.x === 'number' && typeof d.y === 'number');
12
11
  };
13
- const getPointProperties = (args) => {
14
- const { point, xAxis, xScale, yAxis, yScale } = args;
15
- const r = point.radius || DEFAULT_SCATTER_POINT_RADIUS;
12
+ const getCxAttr = (args) => {
13
+ const { point, xAxis, xScale } = args;
16
14
  let cx;
17
- let cy;
18
15
  if (xAxis.type === 'category') {
19
16
  const xBandScale = xScale;
20
17
  cx = (xBandScale(point.category) || 0) + xBandScale.step() / 2;
@@ -23,6 +20,11 @@ const getPointProperties = (args) => {
23
20
  const xLinearScale = xScale;
24
21
  cx = xLinearScale(point.x);
25
22
  }
23
+ return cx;
24
+ };
25
+ const getCyAttr = (args) => {
26
+ const { point, yAxis, yScale } = args;
27
+ let cy;
26
28
  if (yAxis[0].type === 'category') {
27
29
  const yBandScale = yScale;
28
30
  cy = (yBandScale(point.category) || 0) + yBandScale.step() / 2;
@@ -31,35 +33,57 @@ const getPointProperties = (args) => {
31
33
  const yLinearScale = yScale;
32
34
  cy = yLinearScale(point.y);
33
35
  }
34
- return { r, cx, cy };
36
+ return cy;
35
37
  };
36
- export function prepareScatterSeries(args) {
37
- const { top, left, series, xAxis, xScale, yAxis, yScale, onSeriesMouseMove, onSeriesMouseLeave, svgContainer, } = args;
38
- return series.reduce((result, s) => {
38
+ export function ScatterSeriesShape(props) {
39
+ const { series, xAxis, xScale, yAxis, yScale, svgContainer, left, top, onSeriesMouseMove, onSeriesMouseLeave, } = props;
40
+ const ref = React.useRef(null);
41
+ React.useEffect(() => {
39
42
  var _a;
40
- const randomKey = getRandomCKId();
43
+ if (!ref.current) {
44
+ return;
45
+ }
46
+ const svgElement = select(ref.current);
47
+ svgElement.selectAll('*').remove();
41
48
  const preparedData = xAxis.type === 'category' || ((_a = yAxis[0]) === null || _a === void 0 ? void 0 : _a.type) === 'category'
42
- ? prepareCategoricalScatterData(s.data)
43
- : prepareLinearScatterData(s.data);
44
- result.push(...preparedData.map((point, i) => {
45
- const pointProps = getPointProperties({
46
- point,
47
- xAxis,
48
- xScale,
49
- yAxis,
50
- yScale,
49
+ ? prepareCategoricalScatterData(series.data)
50
+ : prepareLinearScatterData(series.data);
51
+ svgElement
52
+ .selectAll('allPoints')
53
+ .data(preparedData)
54
+ .enter()
55
+ .append('circle')
56
+ .attr('class', b('point'))
57
+ .attr('fill', (d) => d.color || series.color || '')
58
+ .attr('r', (d) => d.radius || DEFAULT_SCATTER_POINT_RADIUS)
59
+ .attr('cx', (d) => getCxAttr({ point: d, xAxis, xScale }))
60
+ .attr('cy', (d) => getCyAttr({ point: d, yAxis, yScale }))
61
+ .on('mousemove', (e, d) => {
62
+ const [x, y] = pointer(e, svgContainer);
63
+ onSeriesMouseMove === null || onSeriesMouseMove === void 0 ? void 0 : onSeriesMouseMove({
64
+ hovered: {
65
+ data: d,
66
+ series,
67
+ },
68
+ pointerPosition: [x - left, y - top],
51
69
  });
52
- return (React.createElement("circle", Object.assign({ key: `${i}-${randomKey}`, className: b('point'), fill: s.color }, pointProps, { onMouseMove: function (e) {
53
- const [x, y] = pointer(e, svgContainer);
54
- onSeriesMouseMove === null || onSeriesMouseMove === void 0 ? void 0 : onSeriesMouseMove({
55
- hovered: {
56
- data: point,
57
- series: s,
58
- },
59
- pointerPosition: [x - left, y - top],
60
- });
61
- }, onMouseLeave: onSeriesMouseLeave })));
62
- }));
63
- return result;
64
- }, []);
70
+ })
71
+ .on('mouseleave', () => {
72
+ if (onSeriesMouseLeave) {
73
+ onSeriesMouseLeave();
74
+ }
75
+ });
76
+ }, [
77
+ series,
78
+ xAxis,
79
+ xScale,
80
+ yAxis,
81
+ yScale,
82
+ svgContainer,
83
+ left,
84
+ top,
85
+ onSeriesMouseMove,
86
+ onSeriesMouseLeave,
87
+ ]);
88
+ return React.createElement("g", { ref: ref, className: b() });
65
89
  }
@@ -22,6 +22,8 @@ export type BaseSeriesData<T = any> = {
22
22
  * Here you can add additional data for your own event callbacks and formatter callbacks
23
23
  */
24
24
  custom?: T;
25
+ /** Individual color for the data chunk (point in scatter, segment in pie, bar etc) */
26
+ color?: string;
25
27
  };
26
28
  export type BaseTextStyle = {
27
29
  fontSize: string;
@@ -5,8 +5,6 @@ export type PieSeriesData<T = any> = BaseSeriesData<T> & {
5
5
  value: number;
6
6
  /** The name of the pie segment (used in legend, tooltip etc). */
7
7
  name: string;
8
- /** Individual color for the pie segment. */
9
- color?: string;
10
8
  /** Initial visibility of the pie segment. */
11
9
  visible?: boolean;
12
10
  /** Initial data label of the pie segment. If not specified, the value is used. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gravity-ui/chartkit",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "description": "React component used to render charts based on any sources you need",
5
5
  "license": "MIT",
6
6
  "repository": "git@github.com:gravity-ui/ChartKit.git",
@@ -48,7 +48,7 @@
48
48
  "dependencies": {
49
49
  "@bem-react/classname": "^1.6.0",
50
50
  "@gravity-ui/date-utils": "^1.4.1",
51
- "@gravity-ui/yagr": "^3.7.10",
51
+ "@gravity-ui/yagr": "^3.7.13",
52
52
  "d3": "^7.8.5",
53
53
  "lodash": "^4.17.21",
54
54
  "react-split-pane": "^0.1.92"