@gravity-ui/chartkit 4.6.1 → 4.7.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.
Files changed (96) hide show
  1. package/build/plugins/d3/examples/bar-x/Basic.d.ts +4 -0
  2. package/build/plugins/d3/examples/bar-x/Basic.js +78 -0
  3. package/build/plugins/d3/examples/bar-x/GroupedColumns.d.ts +2 -0
  4. package/build/plugins/d3/examples/bar-x/GroupedColumns.js +44 -0
  5. package/build/plugins/d3/examples/bar-x/StackedColumns.d.ts +2 -0
  6. package/build/plugins/d3/examples/bar-x/StackedColumns.js +45 -0
  7. package/build/plugins/d3/examples/nintendoGames.d.ts +62 -0
  8. package/build/plugins/d3/examples/nintendoGames.js +12037 -0
  9. package/build/plugins/d3/examples/pie/Basic.d.ts +2 -0
  10. package/build/plugins/d3/examples/pie/Basic.js +30 -0
  11. package/build/plugins/d3/examples/pie/Donut.d.ts +2 -0
  12. package/build/plugins/d3/examples/pie/Donut.js +31 -0
  13. package/build/plugins/d3/examples/scatter/Basic.d.ts +2 -0
  14. package/build/plugins/d3/examples/scatter/Basic.js +66 -0
  15. package/build/plugins/d3/renderer/components/AxisX.js +6 -7
  16. package/build/plugins/d3/renderer/components/AxisY.js +50 -18
  17. package/build/plugins/d3/renderer/components/Chart.js +24 -16
  18. package/build/plugins/d3/renderer/components/Legend.js +20 -22
  19. package/build/plugins/d3/renderer/components/Title.js +1 -1
  20. package/build/plugins/d3/renderer/components/Tooltip/DefaultContent.d.ts +2 -2
  21. package/build/plugins/d3/renderer/components/Tooltip/DefaultContent.js +8 -0
  22. package/build/plugins/d3/renderer/components/Tooltip/TooltipTriggerArea.d.ts +14 -0
  23. package/build/plugins/d3/renderer/components/Tooltip/TooltipTriggerArea.js +70 -0
  24. package/build/plugins/d3/renderer/components/Tooltip/index.d.ts +5 -3
  25. package/build/plugins/d3/renderer/components/Tooltip/index.js +4 -2
  26. package/build/plugins/d3/renderer/components/styles.css +3 -0
  27. package/build/plugins/d3/renderer/constants/defaults/axis.d.ts +9 -0
  28. package/build/plugins/d3/renderer/constants/defaults/axis.js +6 -0
  29. package/build/plugins/d3/renderer/constants/defaults/index.d.ts +1 -0
  30. package/build/plugins/d3/renderer/constants/defaults/index.js +1 -0
  31. package/build/plugins/d3/renderer/constants/defaults/series-options.d.ts +11 -0
  32. package/build/plugins/d3/renderer/constants/defaults/series-options.js +41 -0
  33. package/build/plugins/d3/renderer/constants/index.d.ts +0 -1
  34. package/build/plugins/d3/renderer/constants/index.js +0 -1
  35. package/build/plugins/d3/renderer/d3-dispatcher.d.ts +1 -0
  36. package/build/plugins/d3/renderer/d3-dispatcher.js +4 -0
  37. package/build/plugins/d3/renderer/hooks/index.d.ts +0 -1
  38. package/build/plugins/d3/renderer/hooks/index.js +0 -1
  39. package/build/plugins/d3/renderer/hooks/useAxisScales/index.d.ts +6 -5
  40. package/build/plugins/d3/renderer/hooks/useAxisScales/index.js +2 -1
  41. package/build/plugins/d3/renderer/hooks/useChartDimensions/index.js +8 -41
  42. package/build/plugins/d3/renderer/hooks/useChartDimensions/utils.d.ts +3 -0
  43. package/build/plugins/d3/renderer/hooks/useChartDimensions/utils.js +17 -4
  44. package/build/plugins/d3/renderer/hooks/useChartOptions/chart.d.ts +2 -4
  45. package/build/plugins/d3/renderer/hooks/useChartOptions/chart.js +4 -23
  46. package/build/plugins/d3/renderer/hooks/useChartOptions/index.d.ts +1 -1
  47. package/build/plugins/d3/renderer/hooks/useChartOptions/index.js +2 -10
  48. package/build/plugins/d3/renderer/hooks/useChartOptions/title.d.ts +1 -1
  49. package/build/plugins/d3/renderer/hooks/useChartOptions/title.js +1 -0
  50. package/build/plugins/d3/renderer/hooks/useChartOptions/types.d.ts +8 -5
  51. package/build/plugins/d3/renderer/hooks/useChartOptions/x-axis.d.ts +5 -3
  52. package/build/plugins/d3/renderer/hooks/useChartOptions/x-axis.js +61 -6
  53. package/build/plugins/d3/renderer/hooks/useChartOptions/y-axis.d.ts +1 -1
  54. package/build/plugins/d3/renderer/hooks/useChartOptions/y-axis.js +20 -12
  55. package/build/plugins/d3/renderer/hooks/useSeries/index.d.ts +1 -0
  56. package/build/plugins/d3/renderer/hooks/useSeries/index.js +8 -2
  57. package/build/plugins/d3/renderer/hooks/useSeries/prepare-legend.js +8 -3
  58. package/build/plugins/d3/renderer/hooks/useSeries/prepare-options.d.ts +3 -0
  59. package/build/plugins/d3/renderer/hooks/useSeries/prepare-options.js +5 -0
  60. package/build/plugins/d3/renderer/hooks/useSeries/prepareSeries.js +6 -3
  61. package/build/plugins/d3/renderer/hooks/useSeries/types.d.ts +5 -1
  62. package/build/plugins/d3/renderer/hooks/useShapes/bar-x/index.d.ts +12 -0
  63. package/build/plugins/d3/renderer/hooks/useShapes/bar-x/index.js +91 -0
  64. package/build/plugins/d3/renderer/hooks/useShapes/bar-x/prepare-data.d.ts +19 -0
  65. package/build/plugins/d3/renderer/hooks/useShapes/{bar-x.js → bar-x/prepare-data.js} +7 -87
  66. package/build/plugins/d3/renderer/hooks/useShapes/index.d.ts +13 -10
  67. package/build/plugins/d3/renderer/hooks/useShapes/index.js +28 -13
  68. package/build/plugins/d3/renderer/hooks/useShapes/pie.d.ts +6 -4
  69. package/build/plugins/d3/renderer/hooks/useShapes/pie.js +98 -20
  70. package/build/plugins/d3/renderer/hooks/useShapes/scatter/index.d.ts +15 -0
  71. package/build/plugins/d3/renderer/hooks/useShapes/scatter/index.js +89 -0
  72. package/build/plugins/d3/renderer/hooks/useShapes/scatter/prepare-data.d.ts +19 -0
  73. package/build/plugins/d3/renderer/hooks/useShapes/scatter/prepare-data.js +55 -0
  74. package/build/plugins/d3/renderer/hooks/useShapes/styles.css +1 -9
  75. package/build/plugins/d3/renderer/hooks/useTooltip/index.d.ts +6 -6
  76. package/build/plugins/d3/renderer/hooks/useTooltip/index.js +15 -17
  77. package/build/plugins/d3/renderer/hooks/useTooltip/types.d.ts +0 -6
  78. package/build/plugins/d3/renderer/utils/axis-generators/bottom.d.ts +3 -1
  79. package/build/plugins/d3/renderer/utils/axis-generators/bottom.js +32 -22
  80. package/build/plugins/d3/renderer/utils/index.d.ts +5 -0
  81. package/build/plugins/d3/renderer/utils/index.js +13 -8
  82. package/build/plugins/d3/renderer/utils/math.d.ts +2 -0
  83. package/build/plugins/d3/renderer/utils/math.js +8 -0
  84. package/build/plugins/d3/renderer/utils/text.d.ts +6 -6
  85. package/build/plugins/d3/renderer/utils/text.js +24 -14
  86. package/build/types/widget-data/axis.d.ts +10 -0
  87. package/build/types/widget-data/series.d.ts +51 -0
  88. package/build/types/widget-data/tooltip.d.ts +18 -7
  89. package/package.json +2 -2
  90. package/build/plugins/d3/renderer/hooks/useChartEvents/index.d.ts +0 -5
  91. package/build/plugins/d3/renderer/hooks/useChartEvents/index.js +0 -15
  92. package/build/plugins/d3/renderer/hooks/useShapes/bar-x.d.ts +0 -21
  93. package/build/plugins/d3/renderer/hooks/useShapes/defaults.d.ts +0 -5
  94. package/build/plugins/d3/renderer/hooks/useShapes/defaults.js +0 -5
  95. package/build/plugins/d3/renderer/hooks/useShapes/scatter.d.ts +0 -19
  96. package/build/plugins/d3/renderer/hooks/useShapes/scatter.js +0 -89
