@gravity-ui/chartkit 4.0.0-beta.7 → 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
  });
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { scaleOrdinal } from 'd3';
2
+ import { group, scaleOrdinal } from 'd3';
3
3
  import { DEFAULT_PALETTE } from '../../constants';
4
4
  import { getSeriesNames } from '../../utils';
5
5
  import { getActiveLegendItems, getAllLegendItems } from './utils';
@@ -9,9 +9,11 @@ export const useSeries = (args) => {
9
9
  const preparedSeries = React.useMemo(() => {
10
10
  const seriesNames = getSeriesNames(series);
11
11
  const colorScale = scaleOrdinal(seriesNames, DEFAULT_PALETTE);
12
- return series.reduce((acc, singleSeries) => {
12
+ const groupedSeries = group(series, (item) => item.type);
13
+ return Array.from(groupedSeries).reduce((acc, [seriesType, seriesList]) => {
13
14
  acc.push(...prepareSeries({
14
- series: singleSeries,
15
+ type: seriesType,
16
+ series: seriesList,
15
17
  legend,
16
18
  colorScale,
17
19
  }));
@@ -3,7 +3,8 @@ import type { ChartKitWidgetSeries } from '../../../../../types/widget-data';
3
3
  import type { PreparedLegend } from '../useChartOptions/types';
4
4
  import type { PreparedSeries } from './types';
5
5
  export declare function prepareSeries(args: {
6
- series: ChartKitWidgetSeries;
6
+ type: ChartKitWidgetSeries['type'];
7
+ series: ChartKitWidgetSeries[];
7
8
  legend: PreparedLegend;
8
9
  colorScale: ScaleOrdinal<string, string>;
9
10
  }): PreparedSeries[];
@@ -4,6 +4,10 @@ import get from 'lodash/get';
4
4
  import { DEFAULT_PALETTE } from '../../constants';
5
5
  import { DEFAULT_LEGEND_SYMBOL_SIZE } from './constants';
6
6
  import { getRandomCKId } from '../../../../../utils';
7
+ const DEFAULT_DATALABELS_STYLE = {
8
+ fontSize: '11px',
9
+ fontWeight: 'bold',
10
+ };
7
11
  function prepareLegendSymbol(series) {
8
12
  var _a;
9
13
  switch (series.type) {
@@ -34,6 +38,35 @@ function prepareAxisRelatedSeries(args) {
34
38
  };
35
39
  return [preparedSeries];
36
40
  }
41
+ function prepareBarXSeries(args) {
42
+ const { colorScale, series, legend } = args;
43
+ const commonStackId = getRandomCKId();
44
+ return series.map((singleSeries) => {
45
+ var _a, _b, _c, _d;
46
+ const name = singleSeries.name || '';
47
+ const color = singleSeries.color || colorScale(name);
48
+ return {
49
+ type: singleSeries.type,
50
+ color: color,
51
+ name: name,
52
+ visible: get(singleSeries, 'visible', true),
53
+ legend: {
54
+ enabled: get(singleSeries, 'legend.enabled', legend.enabled),
55
+ symbol: prepareLegendSymbol(singleSeries),
56
+ },
57
+ data: singleSeries.data,
58
+ stacking: singleSeries.stacking,
59
+ stackId: singleSeries.stacking === 'normal' ? commonStackId : getRandomCKId(),
60
+ dataLabels: {
61
+ enabled: ((_a = singleSeries.dataLabels) === null || _a === void 0 ? void 0 : _a.enabled) || false,
62
+ inside: typeof ((_b = singleSeries.dataLabels) === null || _b === void 0 ? void 0 : _b.inside) === 'boolean'
63
+ ? (_c = singleSeries.dataLabels) === null || _c === void 0 ? void 0 : _c.inside
64
+ : false,
65
+ style: Object.assign({}, DEFAULT_DATALABELS_STYLE, (_d = singleSeries.dataLabels) === null || _d === void 0 ? void 0 : _d.style),
66
+ },
67
+ };
68
+ }, []);
69
+ }
37
70
  function preparePieSeries(args) {
38
71
  const { series, legend } = args;
39
72
  const dataNames = series.data.map((d) => d.name);
@@ -68,14 +101,22 @@ function preparePieSeries(args) {
68
101
  return preparedSeries;
69
102
  }
70
103
  export function prepareSeries(args) {
71
- const { series, legend, colorScale } = args;
72
- switch (series.type) {
104
+ const { type, series, legend, colorScale } = args;
105
+ switch (type) {
73
106
  case 'pie': {
74
- return preparePieSeries({ series, legend });
107
+ return series.reduce((acc, singleSeries) => {
108
+ acc.push(...preparePieSeries({ series: singleSeries, legend }));
109
+ return acc;
110
+ }, []);
75
111
  }
76
- case 'scatter':
77
112
  case 'bar-x': {
78
- return prepareAxisRelatedSeries({ series, legend, colorScale });
113
+ return prepareBarXSeries({ series: series, legend, colorScale });
114
+ }
115
+ case 'scatter': {
116
+ return series.reduce((acc, singleSeries) => {
117
+ acc.push(...prepareAxisRelatedSeries({ series: singleSeries, legend, colorScale }));
118
+ return acc;
119
+ }, []);
79
120
  }
80
121
  default: {
81
122
  const seriesType = get(series, 'type');
@@ -1,4 +1,4 @@
1
- import { BarXSeries, BarXSeriesData, PieSeries, PieSeriesData, RectLegendSymbolOptions, ScatterSeries, ScatterSeriesData } from '../../../../../types/widget-data';
1
+ import { BarXSeries, BarXSeriesData, BaseTextStyle, PieSeries, PieSeriesData, RectLegendSymbolOptions, ScatterSeries, ScatterSeriesData } from '../../../../../types/widget-data';
2
2
  export type RectLegendSymbol = {
3
3
  shape: 'rect';
4
4
  } & Required<RectLegendSymbolOptions>;
@@ -19,6 +19,12 @@ export type PreparedScatterSeries = {
19
19
  export type PreparedBarXSeries = {
20
20
  type: BarXSeries['type'];
21
21
  data: BarXSeriesData[];
22
+ stackId: string;
23
+ dataLabels: {
24
+ enabled: boolean;
25
+ inside: boolean;
26
+ style: BaseTextStyle;
27
+ };
22
28
  } & BasePreparedSeries;
23
29
  export type PreparedPieSeries = BasePreparedSeries & Required<Omit<PieSeries, 'data'>> & {
24
30
  data: PieSeriesData['value'];
@@ -2,11 +2,11 @@ import React from 'react';
2
2
  import { ChartOptions } from '../useChartOptions/types';
3
3
  import { ChartScale } from '../useAxisScales';
4
4
  import { OnSeriesMouseLeave, OnSeriesMouseMove } from '../useTooltip/types';
5
- import { BarXSeries } from '../../../../../types/widget-data';
5
+ import { PreparedBarXSeries } from '../useSeries/types';
6
6
  type Args = {
7
7
  top: number;
8
8
  left: number;
9
- series: BarXSeries[];
9
+ series: PreparedBarXSeries[];
10
10
  xAxis: ChartOptions['xAxis'];
11
11
  xScale: ChartScale;
12
12
  yAxis: ChartOptions['yAxis'];
@@ -15,5 +15,5 @@ type Args = {
15
15
  onSeriesMouseLeave?: OnSeriesMouseLeave;
16
16
  svgContainer: SVGSVGElement | null;
17
17
  };
18
- export declare function prepareBarXSeries(args: Args): React.ReactElement<any, string | React.JSXElementConstructor<any>>[];
18
+ export declare function BarXSeriesShapes(args: Args): React.JSX.Element;
19
19
  export {};
@@ -1,11 +1,11 @@
1
1
  import React from 'react';
2
2
  import { block } from '../../../../../utils/cn';
3
- import { pointer } from 'd3';
4
- import { getRandomCKId } from '../../../../../utils';
3
+ import { group, pointer, select } from 'd3';
5
4
  const DEFAULT_BAR_RECT_WIDTH = 50;
6
5
  const DEFAULT_LINEAR_BAR_RECT_WIDTH = 20;
7
6
  const MIN_RECT_GAP = 1;
8
- const b = block('d3-bar');
7
+ const DEFAULT_LABEL_PADDING = 7;
8
+ const b = block('d3-bar-x');
9
9
  const getRectProperties = (args) => {
10
10
  const { point, xAxis, xScale, yAxis, yScale, minPointDistance } = args;
11
11
  let cx;
@@ -48,32 +48,103 @@ function minDiff(arr) {
48
48
  }
49
49
  return result;
50
50
  }
51
- export function prepareBarXSeries(args) {
51
+ export function BarXSeriesShapes(args) {
52
52
  const { top, left, series, xAxis, xScale, yAxis, yScale, onSeriesMouseMove, onSeriesMouseLeave, svgContainer, } = args;
53
- const seriesData = series.map(({ data }) => data).flat(2);
54
- const minPointDistance = minDiff(seriesData.map((item) => Number(item.x)));
55
- return series.reduce((result, item) => {
56
- const randomKey = getRandomCKId();
57
- item.data.forEach((point, i) => {
58
- const rectProps = getRectProperties({
59
- point,
60
- xAxis,
61
- xScale,
62
- yAxis,
63
- yScale,
64
- minPointDistance,
65
- });
66
- result.push(React.createElement("rect", Object.assign({ key: `${i}-${randomKey}`, className: b('rect'), fill: item.color }, rectProps, { onMouseMove: function (e) {
53
+ const ref = React.useRef(null);
54
+ React.useEffect(() => {
55
+ if (!ref.current) {
56
+ return;
57
+ }
58
+ const svgElement = select(ref.current);
59
+ svgElement.selectAll('*').remove();
60
+ const xValues = xAxis.type === 'category'
61
+ ? []
62
+ : series.reduce((acc, { data }) => {
63
+ data.forEach((dataItem) => acc.push(Number(dataItem.x)));
64
+ return acc;
65
+ }, []);
66
+ const minPointDistance = minDiff(xValues);
67
+ const stackedSeriesMap = group(series, (item) => item.stackId);
68
+ Array.from(stackedSeriesMap).forEach(([, stackedSeries]) => {
69
+ const stackHeights = {};
70
+ stackedSeries.forEach((item) => {
71
+ const shapes = item.data.map((dataItem) => {
72
+ const rectProps = getRectProperties({
73
+ point: dataItem,
74
+ xAxis,
75
+ xScale,
76
+ yAxis,
77
+ yScale,
78
+ minPointDistance,
79
+ });
80
+ if (!stackHeights[rectProps.x]) {
81
+ stackHeights[rectProps.x] = 0;
82
+ }
83
+ const rectY = rectProps.y - stackHeights[rectProps.x];
84
+ stackHeights[rectProps.x] += rectProps.height + 1;
85
+ return Object.assign(Object.assign({}, rectProps), { y: rectY, data: dataItem });
86
+ });
87
+ svgElement
88
+ .selectAll('allRects')
89
+ .data(shapes)
90
+ .join('rect')
91
+ .attr('class', b('segment'))
92
+ .attr('x', (d) => d.x)
93
+ .attr('y', (d) => d.y)
94
+ .attr('height', (d) => d.height)
95
+ .attr('width', (d) => d.width)
96
+ .attr('fill', (d) => d.data.color || item.color)
97
+ .on('mousemove', (e, point) => {
67
98
  const [x, y] = pointer(e, svgContainer);
68
99
  onSeriesMouseMove === null || onSeriesMouseMove === void 0 ? void 0 : onSeriesMouseMove({
69
100
  hovered: {
70
- data: point,
101
+ data: point.data,
71
102
  series: item,
72
103
  },
73
104
  pointerPosition: [x - left, y - top],
74
105
  });
75
- }, onMouseLeave: onSeriesMouseLeave })));
106
+ })
107
+ .on('mouseleave', () => {
108
+ if (onSeriesMouseLeave) {
109
+ onSeriesMouseLeave();
110
+ }
111
+ });
112
+ if (item.dataLabels.enabled) {
113
+ const selection = svgElement
114
+ .selectAll('allLabels')
115
+ .data(shapes)
116
+ .join('text')
117
+ .text((d) => String(d.data.label || d.data.y))
118
+ .attr('class', b('label'))
119
+ .attr('x', (d) => d.x + d.width / 2)
120
+ .attr('y', (d) => {
121
+ if (item.dataLabels.inside) {
122
+ return d.y + d.height / 2;
123
+ }
124
+ return d.y - DEFAULT_LABEL_PADDING;
125
+ })
126
+ .attr('text-anchor', 'middle')
127
+ .style('font-size', item.dataLabels.style.fontSize);
128
+ if (item.dataLabels.style.fontWeight) {
129
+ selection.style('font-weight', item.dataLabels.style.fontWeight);
130
+ }
131
+ if (item.dataLabels.style.fontColor) {
132
+ selection.style('fill', item.dataLabels.style.fontColor);
133
+ }
134
+ }
135
+ });
76
136
  });
77
- return result;
78
- }, []);
137
+ }, [
138
+ onSeriesMouseMove,
139
+ onSeriesMouseLeave,
140
+ svgContainer,
141
+ xAxis,
142
+ xScale,
143
+ yAxis,
144
+ yScale,
145
+ series,
146
+ left,
147
+ top,
148
+ ]);
149
+ return React.createElement("g", { ref: ref, className: b() });
79
150
  }
@@ -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
- import { prepareBarXSeries } from './bar-x';
5
- import { prepareScatterSeries } from './scatter';
5
+ import { BarXSeriesShapes } from './bar-x';
6
+ import { ScatterSeriesShape } from './scatter';
6
7
  import { PieSeriesComponent } from './pie';
7
8
  import './styles.css';
8
9
  export const useShapes = (args) => {
@@ -15,40 +16,22 @@ export const useShapes = (args) => {
15
16
  switch (seriesType) {
16
17
  case 'bar-x': {
17
18
  if (xScale && yScale) {
18
- acc.push(...prepareBarXSeries({
19
- top,
20
- left,
21
- series: chartSeries,
22
- xAxis,
23
- xScale,
24
- yAxis,
25
- yScale,
26
- onSeriesMouseMove,
27
- onSeriesMouseLeave,
28
- svgContainer,
29
- }));
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 }));
30
20
  }
31
21
  break;
32
22
  }
33
23
  case 'scatter': {
34
24
  if (xScale && yScale) {
35
- acc.push(...prepareScatterSeries({
36
- top,
37
- left,
38
- series: chartSeries,
39
- xAxis,
40
- xScale,
41
- yAxis,
42
- yScale,
43
- onSeriesMouseMove,
44
- onSeriesMouseLeave,
45
- svgContainer,
46
- }));
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);
47
30
  }
48
31
  break;
49
32
  }
50
33
  case 'pie': {
51
- const groupedPieSeries = group(chartSeries, (item) => item.stackId);
34
+ const groupedPieSeries = group(chartSeries, (pieSeries) => pieSeries.stackId);
52
35
  acc.push(...Array.from(groupedPieSeries).map(([key, pieSeries]) => {
53
36
  return (React.createElement(PieSeriesComponent, { key: `pie-${key}`, boundsWidth: boundsWidth, boundsHeight: boundsHeight, series: pieSeries, onSeriesMouseMove: onSeriesMouseMove, onSeriesMouseLeave: onSeriesMouseLeave, svgContainer: svgContainer }));
54
37
  }));
@@ -65,6 +48,8 @@ export const useShapes = (args) => {
65
48
  yAxis,
66
49
  yScale,
67
50
  svgContainer,
51
+ left,
52
+ top,
68
53
  onSeriesMouseMove,
69
54
  onSeriesMouseLeave,
70
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
  }
@@ -19,4 +19,8 @@
19
19
  fill: var(--g-color-text-complementary);
20
20
  font-size: 11px;
21
21
  font-weight: bold;
22
+ }
23
+
24
+ .chartkit-d3-bar-x__label {
25
+ fill: var(--g-color-text-complementary);
22
26
  }
@@ -1,4 +1,4 @@
1
- import { select } from 'd3';
1
+ import { group, select } from 'd3';
2
2
  import get from 'lodash/get';
3
3
  import { dateTime } from '@gravity-ui/date-utils';
4
4
  import { formatNumber } from '../../../shared';
@@ -32,8 +32,35 @@ export const getDomainDataXBySeries = (series) => {
32
32
  }, []);
33
33
  };
34
34
  export const getDomainDataYBySeries = (series) => {
35
- return series.filter(isSeriesWithNumericalYValues).reduce((acc, s) => {
36
- acc.push(...s.data.map((d) => d.y));
35
+ const groupedSeries = group(series, (item) => item.type);
36
+ return Array.from(groupedSeries).reduce((acc, [type, seriesList]) => {
37
+ switch (type) {
38
+ case 'bar-x': {
39
+ const barXSeries = seriesList;
40
+ const stackedSeries = group(barXSeries, (item) => item.stackId);
41
+ Array.from(stackedSeries).forEach(([, stack]) => {
42
+ const values = {};
43
+ stack.forEach((singleSeries) => {
44
+ singleSeries.data.forEach((point) => {
45
+ const key = String(point.x || point.category);
46
+ if (typeof values[key] === 'undefined') {
47
+ values[key] = 0;
48
+ }
49
+ if (point.y) {
50
+ values[key] += point.y;
51
+ }
52
+ });
53
+ });
54
+ acc.push(...Object.values(values));
55
+ });
56
+ break;
57
+ }
58
+ default: {
59
+ seriesList.filter(isSeriesWithNumericalYValues).forEach((s) => {
60
+ acc.push(...s.data.map((d) => d.y));
61
+ });
62
+ }
63
+ }
37
64
  return acc;
38
65
  }, []);
39
66
  };
@@ -8,6 +8,8 @@ export type BarXSeriesData<T = any> = BaseSeriesData<T> & {
8
8
  y?: number;
9
9
  /** Corresponding value of axis category */
10
10
  category?: string;
11
+ /** Data label value of the bar-x column. If not specified, the y value is used. */
12
+ label?: string | number;
11
13
  };
12
14
  export type BarXSeries<T = any> = BaseSeries & {
13
15
  type: 'bar-x';
@@ -16,6 +18,11 @@ export type BarXSeries<T = any> = BaseSeries & {
16
18
  name: string;
17
19
  /** The main color of the series (hex, rgba) */
18
20
  color?: string;
21
+ /** Whether to stack the values of each series on top of each other.
22
+ * Possible values are undefined to disable, "normal" to stack by value or "percent"
23
+ *
24
+ * @default undefined
25
+ * */
19
26
  stacking?: 'normal' | 'percent';
20
27
  /** This option allows grouping series in a stacked chart */
21
28
  stackId?: string;
@@ -26,7 +33,11 @@ export type BarXSeries<T = any> = BaseSeries & {
26
33
  * */
27
34
  grouping?: boolean;
28
35
  dataLabels?: ChartKitWidgetSeriesOptions['dataLabels'] & {
29
- /** Whether to align the data label inside the box or to the actual value point */
36
+ /**
37
+ * Whether to align the data label inside or outside the box
38
+ *
39
+ * @default false
40
+ * */
30
41
  inside?: boolean;
31
42
  };
32
43
  /** Individual series legend options. Has higher priority than legend options in widget data */
@@ -12,6 +12,7 @@ export type BaseSeries = {
12
12
  * @default true
13
13
  */
14
14
  enabled?: boolean;
15
+ style?: Partial<BaseTextStyle>;
15
16
  };
16
17
  };
17
18
  export type BaseSeriesData<T = any> = {
@@ -21,7 +22,11 @@ export type BaseSeriesData<T = any> = {
21
22
  * Here you can add additional data for your own event callbacks and formatter callbacks
22
23
  */
23
24
  custom?: T;
25
+ /** Individual color for the data chunk (point in scatter, segment in pie, bar etc) */
26
+ color?: string;
24
27
  };
25
28
  export type BaseTextStyle = {
26
29
  fontSize: string;
30
+ fontWeight?: string;
31
+ fontColor?: string;
27
32
  };
@@ -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-beta.7",
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",
@@ -41,10 +41,14 @@
41
41
  "publishConfig": {
42
42
  "access": "public"
43
43
  },
44
+ "sideEffects": [
45
+ "*.css",
46
+ "*.scss"
47
+ ],
44
48
  "dependencies": {
45
49
  "@bem-react/classname": "^1.6.0",
46
50
  "@gravity-ui/date-utils": "^1.4.1",
47
- "@gravity-ui/yagr": "^3.7.10",
51
+ "@gravity-ui/yagr": "^3.7.13",
48
52
  "d3": "^7.8.5",
49
53
  "lodash": "^4.17.21",
50
54
  "react-split-pane": "^0.1.92"