@gravity-ui/chartkit 4.6.0 → 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 (99) 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/D3Widget.js +11 -1
  16. package/build/plugins/d3/renderer/components/AxisX.d.ts +1 -2
  17. package/build/plugins/d3/renderer/components/AxisX.js +8 -21
  18. package/build/plugins/d3/renderer/components/AxisY.js +50 -18
  19. package/build/plugins/d3/renderer/components/Chart.js +25 -17
  20. package/build/plugins/d3/renderer/components/Legend.js +20 -22
  21. package/build/plugins/d3/renderer/components/Title.js +1 -1
  22. package/build/plugins/d3/renderer/components/Tooltip/DefaultContent.d.ts +2 -2
  23. package/build/plugins/d3/renderer/components/Tooltip/DefaultContent.js +8 -0
  24. package/build/plugins/d3/renderer/components/Tooltip/TooltipTriggerArea.d.ts +14 -0
  25. package/build/plugins/d3/renderer/components/Tooltip/TooltipTriggerArea.js +70 -0
  26. package/build/plugins/d3/renderer/components/Tooltip/index.d.ts +5 -3
  27. package/build/plugins/d3/renderer/components/Tooltip/index.js +4 -2
  28. package/build/plugins/d3/renderer/components/styles.css +3 -0
  29. package/build/plugins/d3/renderer/constants/defaults/axis.d.ts +9 -0
  30. package/build/plugins/d3/renderer/constants/defaults/axis.js +6 -0
  31. package/build/plugins/d3/renderer/constants/defaults/index.d.ts +1 -0
  32. package/build/plugins/d3/renderer/constants/defaults/index.js +1 -0
  33. package/build/plugins/d3/renderer/constants/defaults/series-options.d.ts +11 -0
  34. package/build/plugins/d3/renderer/constants/defaults/series-options.js +41 -0
  35. package/build/plugins/d3/renderer/constants/index.d.ts +0 -1
  36. package/build/plugins/d3/renderer/constants/index.js +0 -1
  37. package/build/plugins/d3/renderer/d3-dispatcher.d.ts +1 -0
  38. package/build/plugins/d3/renderer/d3-dispatcher.js +4 -0
  39. package/build/plugins/d3/renderer/hooks/index.d.ts +0 -1
  40. package/build/plugins/d3/renderer/hooks/index.js +0 -1
  41. package/build/plugins/d3/renderer/hooks/useAxisScales/index.d.ts +6 -5
  42. package/build/plugins/d3/renderer/hooks/useAxisScales/index.js +2 -1
  43. package/build/plugins/d3/renderer/hooks/useChartDimensions/index.js +8 -41
  44. package/build/plugins/d3/renderer/hooks/useChartDimensions/utils.d.ts +3 -0
  45. package/build/plugins/d3/renderer/hooks/useChartDimensions/utils.js +17 -4
  46. package/build/plugins/d3/renderer/hooks/useChartOptions/chart.d.ts +2 -4
  47. package/build/plugins/d3/renderer/hooks/useChartOptions/chart.js +4 -23
  48. package/build/plugins/d3/renderer/hooks/useChartOptions/index.d.ts +1 -1
  49. package/build/plugins/d3/renderer/hooks/useChartOptions/index.js +2 -10
  50. package/build/plugins/d3/renderer/hooks/useChartOptions/title.d.ts +1 -1
  51. package/build/plugins/d3/renderer/hooks/useChartOptions/title.js +1 -0
  52. package/build/plugins/d3/renderer/hooks/useChartOptions/types.d.ts +8 -5
  53. package/build/plugins/d3/renderer/hooks/useChartOptions/x-axis.d.ts +5 -3
  54. package/build/plugins/d3/renderer/hooks/useChartOptions/x-axis.js +61 -6
  55. package/build/plugins/d3/renderer/hooks/useChartOptions/y-axis.d.ts +1 -1
  56. package/build/plugins/d3/renderer/hooks/useChartOptions/y-axis.js +20 -12
  57. package/build/plugins/d3/renderer/hooks/useSeries/index.d.ts +1 -0
  58. package/build/plugins/d3/renderer/hooks/useSeries/index.js +8 -2
  59. package/build/plugins/d3/renderer/hooks/useSeries/prepare-legend.js +8 -3
  60. package/build/plugins/d3/renderer/hooks/useSeries/prepare-options.d.ts +3 -0
  61. package/build/plugins/d3/renderer/hooks/useSeries/prepare-options.js +5 -0
  62. package/build/plugins/d3/renderer/hooks/useSeries/prepareSeries.js +6 -3
  63. package/build/plugins/d3/renderer/hooks/useSeries/types.d.ts +5 -1
  64. package/build/plugins/d3/renderer/hooks/useShapes/bar-x/index.d.ts +12 -0
  65. package/build/plugins/d3/renderer/hooks/useShapes/bar-x/index.js +91 -0
  66. package/build/plugins/d3/renderer/hooks/useShapes/bar-x/prepare-data.d.ts +19 -0
  67. package/build/plugins/d3/renderer/hooks/useShapes/{bar-x.js → bar-x/prepare-data.js} +9 -88
  68. package/build/plugins/d3/renderer/hooks/useShapes/index.d.ts +13 -10
  69. package/build/plugins/d3/renderer/hooks/useShapes/index.js +28 -13
  70. package/build/plugins/d3/renderer/hooks/useShapes/pie.d.ts +6 -4
  71. package/build/plugins/d3/renderer/hooks/useShapes/pie.js +98 -20
  72. package/build/plugins/d3/renderer/hooks/useShapes/scatter/index.d.ts +15 -0
  73. package/build/plugins/d3/renderer/hooks/useShapes/scatter/index.js +89 -0
  74. package/build/plugins/d3/renderer/hooks/useShapes/scatter/prepare-data.d.ts +19 -0
  75. package/build/plugins/d3/renderer/hooks/useShapes/scatter/prepare-data.js +55 -0
  76. package/build/plugins/d3/renderer/hooks/useShapes/styles.css +1 -9
  77. package/build/plugins/d3/renderer/hooks/useTooltip/index.d.ts +6 -6
  78. package/build/plugins/d3/renderer/hooks/useTooltip/index.js +15 -17
  79. package/build/plugins/d3/renderer/hooks/useTooltip/types.d.ts +0 -6
  80. package/build/plugins/d3/renderer/utils/axis-generators/bottom.d.ts +3 -1
  81. package/build/plugins/d3/renderer/utils/axis-generators/bottom.js +77 -38
  82. package/build/plugins/d3/renderer/utils/axis.js +0 -6
  83. package/build/plugins/d3/renderer/utils/index.d.ts +5 -0
  84. package/build/plugins/d3/renderer/utils/index.js +13 -8
  85. package/build/plugins/d3/renderer/utils/math.d.ts +2 -0
  86. package/build/plugins/d3/renderer/utils/math.js +8 -0
  87. package/build/plugins/d3/renderer/utils/text.d.ts +6 -6
  88. package/build/plugins/d3/renderer/utils/text.js +25 -15
  89. package/build/types/widget-data/axis.d.ts +10 -0
  90. package/build/types/widget-data/series.d.ts +51 -0
  91. package/build/types/widget-data/tooltip.d.ts +18 -7
  92. package/package.json +2 -2
  93. package/build/plugins/d3/renderer/hooks/useChartEvents/index.d.ts +0 -5
  94. package/build/plugins/d3/renderer/hooks/useChartEvents/index.js +0 -15
  95. package/build/plugins/d3/renderer/hooks/useShapes/bar-x.d.ts +0 -21
  96. package/build/plugins/d3/renderer/hooks/useShapes/defaults.d.ts +0 -5
  97. package/build/plugins/d3/renderer/hooks/useShapes/defaults.js +0 -5
  98. package/build/plugins/d3/renderer/hooks/useShapes/scatter.d.ts +0 -19
  99. package/build/plugins/d3/renderer/hooks/useShapes/scatter.js +0 -89