@@ -1,8 +1,13 @@
1
1
  import React from 'react';
2
- import { arc, pie, select } from 'd3';
2
+ import get from 'lodash/get';
3
+ import kebabCase from 'lodash/kebabCase';
4
+ import { arc, color, pie, pointer, select } from 'd3';
3
5
  import { block } from '../../../../../utils/cn';
4
- import { calculateNumericProperty, getHorisontalSvgTextHeight } from '../../utils';
6
+ import { calculateNumericProperty, extractD3DataFromNode, getHorisontalSvgTextHeight, isNodeContainsD3Data, } from '../../utils';
5
7
  const b = block('d3-pie');
8
+ const preparePieData = (series) => {
9
+ return series.map((s) => ({ series: s, data: s.data }));
10
+ };
6
11
  const getCenter = (boundsWidth, boundsHeight, center) => {
7
12
  var _a, _b;
8
13
  const defaultX = boundsWidth * 0.5;
@@ -15,17 +20,29 @@ const getCenter = (boundsWidth, boundsHeight, center) => {
15
20
  const resultY = (_b = calculateNumericProperty({ value: y, base: boundsHeight })) !== null && _b !== void 0 ? _b : defaultY;
16
21
  return [resultX, resultY];
17
22
  };
23
+ const getOpacity = (args) => {
24
+ const { data, hoveredData, opacity } = args;
25
+ if (data.series.id !== (hoveredData === null || hoveredData === void 0 ? void 0 : hoveredData.series.id)) {
26
+ return opacity || null;
27
+ }
28
+ return null;
29
+ };
30
+ const isNodeContainsPieData = (node) => {
31
+ return isNodeContainsD3Data(node);
32
+ };
18
33
  export function PieSeriesComponent(args) {
19
34
  var _a;
20
- const { boundsWidth, boundsHeight, series, onSeriesMouseMove, onSeriesMouseLeave, svgContainer } = args;
35
+ const { boundsWidth, boundsHeight, dispatcher, top, left, series, seriesOptions, svgContainer } = args;
21
36
  const ref = React.useRef(null);
22
37
  const [x, y] = getCenter(boundsWidth, boundsHeight, (_a = series[0]) === null || _a === void 0 ? void 0 : _a.center);
23
38
  React.useEffect(() => {
24
39
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
25
40
  if (!ref.current) {
26
- return;
41
+ return () => { };
27
42
  }
28
43
  const svgElement = select(ref.current);
44
+ const hoverOptions = get(seriesOptions, 'pie.states.hover');
45
+ const inactiveOptions = get(seriesOptions, 'pie.states.inactive');
29
46
  const isLabelsEnabled = (_b = (_a = series[0]) === null || _a === void 0 ? void 0 : _a.dataLabels) === null || _b === void 0 ? void 0 : _b.enabled;
30
47
  let radiusRelatedToChart = Math.min(boundsWidth, boundsHeight) / 2;
31
48
  if (isLabelsEnabled) {
@@ -39,41 +56,46 @@ export function PieSeriesComponent(args) {
39
56
  radius *= 0.9;
40
57
  }
41
58
  const innerRadius = (_f = calculateNumericProperty({ value: series[0].innerRadius, base: radius })) !== null && _f !== void 0 ? _f : 0;
42
- const pieGenerator = pie().value((d) => d.data);
43
- const visibleData = series.filter((d) => d.visible);
59
+ const preparedData = preparePieData(series);
60
+ const pieGenerator = pie().value((d) => d.data.value);
61
+ const visibleData = preparedData.filter((d) => d.series.visible);
44
62
  const dataReady = pieGenerator(visibleData);
45
63
  const arcGenerator = arc()
46
64
  .innerRadius(innerRadius)
47
65
  .outerRadius(radius)
48
- .cornerRadius((d) => d.data.borderRadius);
66
+ .cornerRadius((d) => d.data.series.borderRadius);
49
67
  svgElement.selectAll('*').remove();
50
- svgElement
51
- .selectAll('allSlices')
68
+ const segmentSelection = svgElement
69
+ .selectAll('segments')
52
70
  .data(dataReady)
53
71
  .enter()
54
72
  .append('path')
55
73
  .attr('d', arcGenerator)
56
74
  .attr('class', b('segment'))
57
- .attr('fill', (d) => d.data.color || '')
75
+ .attr('fill', (d) => d.data.series.color)
58
76
  .style('stroke', ((_g = series[0]) === null || _g === void 0 ? void 0 : _g.borderColor) || '')
59
77
  .style('stroke-width', (_j = (_h = series[0]) === null || _h === void 0 ? void 0 : _h.borderWidth) !== null && _j !== void 0 ? _j : 1);
78
+ let polylineSelection;
79
+ let labelSelection;
60
80
  if ((_l = (_k = series[0]) === null || _k === void 0 ? void 0 : _k.dataLabels) === null || _l === void 0 ? void 0 : _l.enabled) {
61
81
  const labelHeight = getHorisontalSvgTextHeight({ text: 'tmp' });
62
82
  const outerArc = arc()
63
83
  .innerRadius(labelsArcRadius)
64
84
  .outerRadius(labelsArcRadius);
85
+ const polylineArc = arc()
86
+ .innerRadius(radius)
87
+ .outerRadius(radius);
65
88
  // Add the polylines between chart and labels
66
- svgElement
67
- .selectAll('allPolylines')
89
+ polylineSelection = svgElement
90
+ .selectAll('polylines')
68
91
  .data(dataReady)
69
92
  .enter()
70
93
  .append('polyline')
71
- .attr('stroke', (d) => d.data.color || '')
72
- .style('fill', 'none')
94
+ .attr('stroke', (d) => d.data.series.color || '')
73
95
  .attr('stroke-width', 1)
74
96
  .attr('points', (d) => {
75
97
  // Line insertion in the slice
76
- const posA = arcGenerator.centroid(d);
98
+ const posA = polylineArc.centroid(d);
77
99
  // Line break: we use the other arc generator that has been built only for that
78
100
  const posB = outerArc.centroid(d);
79
101
  const posC = outerArc.centroid(d);
@@ -101,13 +123,15 @@ export function PieSeriesComponent(args) {
101
123
  }
102
124
  }
103
125
  return result.join(' ');
104
- });
126
+ })
127
+ .attr('pointer-events', 'none')
128
+ .style('fill', 'none');
105
129
  // Add the polylines between chart and labels
106
- svgElement
107
- .selectAll('allLabels')
130
+ labelSelection = svgElement
131
+ .selectAll('labels')
108
132
  .data(dataReady)
109
133
  .join('text')
110
- .text((d) => d.data.label || d.value)
134
+ .text((d) => d.data.series.label || d.value)
111
135
  .attr('class', b('label'))
112
136
  .attr('transform', (d) => {
113
137
  const pos = outerArc.centroid(d);
@@ -116,11 +140,65 @@ export function PieSeriesComponent(args) {
116
140
  pos[1] += labelHeight / 4;
117
141
  return `translate(${pos})`;
118
142
  })
143
+ .attr('pointer-events', 'none')
119
144
  .style('text-anchor', (d) => {
120
145
  const midangle = d.startAngle + (d.endAngle - d.startAngle) / 2;
121
146
  return midangle < Math.PI ? 'start' : 'end';
122
147
  });
123
148
  }
124
- }, [boundsWidth, boundsHeight, series, onSeriesMouseMove, onSeriesMouseLeave, svgContainer]);
149
+ svgElement
150
+ .on('mousemove', (e) => {
151
+ const segment = e.target;
152
+ if (!isNodeContainsPieData(segment)) {
153
+ return;
154
+ }
155
+ const [pointerX, pointerY] = pointer(e, svgContainer);
156
+ const segmentData = extractD3DataFromNode(segment).data;
157
+ dispatcher.call('hover-shape', {}, [segmentData], [pointerX - left, pointerY - top]);
158
+ })
159
+ .on('mouseleave', () => {
160
+ dispatcher.call('hover-shape', {}, undefined);
161
+ });
162
+ const eventName = `hover-shape.pie-${kebabCase(preparedData[0].series.id)}`;
163
+ dispatcher.on(eventName, (datas) => {
164
+ const data = datas === null || datas === void 0 ? void 0 : datas[0];
165
+ const hoverEnabled = hoverOptions === null || hoverOptions === void 0 ? void 0 : hoverOptions.enabled;
166
+ const inactiveEnabled = inactiveOptions === null || inactiveOptions === void 0 ? void 0 : inactiveOptions.enabled;
167
+ if (hoverEnabled && data) {
168
+ segmentSelection.attr('fill', (d) => {
169
+ var _a;
170
+ const fillColor = d.data.series.color;
171
+ if (d.data.series.id === data.series.id) {
172
+ return (((_a = color(fillColor)) === null || _a === void 0 ? void 0 : _a.brighter(hoverOptions === null || hoverOptions === void 0 ? void 0 : hoverOptions.brightness).toString()) ||
173
+ fillColor);
174
+ }
175
+ return fillColor;
176
+ });
177
+ }
178
+ else if (hoverEnabled) {
179
+ segmentSelection.attr('fill', (d) => d.data.series.color);
180
+ }
181
+ if (inactiveEnabled && data) {
182
+ const opacity = inactiveOptions === null || inactiveOptions === void 0 ? void 0 : inactiveOptions.opacity;
183
+ segmentSelection.attr('opacity', (d) => {
184
+ return getOpacity({ data: d.data, hoveredData: data, opacity });
185
+ });
186
+ polylineSelection === null || polylineSelection === void 0 ? void 0 : polylineSelection.attr('opacity', (d) => {
187
+ return getOpacity({ data: d.data, hoveredData: data, opacity });
188
+ });
189
+ labelSelection === null || labelSelection === void 0 ? void 0 : labelSelection.attr('opacity', (d) => {
190
+ return getOpacity({ data: d.data, hoveredData: data, opacity });
191
+ });
192
+ }
193
+ else if (inactiveEnabled) {
194
+ segmentSelection.attr('opacity', null);
195
+ polylineSelection === null || polylineSelection === void 0 ? void 0 : polylineSelection.attr('opacity', null);
196
+ labelSelection === null || labelSelection === void 0 ? void 0 : labelSelection.attr('opacity', null);
197
+ }
198
+ });
199
+ return () => {
200
+ dispatcher.on(eventName, null);
201
+ };
202
+ }, [boundsWidth, boundsHeight, dispatcher, top, left, series, seriesOptions, svgContainer]);
125
203
  return React.createElement("g", { ref: ref, className: b(), transform: `translate(${x}, ${y})` });
126
204
  }
@@ -0,0 +1,15 @@
1
+ import React from 'react';
2
+ import type { Dispatch } from 'd3';
3
+ import { PreparedSeriesOptions } from '../../useSeries/types';
4
+ import type { PreparedScatterData } from './prepare-data';
5
+ export { prepareScatterData } from './prepare-data';
6
+ export type { PreparedScatterData } from './prepare-data';
7
+ type ScatterSeriesShapeProps = {
8
+ dispatcher: Dispatch<object>;
9
+ top: number;
10
+ left: number;
11
+ preparedData: PreparedScatterData[];
12
+ seriesOptions: PreparedSeriesOptions;
13
+ svgContainer: SVGSVGElement | null;
14
+ };
15
+ export declare function ScatterSeriesShape(props: ScatterSeriesShapeProps): React.JSX.Element;
@@ -0,0 +1,89 @@
1
+ import React from 'react';
2
+ import get from 'lodash/get';
3
+ import { color, pointer, select } from 'd3';
4
+ import { block } from '../../../../../../utils/cn';
5
+ import { extractD3DataFromNode, isNodeContainsD3Data } from '../../../utils';
6
+ export { prepareScatterData } from './prepare-data';
7
+ const b = block('d3-scatter');
8
+ const DEFAULT_SCATTER_POINT_RADIUS = 4;
9
+ const EMPTY_SELECTION = null;
10
+ const key = (d) => d.id || -1;
11
+ const isNodeContainsScatterData = (node) => {
12
+ return isNodeContainsD3Data(node);
13
+ };
14
+ export function ScatterSeriesShape(props) {
15
+ const { dispatcher, top, left, preparedData, seriesOptions, svgContainer } = props;
16
+ const ref = React.useRef(null);
17
+ React.useEffect(() => {
18
+ if (!ref.current) {
19
+ return () => { };
20
+ }
21
+ const svgElement = select(ref.current);
22
+ const hoverOptions = get(seriesOptions, 'scatter.states.hover');
23
+ const inactiveOptions = get(seriesOptions, 'scatter.states.inactive');
24
+ const selection = svgElement
25
+ .selectAll(`circle`)
26
+ .data(preparedData, key)
27
+ .join((enter) => enter.append('circle').attr('class', b('point')), (update) => update, (exit) => exit.remove())
28
+ .attr('fill', (d) => d.data.color || d.series.color || '')
29
+ .attr('r', (d) => d.data.radius || DEFAULT_SCATTER_POINT_RADIUS)
30
+ .attr('cx', (d) => d.cx)
31
+ .attr('cy', (d) => d.cy);
32
+ svgElement
33
+ .on('mousemove', (e) => {
34
+ const point = e.target;
35
+ if (!isNodeContainsScatterData(point)) {
36
+ return;
37
+ }
38
+ const [pointerX, pointerY] = pointer(e, svgContainer);
39
+ const segmentData = extractD3DataFromNode(point);
40
+ dispatcher.call('hover-shape', {}, [segmentData], [pointerX - left, pointerY - top]);
41
+ })
42
+ .on('mouseleave', () => {
43
+ dispatcher.call('hover-shape', {}, undefined);
44
+ });
45
+ const hoverEnabled = hoverOptions === null || hoverOptions === void 0 ? void 0 : hoverOptions.enabled;
46
+ const inactiveEnabled = inactiveOptions === null || inactiveOptions === void 0 ? void 0 : inactiveOptions.enabled;
47
+ dispatcher.on('hover-shape.scatter', (data) => {
48
+ const selectedPoint = data === null || data === void 0 ? void 0 : data[0];
49
+ const updates = [];
50
+ preparedData.forEach((p) => {
51
+ const hovered = Boolean(hoverEnabled &&
52
+ selectedPoint &&
53
+ p.cx === selectedPoint.cx &&
54
+ p.cy === selectedPoint.cy);
55
+ if (p.hovered !== hovered) {
56
+ p.hovered = hovered;
57
+ updates.push(p);
58
+ }
59
+ const active = Boolean(!inactiveEnabled || !selectedPoint || selectedPoint.series.id === p.series.id);
60
+ if (p.active !== active) {
61
+ p.active = active;
62
+ updates.push(p);
63
+ }
64
+ });
65
+ selection.data(updates, key).join(() => EMPTY_SELECTION, (update) => {
66
+ update
67
+ .attr('fill', (d) => {
68
+ var _a;
69
+ const initialColor = d.data.color || d.series.color || '';
70
+ if (d.hovered) {
71
+ return (((_a = color(initialColor)) === null || _a === void 0 ? void 0 : _a.brighter(hoverOptions === null || hoverOptions === void 0 ? void 0 : hoverOptions.brightness).toString()) || initialColor);
72
+ }
73
+ return initialColor;
74
+ })
75
+ .attr('opacity', function (d) {
76
+ if (!d.active) {
77
+ return (inactiveOptions === null || inactiveOptions === void 0 ? void 0 : inactiveOptions.opacity) || null;
78
+ }
79
+ return null;
80
+ });
81
+ return update;
82
+ }, (exit) => exit);
83
+ });
84
+ return () => {
85
+ dispatcher.on('hover-shape.scatter', null);
86
+ };
87
+ }, [dispatcher, top, left, preparedData, seriesOptions, svgContainer]);
88
+ return React.createElement("g", { ref: ref, className: b() });
89
+ }
@@ -0,0 +1,19 @@
1
+ import type { TooltipDataChunkScatter } from '../../../../../../types/widget-data';
2
+ import type { ChartScale } from '../../useAxisScales';
3
+ import type { PreparedAxis } from '../../useChartOptions/types';
4
+ import { PreparedScatterSeries } from '../../useSeries/types';
5
+ export type PreparedScatterData = Omit<TooltipDataChunkScatter, 'series'> & {
6
+ cx: number;
7
+ cy: number;
8
+ series: PreparedScatterSeries;
9
+ hovered: boolean;
10
+ active: boolean;
11
+ id: number;
12
+ };
13
+ export declare const prepareScatterData: (args: {
14
+ series: PreparedScatterSeries[];
15
+ xAxis: PreparedAxis;
16
+ xScale: ChartScale;
17
+ yAxis: PreparedAxis;
18
+ yScale: ChartScale;
19
+ }) => PreparedScatterData[];
@@ -0,0 +1,55 @@
1
+ import get from 'lodash/get';
2
+ import { getDataCategoryValue } from '../../../utils';
3
+ const getCxAttr = (args) => {
4
+ const { point, xAxis, xScale } = args;
5
+ let cx;
6
+ if (xAxis.type === 'category') {
7
+ const xBandScale = xScale;
8
+ const categories = get(xAxis, 'categories', []);
9
+ const dataCategory = getDataCategoryValue({ axisDirection: 'x', categories, data: point });
10
+ cx = (xBandScale(dataCategory) || 0) + xBandScale.step() / 2;
11
+ }
12
+ else {
13
+ const xLinearScale = xScale;
14
+ cx = xLinearScale(point.x);
15
+ }
16
+ return cx;
17
+ };
18
+ const getCyAttr = (args) => {
19
+ const { point, yAxis, yScale } = args;
20
+ let cy;
21
+ if (yAxis.type === 'category') {
22
+ const yBandScale = yScale;
23
+ const categories = get(yAxis, 'categories', []);
24
+ const dataCategory = getDataCategoryValue({ axisDirection: 'y', categories, data: point });
25
+ cy = (yBandScale(dataCategory) || 0) + yBandScale.step() / 2;
26
+ }
27
+ else {
28
+ const yLinearScale = yScale;
29
+ cy = yLinearScale(point.y);
30
+ }
31
+ return cy;
32
+ };
33
+ const getFilteredLinearScatterData = (data) => {
34
+ return data.filter((d) => typeof d.x === 'number' && typeof d.y === 'number');
35
+ };
36
+ export const prepareScatterData = (args) => {
37
+ const { series, xAxis, xScale, yAxis, yScale } = args;
38
+ return series.reduce((acc, s) => {
39
+ const filteredData = xAxis.type === 'category' || yAxis.type === 'category'
40
+ ? s.data
41
+ : getFilteredLinearScatterData(s.data);
42
+ filteredData.forEach((d) => {
43
+ acc.push({
44
+ data: d,
45
+ series: s,
46
+ cx: getCxAttr({ point: d, xAxis, xScale }),
47
+ cy: getCyAttr({ point: d, yAxis, yScale }),
48
+ hovered: false,
49
+ active: true,
50
+ id: acc.length - 1,
51
+ });
52
+ });
53
+ return acc;
54
+ }, []);
55
+ };
@@ -2,15 +2,6 @@
2
2
  stroke-width: 1px;