@@ -0,0 +1,2 @@
1
+ import React from 'react';
2
+ export declare const BasicPie: () => React.JSX.Element;
@@ -0,0 +1,30 @@
1
+ import React from 'react';
2
+ import { ChartKit } from '../../../../components/ChartKit';
3
+ import nintendoGames from '../nintendoGames';
4
+ import { groups } from 'd3';
5
+ function prepareData() {
6
+ const gamesByPlatform = groups(nintendoGames, (item) => item.platform);
7
+ return gamesByPlatform.map(([platform, games]) => ({
8
+ name: platform,
9
+ value: games.length,
10
+ }));
11
+ }
12
+ export const BasicPie = () => {
13
+ const data = prepareData();
14
+ const widgetData = {
15
+ series: {
16
+ data: [
17
+ {
18
+ type: 'pie',
19
+ data: data,
20
+ },
21
+ ],
22
+ },
23
+ legend: { enabled: true },
24
+ title: {
25
+ text: 'Platforms',
26
+ style: { fontSize: '12px', fontWeight: 'normal' },
27
+ },
28
+ };
29
+ return React.createElement(ChartKit, { type: "d3", data: widgetData });
30
+ };
@@ -0,0 +1,2 @@
1
+ import React from 'react';
2
+ export declare const Donut: () => React.JSX.Element;
@@ -0,0 +1,31 @@
1
+ import React from 'react';
2
+ import { ChartKit } from '../../../../components/ChartKit';
3
+ import nintendoGames from '../nintendoGames';
4
+ import { groups } from 'd3';
5
+ function prepareData() {
6
+ const gamesByPlatform = groups(nintendoGames, (d) => d.esrb_rating || 'unknown');
7
+ return gamesByPlatform.map(([value, games]) => ({
8
+ name: value,
9
+ value: games.length,
10
+ }));
11
+ }
12
+ export const Donut = () => {
13
+ const data = prepareData();
14
+ const widgetData = {
15
+ series: {
16
+ data: [
17
+ {
18
+ type: 'pie',
19
+ innerRadius: '50%',
20
+ data: data,
21
+ },
22
+ ],
23
+ },
24
+ legend: { enabled: true },
25
+ title: {
26
+ text: 'ESRB ratings',
27
+ style: { fontSize: '12px', fontWeight: 'normal' },
28
+ },
29
+ };
30
+ return React.createElement(ChartKit, { type: "d3", data: widgetData });
31
+ };
@@ -0,0 +1,2 @@
1
+ import React from 'react';
2
+ export declare const Basic: () => React.JSX.Element;
@@ -0,0 +1,66 @@
1
+ import React from 'react';
2
+ import { ChartKit } from '../../../../components/ChartKit';
3
+ import nintendoGames from '../nintendoGames';
4
+ import { dateTime } from '@gravity-ui/date-utils';
5
+ function prepareData() {
6
+ const dataset = nintendoGames.filter((d) => d.date && d.user_score);
7
+ const data = dataset.map((d) => ({
8
+ x: d.date || undefined,
9
+ y: d.user_score || undefined,
10
+ custom: d,
11
+ }));
12
+ return {
13
+ series: [
14
+ {
15
+ data,
16
+ name: 'Nintendo games',
17
+ },
18
+ ],
19
+ };
20
+ }
21
+ export const Basic = () => {
22
+ const { series } = prepareData();
23
+ const widgetData = {
24
+ series: {
25
+ data: series.map((s) => ({
26
+ type: 'scatter',
27
+ data: s.data.filter((d) => d.x),
28
+ name: s.name,
29
+ })),
30
+ },
31
+ yAxis: [
32
+ {
33
+ title: {
34
+ text: 'User score',
35
+ },
36
+ },
37
+ ],
38
+ xAxis: {
39
+ type: 'datetime',
40
+ title: {
41
+ text: 'Release dates',
42
+ },
43
+ },
44
+ tooltip: {
45
+ renderer: (d) => {
46
+ var _a;
47
+ const point = (_a = d.hovered[0]) === null || _a === void 0 ? void 0 : _a.data;
48
+ if (!point) {
49
+ return null;
50
+ }
51
+ const title = point.custom.title;
52
+ const score = point.custom.user_score;
53
+ const date = dateTime({ input: point.custom.date }).format('DD MMM YYYY');
54
+ return (React.createElement(React.Fragment, null,
55
+ React.createElement("b", null, title),
56
+ React.createElement("br", null),
57
+ "Release date: ",
58
+ date,
59
+ React.createElement("br", null),
60
+ "User score: ",
61
+ score));
62
+ },
63
+ },
64
+ };
65
+ return React.createElement(ChartKit, { type: "d3", data: widgetData });
66
+ };
@@ -4,9 +4,19 @@ import debounce from 'lodash/debounce';
4
4
  import { getRandomCKId } from '../../../utils';
5
5
  import { Chart } from './components';
6
6
  const D3Widget = React.forwardRef(function D3Widget(props, forwardedRef) {
7
+ const { data, onLoad, onRender } = props;
7
8
  const ref = React.useRef(null);
8
9
  const debounced = React.useRef();
9
10
  const [dimensions, setDimensions] = React.useState();
11
+ //FIXME: add chartPerfomance data to callbacks;
12
+ React.useLayoutEffect(() => {
13
+ if (onLoad) {
14
+ onLoad({});
15
+ }
16
+ if (onRender) {
17
+ onRender({});
18
+ }
19
+ }, []);
10
20
  const handleResize = React.useCallback(() => {
11
21
  var _a;
12
22
  const parentElement = (_a = ref.current) === null || _a === void 0 ? void 0 : _a.parentElement;
@@ -44,6 +54,6 @@ const D3Widget = React.forwardRef(function D3Widget(props, forwardedRef) {
44
54
  width: (dimensions === null || dimensions === void 0 ? void 0 : dimensions.width) || '100%',
45
55
  height: (dimensions === null || dimensions === void 0 ? void 0 : dimensions.height) || '100%',
46
56
  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 }))));
57
+ } }, (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: data }))));
48
58
  });
49
59
  export default D3Widget;
@@ -5,7 +5,6 @@ type Props = {
5
5
  width: number;
6
6
  height: number;
7
7
  scale: ChartScale;
8
- chartWidth: number;
9
8
  };
10
- export declare const AxisX: React.MemoExoticComponent<({ axis, width, height, scale, chartWidth }: Props) => React.JSX.Element>;
9
+ export declare const AxisX: React.MemoExoticComponent<({ axis, width, height, scale }: Props) => React.JSX.Element>;
11
10
  export {};
@@ -18,14 +18,12 @@ function getLabelFormatter({ axis, scale }) {
18
18
  });
19
19
  };
20
20
  }