3
3
  }
4
4
 
5
- .chartkit-d3_hovered .chartkit-d3-scatter__point {
6
- opacity: 0.5;
7
- }
8
-
9
- .chartkit-d3-scatter__point:hover {
10
- stroke: #fff;
11
- opacity: 1;
12
- }
13
-
14
5
  .chartkit-d3-pie__segment {
15
6
  stroke: var(--g-color-base-background);
16
7
  }
@@ -23,4 +14,5 @@
23
14
 
24
15
  .chartkit-d3-bar-x__label {
25
16
  fill: var(--g-color-text-complementary);
17
+ user-select: none;
26
18
  }
@@ -1,13 +1,13 @@
1
- import type { TooltipHoveredData } from '../../../../../types/widget-data';
1
+ import type { Dispatch } from 'd3';
2
+ import type { TooltipDataChunk } from '../../../../../types/widget-data';
2
3
  import { PreparedTooltip } from '../useChartOptions/types';
3
- import type { PointerPosition, OnSeriesMouseMove, OnSeriesMouseLeave } from './types';
4
+ import type { PointerPosition } from './types';
4
5
  type Args = {
6
+ dispatcher: Dispatch<object>;
5
7
  tooltip: PreparedTooltip;
6
8
  };
7
- export declare const useTooltip: ({ tooltip }: Args) => {
8
- hovered: TooltipHoveredData<any> | undefined;
9
+ export declare const useTooltip: ({ dispatcher, tooltip }: Args) => {
10
+ hovered: TooltipDataChunk<any>[] | undefined;
9
11
  pointerPosition: PointerPosition | undefined;
10
- handleSeriesMouseMove: OnSeriesMouseMove | undefined;
11
- handleSeriesMouseLeave: OnSeriesMouseLeave | undefined;
12
12
  };
13
13
  export {};
@@ -1,19 +1,17 @@
1
1
  import React from 'react';
2
- export const useTooltip = ({ tooltip }) => {
3
- const [hovered, setTooltipHoveredData] = React.useState();
4
- const [pointerPosition, setPointerPosition] = React.useState();
5
- const handleSeriesMouseMove = React.useCallback(({ pointerPosition: nextPointerPosition, hovered: nextHovered }) => {
6
- setTooltipHoveredData(nextHovered);
7
- setPointerPosition(nextPointerPosition);
8
- }, []);
9
- const handleSeriesMouseLeave = React.useCallback(() => {
10
- setTooltipHoveredData(undefined);
11
- setPointerPosition(undefined);
12
- }, []);
13
- return {
14
- hovered,
15
- pointerPosition,
16
- handleSeriesMouseMove: tooltip.enabled ? handleSeriesMouseMove : undefined,
17
- handleSeriesMouseLeave: tooltip.enabled ? handleSeriesMouseLeave : undefined,
18
- };
2
+ export const useTooltip = ({ dispatcher, tooltip }) => {
3
+ const [{ hovered, pointerPosition }, setTooltipState] = React.useState({});
4
+ React.useEffect(() => {
5
+ if (tooltip === null || tooltip === void 0 ? void 0 : tooltip.enabled) {
6
+ dispatcher.on('hover-shape.tooltip', (nextHovered, nextPointerPosition) => {
7
+ setTooltipState({ hovered: nextHovered, pointerPosition: nextPointerPosition });
8
+ });
9
+ }
10
+ return () => {
11
+ if (tooltip === null || tooltip === void 0 ? void 0 : tooltip.enabled) {
12
+ dispatcher.on('hover-shape.tooltip', null);
13
+ }
14
+ };
15
+ }, [dispatcher, tooltip]);
16
+ return { hovered, pointerPosition };
19
17
  };
@@ -1,7 +1 @@
1
- import type { TooltipHoveredData } from '../../../../../types/widget-data';
2
1
  export type PointerPosition = [number, number];
3
- export type OnSeriesMouseMove = (args: {
4
- hovered: TooltipHoveredData;
5
- pointerPosition?: PointerPosition;
6
- }) => void;
7
- export type OnSeriesMouseLeave = () => void;
@@ -9,8 +9,10 @@ type AxisBottomArgs = {
9
9
  labelsPaddings?: number;
10
10
  labelsMargin?: number;
11
11
  labelsStyle?: BaseTextStyle;
12
+ labelsMaxWidth?: number;
13
+ labelsLineHeight: number;
12
14
  size: number;
13
- autoRotation?: boolean;
15
+ rotation: number;
14
16
  };
15
17
  domain: {
16
18
  size: number;
@@ -1,6 +1,7 @@
1
1
  import { select } from 'd3';
2
2
  import { getXAxisItems, getXAxisOffset, getXTickPosition } from '../axis';
3
- import { hasOverlappingLabels, setEllipsisForOverflowText } from '../text';
3
+ import { getLabelsMaxHeight, setEllipsisForOverflowText } from '../text';
4
+ import { calculateCos, calculateSin } from '../math';
4
5
  function addDomain(selection, options) {
5
6
  const { size, color } = options;
6
7
  selection
@@ -13,15 +14,27 @@ function addDomain(selection, options) {
13
14
  .attr('d', `M0,0V0H${size}`);
14
15
  }
15
16
  export function axisBottom(args) {
16
- const { scale, ticks: { labelFormat, labelsPaddings = 0, labelsMargin = 0, labelsStyle, size: tickSize, count: ticksCount, maxTickCount, autoRotation = true, }, domain: { size: domainSize, color: domainColor }, } = args;
17
+ const { scale, ticks: { labelFormat, labelsPaddings = 0, labelsMargin = 0, labelsMaxWidth = Infinity, labelsStyle, labelsLineHeight, size: tickSize, count: ticksCount, maxTickCount, rotation, }, domain: { size: domainSize, color: domainColor }, } = args;
17
18
  const offset = getXAxisOffset();
18
- const spacing = Math.max(tickSize, 0) + labelsMargin;
19
19
  const position = getXTickPosition({ scale, offset });
20
20
  const values = getXAxisItems({ scale, count: ticksCount, maxCount: maxTickCount });
21
+ const labelHeight = getLabelsMaxHeight({
22
+ labels: values,
23
+ style: { 'font-size': (labelsStyle === null || labelsStyle === void 0 ? void 0 : labelsStyle.fontSize) || '' },
24
+ });
21
25
  return function (selection) {
22
- var _a, _b, _c, _e;
26
+ var _a, _b;
23
27
  const x = ((_b = (_a = selection.node()) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect()) === null || _b === void 0 ? void 0 : _b.x) || 0;
24
28
  const right = x + domainSize;
29
+ let transform = `translate(0, ${labelHeight + labelsMargin}px)`;
30
+ if (rotation) {
31
+ const labelsOffsetTop = labelHeight * calculateCos(rotation) + labelsMargin;
32
+ let labelsOffsetLeft = calculateSin(rotation) * labelHeight;
33
+ if (Math.abs(rotation) % 360 === 90) {
34
+ labelsOffsetLeft += ((rotation > 0 ? -1 : 1) * labelHeight) / 2;
35
+ }
36
+ transform = `translate(${-labelsOffsetLeft}px, ${labelsOffsetTop}px) rotate(${rotation}deg)`;
37
+ }
25
38
  selection
26
39
  .selectAll('.tick')
27
40
  .data(values)
@@ -30,10 +43,16 @@ export function axisBottom(args) {
30
43
  const tick = el.append('g').attr('class', 'tick');
31
44
  tick.append('line').attr('stroke', 'currentColor').attr('y2', tickSize);
32
45
  tick.append('text')
46
+ .text(labelFormat)
33
47
  .attr('fill', 'currentColor')
34
- .attr('y', spacing)
35
- .attr('dy', '0.71em')
36
- .text(labelFormat);
48
+ .attr('text-anchor', () => {
49
+ if (rotation) {
50
+ return rotation > 0 ? 'start' : 'end';
51
+ }
52
+ return 'middle';
53
+ })
54
+ .style('transform', transform)
55
+ .style('alignment-baseline', 'after-edge');
37
56
  return tick;
38
57
  })
39
58
  .attr('transform', function (d) {
@@ -48,20 +67,12 @@ export function axisBottom(args) {
48
67
  .select('line')
49
68
  .remove();
50
69
  const labels = selection.selectAll('.tick text');
51
- const labelNodes = labels.nodes();
52
- const overlapping = hasOverlappingLabels({
53
- width: domainSize,
54
- labels: values.map(labelFormat),
55
- padding: labelsPaddings,
56
- style: labelsStyle,
57
- });
58
- const rotationAngle = overlapping && autoRotation ? '-45' : undefined;
59
- if (rotationAngle) {
60
- const labelHeight = (_e = (_c = labelNodes[0]) === null || _c === void 0 ? void 0 : _c.getBoundingClientRect()) === null || _e === void 0 ? void 0 : _e.height;
61
- const labelOffset = (labelHeight / 2 + labelsMargin) / 2;
62
- labels
63
- .attr('text-anchor', 'end')
64
- .attr('transform', `rotate(${rotationAngle}) translate(-${labelOffset}, -${labelOffset})`);
70
+ // FIXME: handle rotated overlapping labels (with a smarter approach)
71
+ if (rotation) {
72
+ const maxWidth = labelsMaxWidth * calculateCos(rotation) + labelsLineHeight * calculateSin(rotation);
73
+ labels.each(function () {
74
+ setEllipsisForOverflowText(select(this), maxWidth);
75
+ });
65
76
  }
66
77
  else {
67
78
  // remove overlapping labels
@@ -98,7 +109,6 @@ export function axisBottom(args) {
98
109
  }
99
110
  selection
100
111
  .call(addDomain, { size: domainSize, color: domainColor })
101
- .attr('text-anchor', 'middle')
102
112
  .style('font-size', (labelsStyle === null || labelsStyle === void 0 ? void 0 : labelsStyle.fontSize) || '');
103
113
  };
104
114
  }
@@ -6,6 +6,9 @@ export * from './text';
6
6
  export * from './time';
7
7
  export * from './axis';
8
8
  export type AxisDirection = 'x' | 'y';
9
+ export type NodeWithD3Data<T = unknown> = Element & {
10
+ __data__: T;
11
+ };
9
12
  type UnknownSeries = {
10
13
  type: ChartKitWidgetSeries['type'];
11
14
  data: unknown;
@@ -68,3 +71,5 @@ export declare const getDataCategoryValue: (args: {
68
71
  data: ChartKitWidgetSeriesData;
69
72
  }) => string;
70
73
  export declare function getClosestPointsRange(axis: PreparedAxis, points: AxisDomain[]): number | undefined;
74
+ export declare const isNodeContainsD3Data: (node?: Element | null) => node is NodeWithD3Data<unknown>;
75
+ export declare const extractD3DataFromNode: <T extends unknown>(node: NodeWithD3Data<T>) => T;
@@ -120,18 +120,16 @@ export const formatAxisTickLabel = (args) => {
120
120
  * @return {number} The height of the text element.
121
121
  */
122
122
  export const getHorisontalSvgTextHeight = (args) => {
123
+ var _a;
123
124
  const { text, style } = args;
124
- const textSelection = select(document.body).append('text').text(text);
125
+ const container = select(document.body).append('svg');
126
+ const textSelection = container.append('text').text(text);
125
127
  const fontSize = get(style, 'fontSize', DEFAULT_AXIS_LABEL_FONT_SIZE);
126
- let height = 0;
127
128
  if (fontSize) {
128
- textSelection.style('font-size', fontSize);
129
+ textSelection.style('font-size', fontSize).style('alignment-baseline', 'after-edge');
129
130
  }
130
- textSelection
131
- .each(function () {
132
- height = this.getBoundingClientRect().height;
133
- })
134
- .remove();
131
+ const height = ((_a = textSelection.node()) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect().height) || 0;
132
+ container.remove();
135
133
  return height;
136
134
  };
137
135
  const extractCategoryValue = (args) => {
@@ -163,3 +161,10 @@ export function getClosestPointsRange(axis, points) {
163
161
  }
164
162
  return points[1] - points[0];
165
163
  }
164
+ // https://d3js.org/d3-selection/joining#selection_data
165
+ export const isNodeContainsD3Data = (node) => {
166
+ return Boolean(node && '__data__' in node);
167
+ };
168
+ export const extractD3DataFromNode = (node) => {
169
+ return node.__data__;
170
+ };
@@ -21,3 +21,5 @@ export declare const calculateNumericProperty: (args: {
21
21
  value?: string | number | null;
22
22
  base?: number;
23
23
  }) => number | undefined;
24
+ export declare function calculateCos(deg: number, precision?: number): number;
25
+ export declare function calculateSin(deg: number, precision?: number): number;
@@ -41,3 +41,11 @@ export const calculateNumericProperty = (args) => {
41
41
  }
42
42
  return value;
43
43
  };
44
+ export function calculateCos(deg, precision = 2) {
45
+ const factor = Math.pow(10, precision);
46
+ return Math.floor(Math.cos((Math.PI / 180) * deg) * factor) / factor;
47
+ }
48
+ export function calculateSin(deg, precision = 2) {
49
+ const factor = Math.pow(10, precision);
50
+ return Math.floor(Math.sin((Math.PI / 180) * deg) * factor) / factor;
51
+ }