21
- export const AxisX = React.memo(({ axis, width, height, scale, chartWidth }) => {
21
+ export const AxisX = React.memo(({ axis, width, height, scale }) => {
22
22
  const ref = React.useRef(null);
23
23
  React.useEffect(() => {
24
24
  if (!ref.current) {
25
25
  return;
26
26
  }
27
- const svgElement = select(ref.current);
28
- svgElement.selectAll('*').remove();
29
27
  const xAxisGenerator = axisBottom({
30
28
  scale: scale,
31
29
  ticks: {
@@ -34,40 +32,29 @@ export const AxisX = React.memo(({ axis, width, height, scale, chartWidth }) =>
34
32
  labelsPaddings: axis.labels.padding,
35
33
  labelsMargin: axis.labels.margin,
36
34
  labelsStyle: axis.labels.style,
35
+ labelsMaxWidth: axis.labels.maxWidth,
36
+ labelsLineHeight: axis.labels.lineHeight,
37
37
  count: getTicksCount({ axis, range: width }),
38
38
  maxTickCount: getMaxTickCount({ axis, width }),
39
- autoRotation: axis.labels.autoRotation,
39
+ rotation: axis.labels.rotation,
40
40
  },
41
41
  domain: {
42
42
  size: width,
43
43
  color: axis.lineColor,
44
44
  },
45
45
  });
46
+ const svgElement = select(ref.current);
47
+ svgElement.selectAll('*').remove();
46
48
  svgElement.call(xAxisGenerator).attr('class', b());
47
- if (axis.labels.enabled) {
48
- svgElement.style('font-size', axis.labels.style.fontSize);
49
- }
50
- // add an ellipsis to the labels on the right that go beyond the boundaries of the chart
51
- svgElement.selectAll('.tick text').each(function () {
52
- var _a;
53
- const node = this;
54
- const textRect = node.getBBox();
55
- const matrix = ((_a = node.transform.baseVal.consolidate()) === null || _a === void 0 ? void 0 : _a.matrix) || {};
56
- const right = matrix.a * textRect.right + matrix.c * textRect.bottom + matrix.e;
57
- if (right > chartWidth) {
58
- const maxWidth = textRect.width - (right - chartWidth) * 2;
59
- select(node).call(setEllipsisForOverflowText, maxWidth);
60
- }
61
- });
62
49
  // add an axis header if necessary
63
50
  if (axis.title.text) {
64
- const textY = axis.title.height + parseInt(axis.labels.style.fontSize) + axis.labels.padding;
51
+ const y = axis.title.height + axis.title.margin + axis.labels.height + axis.labels.margin;
65
52
  svgElement
66
53
  .append('text')
67
54
  .attr('class', b('title'))
68
55
  .attr('text-anchor', 'middle')
69
56
  .attr('x', width / 2)
70
- .attr('y', textY)
57
+ .attr('y', y)
71
58
  .attr('font-size', axis.title.style.fontSize)
72
59
  .text(axis.title.text)
73
60
  .call(setEllipsisForOverflowText, width);
@@ -1,9 +1,29 @@
1
1
  import React from 'react';
2
2
  import { axisLeft, select } from 'd3';
3
3
  import { block } from '../../../../utils/cn';
4
- import { formatAxisTickLabel, getClosestPointsRange, parseTransformStyle, setEllipsisForOverflowText, setEllipsisForOverflowTexts, getTicksCount, getScaleTicks, } from '../utils';
4
+ import { formatAxisTickLabel, getClosestPointsRange, parseTransformStyle, setEllipsisForOverflowText, setEllipsisForOverflowTexts, getTicksCount, getScaleTicks, calculateSin, calculateCos, } from '../utils';
5
5
  const b = block('d3-axis');
6
- const MAX_WIDTH = 80;
6
+ function transformLabel(node, axis) {
7
+ let topOffset = axis.labels.lineHeight / 2;
8
+ let leftOffset = -axis.labels.margin;
9
+ if (axis.labels.rotation) {
10
+ if (axis.labels.rotation > 0) {
11
+ leftOffset -= axis.labels.lineHeight * calculateSin(axis.labels.rotation);
12
+ topOffset = axis.labels.lineHeight * calculateCos(axis.labels.rotation);
13
+ if (axis.labels.rotation % 360 === 90) {
14
+ topOffset = ((node === null || node === void 0 ? void 0 : node.getBoundingClientRect().width) || 0) / 2;
15
+ }
16
+ }
17
+ else {
18
+ topOffset = 0;
19
+ if (axis.labels.rotation % 360 === -90) {
20
+ topOffset = -((node === null || node === void 0 ? void 0 : node.getBoundingClientRect().width) || 0) / 2;
21
+ }
22
+ }
23
+ return `translate(${leftOffset}px, ${topOffset}px) rotate(${axis.labels.rotation}deg)`;
24
+ }
25
+ return `translate(${leftOffset}px, ${topOffset}px)`;
26
+ }
7
27
  export const AxisY = ({ axises, width, height, scale }) => {
8
28
  const ref = React.useRef(null);
9
29
  React.useEffect(() => {
@@ -40,9 +60,19 @@ export const AxisY = ({ axises, width, height, scale }) => {
40
60
  if (axis.labels.enabled) {
41
61
  const tickTexts = svgElement
42
62
  .selectAll('.tick text')
63
+ // The offset must be applied before the labels are rotated.
64
+ // Therefore, we reset the values and make an offset in transform attribute.
65
+ // FIXME: give up axisLeft(d3) and switch to our own generation method
66
+ .attr('x', null)
67
+ .attr('dy', null)
43
68
  .style('font-size', axis.labels.style.fontSize)
44
- .style('transform', 'translateY(-1px)');
45
- tickTexts.call(setEllipsisForOverflowTexts, MAX_WIDTH);
69
+ .style('transform', function () {
70
+ return transformLabel(this, axis);
71
+ });
72
+ const textMaxWidth = !axis.labels.rotation || Math.abs(axis.labels.rotation) % 360 !== 90
73
+ ? axis.labels.maxWidth
74
+ : (height - axis.labels.padding * (tickTexts.size() - 1)) / tickTexts.size();
75
+ tickTexts.call(setEllipsisForOverflowTexts, textMaxWidth);
46
76
  }
47
77
  const transformStyle = svgElement.select('.tick').attr('transform');
48
78
  const { y } = parseTransformStyle(transformStyle);
@@ -52,21 +82,23 @@ export const AxisY = ({ axises, width, height, scale }) => {
52
82
  }
53
83
  // remove overlapping ticks
54
84
  // Note: this method do not prepared for rotated labels
55
- let elementY = 0;
56
- svgElement
57
- .selectAll('.tick')
58
- .filter(function (_d, index) {
59
- const node = this;
60
- const r = node.getBoundingClientRect();
61
- if (r.bottom > elementY && index !== 0) {
62
- return true;
63
- }
64
- elementY = r.top - axis.labels.padding;
65
- return false;
66
- })
67
- .remove();
85
+ if (!axis.labels.rotation) {
86
+ let elementY = 0;
87
+ svgElement
88
+ .selectAll('.tick')
89
+ .filter(function (_d, index) {
90
+ const node = this;
91
+ const r = node.getBoundingClientRect();
92
+ if (r.bottom > elementY && index !== 0) {
93
+ return true;
94
+ }
95
+ elementY = r.top - axis.labels.padding;
96
+ return false;
97
+ })
98
+ .remove();
99
+ }
68
100
  if (axis.title.text) {
69
- const textY = axis.title.height + axis.labels.margin;
101
+ const textY = axis.title.margin + axis.labels.margin + axis.labels.width;
70
102
  svgElement
71
103
  .append('text')
72
104
  .attr('class', b('title'))
@@ -1,22 +1,30 @@
1
1
  import React from 'react';
2
2
  import { block } from '../../../../utils/cn';
3
- import { useAxisScales, useChartDimensions, useChartEvents, useChartOptions, useSeries, useShapes, useTooltip, } from '../hooks';
3
+ import { getD3Dispatcher } from '../d3-dispatcher';
4
+ import { useAxisScales, useChartDimensions, useChartOptions, useSeries, useShapes, useTooltip, } from '../hooks';
4
5
  import { AxisY } from './AxisY';
5
6
  import { AxisX } from './AxisX';
6
7
  import { Legend } from './Legend';
7
8
  import { Title } from './Title';
8
- import { Tooltip } from './Tooltip';
9
+ import { Tooltip, TooltipTriggerArea } from './Tooltip';
10
+ import { getPreparedXAxis } from '../hooks/useChartOptions/x-axis';
11
+ import { getWidthOccupiedByYAxis } from '../hooks/useChartDimensions/utils';
12
+ import { getPreparedYAxis } from '../hooks/useChartOptions/y-axis';
9
13
  import './styles.css';
10
14
  const b = block('d3');
11
15
  export const Chart = (props) => {
12
16
  // FIXME: add data validation
13
17
  const { top, left, width, height, data } = props;
14
18
  const svgRef = React.createRef();
15
- const { chartHovered, handleMouseEnter, handleMouseLeave } = useChartEvents();
16
- const { chart, title, tooltip, xAxis, yAxis } = useChartOptions({
19
+ const dispatcher = React.useMemo(() => {
20
+ return getD3Dispatcher();
21
+ }, []);
22
+ const { chart, title, tooltip } = useChartOptions({
17
23
  data,
18
24
  });
19
- const { legendItems, legendConfig, preparedSeries, preparedLegend, handleLegendItemClick } = useSeries({
25
+ const xAxis = React.useMemo(() => getPreparedXAxis({ xAxis: data.xAxis, width, series: data.series.data }), [data, width]);
26
+ const yAxis = React.useMemo(() => getPreparedYAxis({ series: data.series.data, yAxis: data.yAxis }), [data, width]);
27
+ const { legendItems, legendConfig, preparedSeries, preparedSeriesOptions, preparedLegend, handleLegendItemClick, } = useSeries({
20
28
  chartWidth: width,
21
29
  chartHeight: height,
22
30
  chartMargin: chart.margin,
@@ -40,33 +48,33 @@ export const Chart = (props) => {
40
48
  xAxis,
41
49
  yAxis,
42
50
  });
43
- const { hovered, pointerPosition, handleSeriesMouseMove, handleSeriesMouseLeave } = useTooltip({
44
- tooltip,
45
- });
46
- const { shapes } = useShapes({
51
+ const { hovered, pointerPosition } = useTooltip({ dispatcher, tooltip });
52
+ const { shapes, shapesData } = useShapes({
47
53
  top,
48
54
  left,
49
55
  boundsWidth,
50
56
  boundsHeight,
57
+ dispatcher,
51
58
  series: preparedSeries,
52
- seriesOptions: data.series.options,
59
+ seriesOptions: preparedSeriesOptions,
53
60
  xAxis,
54
61
  xScale,
55
62
  yAxis,
56
63
  yScale,
57
64
  svgContainer: svgRef.current,
58
- onSeriesMouseMove: handleSeriesMouseMove,
59
- onSeriesMouseLeave: handleSeriesMouseLeave,
60
65
  });
66
+ const boundsOffsetTop = chart.margin.top;
67
+ const boundsOffsetLeft = chart.margin.left + getWidthOccupiedByYAxis({ preparedAxis: yAxis });
61
68
  return (React.createElement(React.Fragment, null,
62
- React.createElement("svg", { ref: svgRef, className: b({ hovered: chartHovered }), width: width, height: height, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave },
69
+ React.createElement("svg", { ref: svgRef, className: b(), width: width, height: height },
63
70
  title && React.createElement(Title, Object.assign({}, title, { chartWidth: width })),
64
- React.createElement("g", { width: boundsWidth, height: boundsHeight, transform: `translate(${[chart.margin.left, chart.margin.top].join(',')})` },
71
+ React.createElement("g", { width: boundsWidth, height: boundsHeight, transform: `translate(${[boundsOffsetLeft, boundsOffsetTop].join(',')})` },
65
72
  xScale && yScale && (React.createElement(React.Fragment, null,
66
73
  React.createElement(AxisY, { axises: yAxis, width: boundsWidth, height: boundsHeight, scale: yScale }),
67
74
  React.createElement("g", { transform: `translate(0, ${boundsHeight})` },
68
- React.createElement(AxisX, { axis: xAxis, width: boundsWidth, height: boundsHeight, scale: xScale, chartWidth: width })))),
69
- shapes),
75
+ React.createElement(AxisX, { axis: xAxis, width: boundsWidth, height: boundsHeight, scale: xScale })))),
76
+ shapes,
77
+ (tooltip === null || tooltip === void 0 ? void 0 : tooltip.enabled) && Boolean(shapesData.length) && (React.createElement(TooltipTriggerArea, { boundsWidth: boundsWidth, boundsHeight: boundsHeight, dispatcher: dispatcher, offsetLeft: left, offsetTop: top, shapesData: shapesData, svgContainer: svgRef.current }))),
70
78
  preparedLegend.enabled && (React.createElement(Legend, { chartSeries: preparedSeries, boundsWidth: boundsWidth, legend: preparedLegend, items: legendItems, config: legendConfig, onItemClick: handleLegendItemClick }))),
71
- React.createElement(Tooltip, { hovered: hovered, pointerPosition: pointerPosition, tooltip: tooltip, xAxis: xAxis, yAxis: yAxis[0] })));
79
+ React.createElement(Tooltip, { dispatcher: dispatcher, tooltip: tooltip, xAxis: xAxis, yAxis: yAxis[0], hovered: hovered, pointerPosition: pointerPosition })));
72
80
  };
@@ -76,7 +76,6 @@ export const Legend = (props) => {
76
76
  : items;
77
77
  pageItems.forEach((line, lineIndex) => {
78
78
  var _a;
79
- const textWidths = [];
80
79
  const legendLine = svgElement.append('g').attr('class', b('line'));
81
80
  const legendItemTemplate = legendLine
82
81
  .selectAll('legend-history')
@@ -86,21 +85,23 @@ export const Legend = (props) => {
86
85
  .attr('class', b('item'))
87
86
  .on('click', function (e, d) {
88
87
  onItemClick({ name: d.name, metaKey: e.metaKey });
89
- })
90
- .each(function (d) {
91
- textWidths.push(d.textWidth);
92
88
  });
89
+ const getXPosition = (i) => {
90
+ return line.slice(0, i).reduce((acc, legendItem) => {
91
+ return (acc +
92
+ legendItem.symbol.width +
93
+ legendItem.symbol.padding +
94
+ legendItem.textWidth +
95
+ legend.itemDistance);
96
+ }, 0);
97
+ };
93
98
  legendItemTemplate
94
99
  .append('rect')
95
- .attr('x', function (legendItem, i) {
96
- return (i * legendItem.symbol.width +
97
- i * legend.itemDistance +
98
- i * legendItem.symbol.padding +
99
- textWidths.slice(0, i).reduce((acc, tw) => acc + tw, 0));
100
+ .attr('x', function (_d, i) {
101
+ return getXPosition(i);
100
102
  })
101
103
  .attr('y', (legendItem) => {
102
- const lineOffset = legend.lineHeight * lineIndex;
103
- return config.offset.top + lineOffset - legendItem.symbol.height / 2;
104
+ return (legend.lineHeight - legendItem.symbol.height) / 2;
104
105
  })
105
106
  .attr('width', (legendItem) => {
106
107
  return legendItem.symbol.width;
@@ -116,14 +117,9 @@ export const Legend = (props) => {
116
117
  legendItemTemplate
117
118
  .append('text')
118
119
  .attr('x', function (legendItem, i) {
119
- return (i * legendItem.symbol.width +
120
- i * legend.itemDistance +
121
- i * legendItem.symbol.padding +
122
- legendItem.symbol.width +
123
- legendItem.symbol.padding +
124
- textWidths.slice(0, i).reduce((acc, tw) => acc + tw, 0));
120
+ return getXPosition(i) + legendItem.symbol.width + legendItem.symbol.padding;
125
121
  })
126
- .attr('y', config.offset.top + legend.lineHeight * lineIndex)
122
+ .attr('height', legend.lineHeight)
127
123
  .attr('class', function (d) {
128
124
  const mods = { selected: d.visible, unselected: !d.visible };
129
125
  return b('item-text', mods);
@@ -131,8 +127,7 @@ export const Legend = (props) => {
131
127
  .text(function (d) {
132
128
  return ('name' in d && d.name);
133
129
  })
134
- .style('font-size', legend.itemStyle.fontSize)
135
- .style('alignment-baseline', 'middle');
130
+ .style('font-size', legend.itemStyle.fontSize);
136
131
  const contentWidth = ((_a = legendLine.node()) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect().width) || 0;
137
132
  const { left } = getLegendPosition({
138
133
  align: legend.align,
@@ -140,12 +135,15 @@ export const Legend = (props) => {
140
135
  offsetWidth: config.offset.left,
141
136
  contentWidth,
142
137
  });
143
- legendLine.attr('transform', `translate(${[left, 0].join(',')})`);
138
+ const top = config.offset.top + legend.lineHeight * lineIndex;
139
+ legendLine.attr('transform', `translate(${[left, top].join(',')})`);
144
140
  });
145
141
  if (config.pagination) {
146
142
  const transform = `translate(${[
147
143
  config.offset.left,
148
- config.offset.top + legend.lineHeight * config.pagination.limit,
144
+ config.offset.top +
145
+ legend.lineHeight * config.pagination.limit +
146
+ legend.lineHeight / 2,
149
147
  ].join(',')})`;
150
148
  appendPaginator({
151
149
  container: svgElement,
@@ -3,6 +3,6 @@ import { block } from '../../../../utils/cn';
3
3
  const b = block('d3-title');
4
4
  export const Title = (props) => {
5
5
  const { chartWidth, text, height, style } = props;
6
- return (React.createElement("text", { className: b(), dx: chartWidth / 2, dy: height / 2, dominantBaseline: "middle", textAnchor: "middle", style: { fontSize: style === null || style === void 0 ? void 0 : style.fontSize, lineHeight: `${height}px` } },
6
+ return (React.createElement("text", { className: b(), dx: chartWidth / 2, dy: height / 2, dominantBaseline: "middle", textAnchor: "middle", style: Object.assign({ lineHeight: `${height}px` }, style) },
7
7
  React.createElement("tspan", null, text)));
8
8
  };
@@ -1,8 +1,8 @@
1
1
  import React from 'react';
2
- import type { TooltipHoveredData } from '../../../../../types';
2
+ import type { TooltipDataChunk } from '../../../../../types';
3
3
  import type { PreparedAxis } from '../../hooks';
4
4
  type Props = {
5
- hovered: TooltipHoveredData;
5
+ hovered: TooltipDataChunk;
6
6
  xAxis: PreparedAxis;
7
7
  yAxis: PreparedAxis;
8
8
  };
@@ -48,6 +48,14 @@ export const DefaultContent = ({ hovered, xAxis, yAxis }) => {
48
48
  ": ",
49
49
  yRow))));
50
50
  }
51
+ case 'pie': {
52
+ const pieSeries = series;
53
+ return (React.createElement("div", null,
54
+ React.createElement("span", null,
55
+ pieSeries.name || pieSeries.id,
56
+ "\u00A0"),
57
+ React.createElement("span", null, pieSeries.value)));
58
+ }
51
59
  default: {
52
60
  return null;
53
61
  }
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+ import type { Dispatch } from 'd3';
3
+ import type { ShapeData } from '../../hooks';
4
+ type Args = {
5
+ boundsWidth: number;
6
+ boundsHeight: number;
7
+ dispatcher: Dispatch<object>;
8
+ offsetTop: number;
9
+ offsetLeft: number;
10
+ shapesData: ShapeData[];
11
+ svgContainer: SVGSVGElement | null;
12
+ };
13
+ export declare const TooltipTriggerArea: (args: Args) => React.JSX.Element;
14
+ export {};
@@ -0,0 +1,70 @@
1
+ import React from 'react';
2
+ import throttle from 'lodash/throttle';
3
+ import { bisector, pointer, sort } from 'd3';
4
+ import { extractD3DataFromNode, isNodeContainsD3Data } from '../../utils';
5
+ const THROTTLE_DELAY = 50;
6
+ const isNodeContainsData = (node) => {
7
+ return isNodeContainsD3Data(node);
8
+ };
9
+ const getCalculationType = (shapesData) => {
10
+ if (shapesData.every((d) => d.series.type === 'bar-x')) {
11
+ return 'x-primary';
12
+ }
13
+ return 'none';
14
+ };
15
+ export const TooltipTriggerArea = (args) => {
16
+ const { boundsWidth, boundsHeight, dispatcher, offsetTop, offsetLeft, shapesData, svgContainer } = args;
17
+ const rectRef = React.useRef(null);
18
+ const calculationType = React.useMemo(() => {
19
+ return getCalculationType(shapesData);
20
+ }, [shapesData]);
21
+ const xData = React.useMemo(() => {
22
+ return calculationType === 'x-primary'
23
+ ? sort(new Set(shapesData.map((d) => d.x)))
24
+ : [];
25
+ }, [shapesData, calculationType]);
26
+ const handleXprimaryMouseMove = (e) => {
27
+ var _a, _b, _c;
28
+ const { left, top } = ((_a = rectRef.current) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect()) || { left: 0, top: 0 };
29
+ const [pointerX, pointerY] = pointer(e, svgContainer);
30
+ const barWidthOffset = shapesData[0].width / 2;
31
+ const xPosition = pointerX - left - barWidthOffset - window.pageXOffset;
32
+ const xDataIndex = bisector((d) => d).center(xData, xPosition);
33
+ const xNodes = Array.from(((_c = (_b = rectRef.current) === null || _b === void 0 ? void 0 : _b.parentElement) === null || _c === void 0 ? void 0 : _c.querySelectorAll(`[x="${xData[xDataIndex]}"]`)) || []);
34
+ let hoverShapeData;
35
+ if (xNodes.length === 1 && isNodeContainsData(xNodes[0])) {
36
+ hoverShapeData = [extractD3DataFromNode(xNodes[0])];
37
+ }
38
+ else if (xNodes.length > 1 && xNodes.every(isNodeContainsData)) {
39
+ const yPosition = pointerY - top - window.pageYOffset;
40
+ const xyNode = xNodes.find((node, i) => {
41
+ const { y, height } = extractD3DataFromNode(node);
42
+ if (i === xNodes.length - 1) {
43
+ return yPosition <= y + height;
44
+ }
45
+ return yPosition >= y && yPosition <= y + height;
46
+ });
47
+ if (xyNode) {
48
+ hoverShapeData = [extractD3DataFromNode(xyNode)];
49
+ }
50
+ }
51
+ if (hoverShapeData) {
52
+ const position = [pointerX - offsetLeft, pointerY - offsetTop];
53
+ dispatcher.call('hover-shape', e.target, hoverShapeData, position);
54
+ }
55
+ };
56
+ const handleMouseMove = (e) => {
57
+ switch (calculationType) {
58
+ case 'x-primary': {
59
+ handleXprimaryMouseMove(e);
60
+ return;
61
+ }
62
+ }
63
+ };
64
+ const throttledHandleMouseMove = throttle(handleMouseMove, THROTTLE_DELAY);
65
+ const handleMouseLeave = () => {
66
+ throttledHandleMouseMove.cancel();
67
+ dispatcher.call('hover-shape', {}, undefined);
68
+ };
69
+ return (React.createElement("rect", { ref: rectRef, width: boundsWidth, height: boundsHeight, fill: "transparent", onMouseMove: throttledHandleMouseMove, onMouseLeave: handleMouseLeave }));
70
+ };
@@ -1,12 +1,14 @@
1
1
  import React from 'react';
2
- import type { TooltipHoveredData } from '../../../../../types/widget-data';
2
+ import type { Dispatch } from 'd3';
3
+ import type { TooltipDataChunk } from '../../../../../types/widget-data';
3
4
  import type { PointerPosition, PreparedAxis, PreparedTooltip } from '../../hooks';
5
+ export * from './TooltipTriggerArea';
4
6
  type TooltipProps = {
7
+ dispatcher: Dispatch<object>;
5
8
  tooltip: PreparedTooltip;
6
9
  xAxis: PreparedAxis;
7
10
  yAxis: PreparedAxis;
8
- hovered?: TooltipHoveredData;
11
+ hovered?: TooltipDataChunk[];
9
12
  pointerPosition?: PointerPosition;
10
13
  };
11
14
  export declare const Tooltip: (props: TooltipProps) => React.JSX.Element | null;
12
- export {};