@gravity-ui/chartkit 4.19.1 → 4.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/build/constants/widget-data.d.ts +8 -0
  2. package/build/constants/widget-data.js +9 -0
  3. package/build/i18n/keysets/en.json +3 -1
  4. package/build/i18n/keysets/ru.json +3 -1
  5. package/build/plugins/d3/renderer/components/Chart.js +0 -1
  6. package/build/plugins/d3/renderer/components/Tooltip/DefaultContent.js +2 -1
  7. package/build/plugins/d3/renderer/components/Tooltip/index.js +11 -40
  8. package/build/plugins/d3/renderer/components/styles.css +12 -3
  9. package/build/plugins/d3/renderer/constants/defaults/series-options.js +12 -0
  10. package/build/plugins/d3/renderer/hooks/useSeries/index.js +6 -3
  11. package/build/plugins/d3/renderer/hooks/useSeries/prepare-pie.js +2 -2
  12. package/build/plugins/d3/renderer/hooks/useSeries/prepare-treemap.d.ts +11 -0
  13. package/build/plugins/d3/renderer/hooks/useSeries/prepare-treemap.js +34 -0
  14. package/build/plugins/d3/renderer/hooks/useSeries/prepareSeries.js +10 -1
  15. package/build/plugins/d3/renderer/hooks/useSeries/types.d.ts +14 -3
  16. package/build/plugins/d3/renderer/hooks/useShapes/index.js +13 -0
  17. package/build/plugins/d3/renderer/hooks/useShapes/styles.css +7 -0
  18. package/build/plugins/d3/renderer/hooks/useShapes/treemap/index.d.ts +12 -0
  19. package/build/plugins/d3/renderer/hooks/useShapes/treemap/index.js +103 -0
  20. package/build/plugins/d3/renderer/hooks/useShapes/treemap/prepare-data.d.ts +7 -0
  21. package/build/plugins/d3/renderer/hooks/useShapes/treemap/prepare-data.js +65 -0
  22. package/build/plugins/d3/renderer/hooks/useShapes/treemap/types.d.ts +15 -0
  23. package/build/plugins/d3/renderer/hooks/useShapes/treemap/types.js +1 -0
  24. package/build/plugins/d3/renderer/utils/index.js +1 -1
  25. package/build/plugins/d3/renderer/validation/index.js +53 -0
  26. package/build/plugins/yagr/renderer/utils.js +1 -1
  27. package/build/types/widget-data/index.d.ts +1 -0
  28. package/build/types/widget-data/index.js +1 -0
  29. package/build/types/widget-data/series.d.ts +10 -2
  30. package/build/types/widget-data/tooltip.d.ts +6 -1
  31. package/build/types/widget-data/treemap.d.ts +38 -0
  32. package/build/types/widget-data/treemap.js +1 -0
  33. package/package.json +1 -1
@@ -5,6 +5,7 @@ export declare const SeriesType: {
5
5
  readonly Line: "line";
6
6
  readonly Pie: "pie";
7
7
  readonly Scatter: "scatter";
8
+ readonly Treemap: "treemap";
8
9
  };
9
10
  export declare enum DashStyle {
10
11
  Dash = "Dash",
@@ -32,3 +33,10 @@ export declare enum LineCap {
32
33
  Square = "square",
33
34
  None = "none"
34
35
  }
36
+ export declare enum LayoutAlgorithm {
37
+ Binary = "binary",
38
+ Dice = "dice",
39
+ Slice = "slice",
40
+ SliceDice = "slice-dice",
41
+ Squarify = "squarify"
42
+ }
@@ -5,6 +5,7 @@ export const SeriesType = {
5
5
  Line: 'line',
6
6
  Pie: 'pie',
7
7
  Scatter: 'scatter',
8
+ Treemap: 'treemap',
8
9
  };
9
10
  export var DashStyle;
10
11
  (function (DashStyle) {
@@ -35,3 +36,11 @@ export var LineCap;
35
36
  LineCap["Square"] = "square";
36
37
  LineCap["None"] = "none";
37
38
  })(LineCap || (LineCap = {}));
39
+ export var LayoutAlgorithm;
40
+ (function (LayoutAlgorithm) {
41
+ LayoutAlgorithm["Binary"] = "binary";
42
+ LayoutAlgorithm["Dice"] = "dice";
43
+ LayoutAlgorithm["Slice"] = "slice";
44
+ LayoutAlgorithm["SliceDice"] = "slice-dice";
45
+ LayoutAlgorithm["Squarify"] = "squarify";
46
+ })(LayoutAlgorithm || (LayoutAlgorithm = {}));
@@ -34,7 +34,9 @@
34
34
  "label_invalid-axis-linear-data-point": "It seems you are trying to use inappropriate data type for \"{{key}}\" value in series \"{{seriesName}}\" for axis with type \"linear\". Numbers and nulls are allowed.",
35
35
  "label_invalid-pie-data-value": "It seems you are trying to use inappropriate data type for \"value\" value. Only numbers are allowed.",
36
36
  "label_invalid-series-type": "It seems you haven't defined \"series.type\" property, or defined it incorrectly. Available values: [{{types}}].",
37
- "label_invalid-series-property": "It seems you are trying to use inappropriate value for \"{{key}}\", or defined it incorrectly. Available values: [{{values}}]."
37
+ "label_invalid-series-property": "It seems you are trying to use inappropriate value for \"{{key}}\", or defined it incorrectly. Available values: [{{values}}].",
38
+ "label_invalid-treemap-redundant-value": "It seems you are trying to set \"value\" for container node. Check node with this properties: { id: \"{{id}}\", name: \"{{name}}\" }",
39
+ "label_invalid-treemap-missing-value": "It seems you are trying to use node without \"value\". Check node with this properties: { id: \"{{id}}\", name: \"{{name}}\" }"
38
40
  },
39
41
  "highcharts": {
40
42
  "reset-zoom-title": "Reset zoom",
@@ -36,7 +36,9 @@
36
36
  "label_invalid-axis-linear-data-point": "Похоже, что вы пытаетесь использовать недопустимый тип данных для значения \"{{key}}\" в серии \"{{seriesName}}\" для оси с типом \"linear\". Допускается использование чисел и значений null.",
37
37
  "label_invalid-pie-data-value": "Похоже, что вы пытаетесь использовать недопустимый тип данных для значения \"value\". Допускается только использование чисел.",
38
38
  "label_invalid-series-type": "Похоже, что вы не указали значение \"series.type\" или указали его неверно. Доступные значения: [{{types}}].",
39
- "label_invalid-series-property": "Похоже, что вы пытаетесь использовать недопустимое значение для \"{{key}}\", или указали его неверно. Доступные значения: [{{values}}]."
39
+ "label_invalid-series-property": "Похоже, что вы пытаетесь использовать недопустимое значение для \"{{key}}\", или указали его неверно. Доступные значения: [{{values}}].",
40
+ "label_invalid-treemap-redundant-value": "Похоже, что вы пытаетесь установить значение \"value\" для узла, используемого в качестве контейнера. Проверьте узел с этими свойствами: { id: \"{{id}}\", name: \"{{name}}\" }",
41
+ "label_invalid-treemap-missing-value": "Похоже, что вы пытаетесь использовать узел без значения \"value\". Проверьте узел с этими свойствами: { id: \"{{id}}\", name: \"{{name}}\" }"
40
42
  },
41
43
  "highcharts": {
42
44
  "reset-zoom-title": "Сбросить увеличение",
@@ -13,7 +13,6 @@ import { getPreparedYAxis } from '../hooks/useChartOptions/y-axis';
13
13
  import './styles.css';
14
14
  const b = block('d3');
15
15
  export const Chart = (props) => {
16
- // FIXME: add data validation
17
16
  const { width, height, data } = props;
18
17
  const svgRef = React.useRef(null);
19
18
  const dispatcher = React.useMemo(() => {
@@ -55,7 +55,8 @@ export const DefaultContent = ({ hovered, xAxis, yAxis }) => {
55
55
  ": ",
56
56
  xRow))));
57
57
  }
58
- case 'pie': {
58
+ case 'pie':
59
+ case 'treemap': {
59
60
  const pieSeriesData = data;
60
61
  return (React.createElement("div", { key: id },
61
62
  React.createElement("span", null,
@@ -1,40 +1,16 @@
1
1
  import React from 'react';
2
2
  import isNil from 'lodash/isNil';
3
+ import { Popup, useVirtualElementRef } from '@gravity-ui/uikit';
3
4
  import { block } from '../../../../../utils/cn';
4
5
  import { DefaultContent } from './DefaultContent';
5
6
  export * from './TooltipTriggerArea';
6
7
  const b = block('d3-tooltip');
7
- const POINTER_OFFSET_X = 20;
8
8
  export const Tooltip = (props) => {
9
- const { tooltip, svgContainer, xAxis, yAxis, hovered, pointerPosition } = props;
10
- const ref = React.useRef(null);
11
- const size = React.useMemo(() => {
12
- if (ref.current && hovered) {
13
- const { width, height } = ref.current.getBoundingClientRect();
14
- return { width, height };
15
- }
16
- return undefined;
17
- // eslint-disable-next-line react-hooks/exhaustive-deps
18
- }, [hovered, pointerPosition]);
19
- const position = React.useMemo(() => {
20
- if (hovered && pointerPosition && size) {
21
- const { clientWidth } = document.documentElement;
22
- const { width, height } = size;
23
- const rect = (svgContainer === null || svgContainer === void 0 ? void 0 : svgContainer.getBoundingClientRect()) || { left: 0, top: 0 };
24
- const [pointerLeft, pointetTop] = pointerPosition;
25
- const outOfRightBoudary = pointerLeft + width + rect.left + POINTER_OFFSET_X >= clientWidth;
26
- const outOfTopBoundary = pointetTop - height / 2 <= 0;
27
- const left = outOfRightBoudary
28
- ? pointerLeft - width - POINTER_OFFSET_X
29
- : pointerLeft + POINTER_OFFSET_X;
30
- const top = outOfTopBoundary ? 0 : pointetTop - height / 2;
31
- return { left, top };
32
- }
33
- else if (hovered && pointerPosition) {
34
- return { left: -1000, top: -1000 };
35
- }
36
- return undefined;
37
- }, [hovered, pointerPosition, size, svgContainer]);
9
+ const { tooltip, xAxis, yAxis, hovered, svgContainer, pointerPosition } = props;
10
+ const containerRect = (svgContainer === null || svgContainer === void 0 ? void 0 : svgContainer.getBoundingClientRect()) || { left: 0, top: 0 };
11
+ const left = ((pointerPosition === null || pointerPosition === void 0 ? void 0 : pointerPosition[0]) || 0) + containerRect.left;
12
+ const top = ((pointerPosition === null || pointerPosition === void 0 ? void 0 : pointerPosition[1]) || 0) + containerRect.top;
13
+ const anchorRef = useVirtualElementRef({ rect: { top, left } });
38
14
  const content = React.useMemo(() => {
39
15
  var _a;
40
16
  if (!hovered) {
@@ -43,14 +19,9 @@ export const Tooltip = (props) => {
43
19
  const customTooltip = (_a = tooltip.renderer) === null || _a === void 0 ? void 0 : _a.call(tooltip, { hovered });
44
20
  return isNil(customTooltip) ? (React.createElement(DefaultContent, { hovered: hovered, xAxis: xAxis, yAxis: yAxis })) : (customTooltip);
45
21
  }, [hovered, tooltip, xAxis, yAxis]);
46
- if (!position || !hovered) {
47
- return null;
48
- }
49
- const { left, top } = position;
50
- const style = {
51
- position: 'absolute',
52
- top,
53
- left: left,
54
- };
55
- return (React.createElement("div", { ref: ref, className: b(), style: style }, content));
22
+ React.useEffect(() => {
23
+ window.dispatchEvent(new CustomEvent('scroll'));
24
+ }, [left, top]);
25
+ return hovered ? (React.createElement(Popup, { className: b(), open: true, anchorRef: anchorRef, offset: [0, 20], placement: ['right', 'left', 'top', 'bottom'], modifiers: [{ name: 'preventOverflow', options: { padding: 10, altAxis: true } }] },
26
+ React.createElement("div", { className: b('content') }, content))) : null;
56
27
  };
@@ -75,13 +75,22 @@
75
75
  fill: var(--g-color-text-primary);
76
76
  }
77
77
 
78
- .chartkit-d3-tooltip {
79
- position: absolute;
78
+ .chartkit-d3-tooltip[class] {
79
+ --yc-popup-border-width: 0;
80
+ pointer-events: none;
81
+ }
82
+
83
+ .chartkit-d3-tooltip[class] > div {
84
+ animation-duration: unset;
85
+ animation-timing-function: unset;
86
+ animation-fill-mode: unset;
87
+ }
88
+
89
+ .chartkit-d3-tooltip__content {
80
90
  padding: 10px 14px;
81
91
  background-color: var(--g-color-infographics-tooltip-bg);
82
92
  border: 1px solid var(--g-color-line-generic);
83
93
  border-radius: 3px;
84
94
  box-shadow: 0 2px 12px var(--g-color-sfx-shadow);
85
- z-index: 100001;
86
95
  text-wrap: nowrap;
87
96
  }
@@ -77,4 +77,16 @@ export const seriesOptionsDefaults = {
77
77
  },
78
78
  },
79
79
  },
80
+ treemap: {
81
+ states: {
82
+ hover: {
83
+ enabled: true,
84
+ brightness: 0.3,
85
+ },
86
+ inactive: {
87
+ enabled: false,
88
+ opacity: 0.5,
89
+ },
90
+ },
91
+ },
80
92
  };
@@ -29,8 +29,7 @@ export const useSeries = (args) => {
29
29
  }, [seriesOptions]);
30
30
  const [activeLegendItems, setActiveLegendItems] = React.useState(getActiveLegendItems(preparedSeries));
31
31
  const chartSeries = React.useMemo(() => {
32
- return preparedSeries.map((singleSeries, i) => {
33
- singleSeries.id = `Series ${i + 1}`;
32
+ return preparedSeries.map((singleSeries) => {
34
33
  if (singleSeries.legend.enabled) {
35
34
  return Object.assign(Object.assign({}, singleSeries), { visible: activeLegendItems.includes(singleSeries.name) });
36
35
  }
@@ -48,6 +47,7 @@ export const useSeries = (args) => {
48
47
  });
49
48
  }, [chartWidth, chartHeight, chartMargin, chartSeries, preparedLegend, preparedYAxis]);
50
49
  const handleLegendItemClick = React.useCallback(({ name, metaKey }) => {
50
+ const allItems = getAllLegendItems(preparedSeries);
51
51
  const onlyItemSelected = activeLegendItems.length === 1 && activeLegendItems.includes(name);
52
52
  let nextActiveLegendItems;
53
53
  if (metaKey && activeLegendItems.includes(name)) {
@@ -56,8 +56,11 @@ export const useSeries = (args) => {
56
56
  else if (metaKey && !activeLegendItems.includes(name)) {
57
57
  nextActiveLegendItems = activeLegendItems.concat(name);
58
58
  }
59
+ else if (onlyItemSelected && allItems.length === 1) {
60
+ nextActiveLegendItems = [];
61
+ }
59
62
  else if (onlyItemSelected) {
60
- nextActiveLegendItems = getAllLegendItems(preparedSeries);
63
+ nextActiveLegendItems = allItems;
61
64
  }
62
65
  else {
63
66
  nextActiveLegendItems = [name];
@@ -10,7 +10,7 @@ export function preparePieSeries(args) {
10
10
  const colorScale = scaleOrdinal(dataNames, DEFAULT_PALETTE);
11
11
  const stackId = getRandomCKId();
12
12
  const seriesHoverState = get(seriesOptions, 'pie.states.hover');
13
- const preparedSeries = series.data.map((dataItem) => {
13
+ const preparedSeries = series.data.map((dataItem, i) => {
14
14
  var _a, _b, _c;
15
15
  const result = {
16
16
  type: 'pie',
@@ -29,7 +29,7 @@ export function preparePieSeries(args) {
29
29
  value: dataItem.value,
30
30
  visible: typeof dataItem.visible === 'boolean' ? dataItem.visible : true,
31
31
  name: dataItem.name,
32
- id: '',
32
+ id: `Series ${i}`,
33
33
  color: dataItem.color || colorScale(dataItem.name),
34
34
  legend: {
35
35
  enabled: get(series, 'legend.enabled', legend.enabled),
@@ -0,0 +1,11 @@
1
+ import type { ScaleOrdinal } from 'd3';
2
+ import type { ChartKitWidgetSeriesOptions, TreemapSeries } from '../../../../../types';
3
+ import type { PreparedLegend, PreparedTreemapSeries } from './types';
4
+ type PrepareTreemapSeriesArgs = {
5
+ colorScale: ScaleOrdinal<string, string>;
6
+ legend: PreparedLegend;
7
+ series: TreemapSeries[];
8
+ seriesOptions?: ChartKitWidgetSeriesOptions;
9
+ };
10
+ export declare function prepareTreemap(args: PrepareTreemapSeriesArgs): PreparedTreemapSeries[];
11
+ export {};
@@ -0,0 +1,34 @@
1
+ import get from 'lodash/get';
2
+ import { LayoutAlgorithm } from '../../../../../constants';
3
+ import { getRandomCKId } from '../../../../../utils';
4
+ import { DEFAULT_DATALABELS_PADDING, DEFAULT_DATALABELS_STYLE } from './constants';
5
+ import { prepareLegendSymbol } from './utils';
6
+ export function prepareTreemap(args) {
7
+ const { colorScale, legend, series } = args;
8
+ return series.map((s) => {
9
+ var _a;
10
+ const id = getRandomCKId();
11
+ const name = s.name || '';
12
+ const color = s.color || colorScale(name);
13
+ return {
14
+ color,
15
+ data: s.data,
16
+ dataLabels: {
17
+ enabled: get(s, 'dataLabels.enabled', true),
18
+ style: Object.assign({}, DEFAULT_DATALABELS_STYLE, (_a = s.dataLabels) === null || _a === void 0 ? void 0 : _a.style),
19
+ padding: get(s, 'dataLabels.padding', DEFAULT_DATALABELS_PADDING),
20
+ allowOverlap: get(s, 'dataLabels.allowOverlap', false),
21
+ },
22
+ id,
23
+ type: s.type,
24
+ name,
25
+ visible: get(s, 'visible', true),
26
+ legend: {
27
+ enabled: get(s, 'legend.enabled', legend.enabled),
28
+ symbol: prepareLegendSymbol(s),
29
+ },
30
+ levels: s.levels,
31
+ layoutAlgorithm: get(s, 'layoutAlgorithm', LayoutAlgorithm.Binary),
32
+ };
33
+ });
34
+ }
@@ -1,10 +1,11 @@
1
+ import { ChartKitError } from '../../../../../libs';
1
2
  import { prepareLineSeries } from './prepare-line';
2
3
  import { prepareBarXSeries } from './prepare-bar-x';
3
4
  import { prepareBarYSeries } from './prepare-bar-y';
4
- import { ChartKitError } from '../../../../../libs';
5
5
  import { preparePieSeries } from './prepare-pie';
6
6
  import { prepareArea } from './prepare-area';
7
7
  import { prepareScatterSeries } from './prepare-scatter';
8
+ import { prepareTreemap } from './prepare-treemap';
8
9
  export function prepareSeries(args) {
9
10
  const { type, series, seriesOptions, legend, colorScale } = args;
10
11
  switch (type) {
@@ -39,6 +40,14 @@ export function prepareSeries(args) {
39
40
  colorScale,
40
41
  });
41
42
  }
43
+ case 'treemap': {
44
+ return prepareTreemap({
45
+ series: series,
46
+ seriesOptions,
47
+ legend,
48
+ colorScale,
49
+ });
50
+ }
42
51
  default: {
43
52
  throw new ChartKitError({
44
53
  message: `Series type "${type}" does not support data preparation for series that do not support the presence of axes`,
@@ -1,6 +1,6 @@
1
- import { BarXSeries, BarXSeriesData, BaseTextStyle, ChartKitWidgetLegend, PieSeries, PieSeriesData, RectLegendSymbolOptions, ScatterSeries, ScatterSeriesData, BarYSeries, BarYSeriesData, LineSeries, LineSeriesData, ConnectorShape, ConnectorCurve, PathLegendSymbolOptions, SymbolLegendSymbolOptions, AreaSeries, AreaSeriesData } from '../../../../../types';
1
+ import { BarXSeries, BarXSeriesData, BaseTextStyle, ChartKitWidgetLegend, PieSeries, PieSeriesData, RectLegendSymbolOptions, ScatterSeries, ScatterSeriesData, BarYSeries, BarYSeriesData, LineSeries, LineSeriesData, ConnectorShape, ConnectorCurve, PathLegendSymbolOptions, SymbolLegendSymbolOptions, AreaSeries, AreaSeriesData, TreemapSeries, TreemapSeriesData } from '../../../../../types';
2
2
  import type { SeriesOptionsDefaults } from '../../constants';
3
- import { DashStyle, LineCap, SymbolType } from '../../../../../constants';
3
+ import { DashStyle, LayoutAlgorithm, LineCap, SymbolType } from '../../../../../constants';
4
4
  export type RectLegendSymbol = {
5
5
  shape: 'rect';
6
6
  } & Required<RectLegendSymbolOptions>;
@@ -194,7 +194,18 @@ export type PreparedAreaSeries = {
194
194
  };
195
195
  };
196
196
  } & BasePreparedSeries;
197
- export type PreparedSeries = PreparedScatterSeries | PreparedBarXSeries | PreparedBarYSeries | PreparedPieSeries | PreparedLineSeries | PreparedAreaSeries;
197
+ export type PreparedTreemapSeries = {
198
+ type: TreemapSeries['type'];
199
+ data: TreemapSeriesData[];
200
+ dataLabels: {
201
+ enabled: boolean;
202
+ style: BaseTextStyle;
203
+ padding: number;
204
+ allowOverlap: boolean;
205
+ };
206
+ layoutAlgorithm: `${LayoutAlgorithm}`;
207
+ } & BasePreparedSeries & TreemapSeries;
208
+ export type PreparedSeries = PreparedScatterSeries | PreparedBarXSeries | PreparedBarYSeries | PreparedPieSeries | PreparedLineSeries | PreparedAreaSeries | PreparedTreemapSeries;
198
209
  export type PreparedSeriesOptions = SeriesOptionsDefaults;
199
210
  export type StackedSeries = BarXSeries | AreaSeries | BarYSeries;
200
211
  export {};
@@ -10,6 +10,8 @@ import { LineSeriesShapes } from './line';
10
10
  import { BarYSeriesShapes, prepareBarYData } from './bar-y';
11
11
  import { AreaSeriesShapes } from './area';
12
12
  import { prepareAreaData } from './area/prepare-data';
13
+ import { TreemapSeriesShape } from './treemap';
14
+ import { prepareTreemapData } from './treemap/prepare-data';
13
15
  import './styles.css';
14
16
  export const useShapes = (args) => {
15
17
  const { boundsWidth, boundsHeight, dispatcher, series, seriesOptions, xAxis, xScale, yAxis, yScale, svgContainer, } = args;
@@ -98,6 +100,17 @@ export const useShapes = (args) => {
98
100
  boundsHeight,
99
101
  });
100
102
  acc.push(React.createElement(PieSeriesShapes, { key: "pie", dispatcher: dispatcher, preparedData: preparedData, seriesOptions: seriesOptions, svgContainer: svgContainer }));
103
+ break;
104
+ }
105
+ case 'treemap': {
106
+ const preparedData = prepareTreemapData({
107
+ // We should have exactly one series with "treemap" type
108
+ // Otherwise data validation should emit an error
109
+ series: chartSeries[0],
110
+ width: boundsWidth,
111
+ height: boundsHeight,
112
+ });
113
+ acc.push(React.createElement(TreemapSeriesShape, { key: "treemap", dispatcher: dispatcher, preparedData: preparedData, seriesOptions: seriesOptions, svgContainer: svgContainer }));
101
114
  }
102
115
  }
103
116
  return acc;
@@ -22,4 +22,11 @@
22
22
  fill: var(--g-color-text-complementary);
23
23
  user-select: none;
24
24
  alignment-baseline: after-edge;
25
+ }
26
+
27
+ .chartkit-d3-treemap__label {
28
+ fill: var(--g-color-text-complementary);
29
+ alignment-baseline: text-before-edge;
30
+ user-select: none;
31
+ pointer-events: none;
25
32
  }
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ import type { Dispatch } from 'd3';
3
+ import { PreparedSeriesOptions } from '../../useSeries/types';
4
+ import type { PreparedTreemapData } from './types';
5
+ type ShapeProps = {
6
+ dispatcher: Dispatch<object>;
7
+ preparedData: PreparedTreemapData;
8
+ seriesOptions: PreparedSeriesOptions;
9
+ svgContainer: SVGSVGElement | null;
10
+ };
11
+ export declare const TreemapSeriesShape: (props: ShapeProps) => React.JSX.Element;
12
+ export {};
@@ -0,0 +1,103 @@
1
+ import React from 'react';
2
+ import { color, pointer, select } from 'd3';
3
+ import get from 'lodash/get';
4
+ import { setEllipsisForOverflowTexts } from '../../../utils';
5
+ import { block } from '../../../../../../utils/cn';
6
+ const b = block('d3-treemap');
7
+ export const TreemapSeriesShape = (props) => {
8
+ const { dispatcher, preparedData, seriesOptions, svgContainer } = props;
9
+ const ref = React.useRef(null);
10
+ React.useEffect(() => {
11
+ if (!ref.current) {
12
+ return () => { };
13
+ }
14
+ const svgElement = select(ref.current);
15
+ svgElement.selectAll('*').remove();
16
+ const { labelData, leaves, series } = preparedData;
17
+ const leaf = svgElement
18
+ .selectAll('g')
19
+ .data(leaves)
20
+ .join('g')
21
+ .attr('transform', (d) => `translate(${d.x0},${d.y0})`);
22
+ const rectSelection = leaf
23
+ .append('rect')
24
+ .attr('id', (d) => d.id || d.name)
25
+ .attr('fill', (d) => {
26
+ var _a;
27
+ if (d.data.color) {
28
+ return d.data.color;
29
+ }
30
+ const levelOptions = (_a = series.levels) === null || _a === void 0 ? void 0 : _a.find((l) => l.index === d.depth);
31
+ return (levelOptions === null || levelOptions === void 0 ? void 0 : levelOptions.color) || series.color;
32
+ })
33
+ .attr('width', (d) => d.x1 - d.x0)
34
+ .attr('height', (d) => d.y1 - d.y0);
35
+ const labelSelection = svgElement
36
+ .selectAll('tspan')
37
+ .data(labelData)
38
+ .join('text')
39
+ .text((d) => d.text)
40
+ .attr('class', b('label'))
41
+ .attr('x', (d) => d.x)
42
+ .attr('y', (d) => d.y)
43
+ .style('font-size', () => series.dataLabels.style.fontSize)
44
+ .style('font-weight', () => { var _a; return ((_a = series.dataLabels.style) === null || _a === void 0 ? void 0 : _a.fontWeight) || null; })
45
+ .style('fill', () => { var _a; return ((_a = series.dataLabels.style) === null || _a === void 0 ? void 0 : _a.fontColor) || null; })
46
+ .call(setEllipsisForOverflowTexts, (d) => d.width);
47
+ const eventName = `hover-shape.pie`;
48
+ const hoverOptions = get(seriesOptions, 'treemap.states.hover');
49
+ const inactiveOptions = get(seriesOptions, 'treemap.states.inactive');
50
+ svgElement
51
+ .on('mousemove', (e) => {
52
+ const hoveredRect = select(e.target);
53
+ const datum = hoveredRect.datum();
54
+ dispatcher.call('hover-shape', {}, [{ data: datum.data, series }], pointer(e, svgContainer));
55
+ })
56
+ .on('mouseleave', () => {
57
+ dispatcher.call('hover-shape', {}, undefined);
58
+ });
59
+ dispatcher.on(eventName, (data) => {
60
+ const hoverEnabled = hoverOptions === null || hoverOptions === void 0 ? void 0 : hoverOptions.enabled;
61
+ const inactiveEnabled = inactiveOptions === null || inactiveOptions === void 0 ? void 0 : inactiveOptions.enabled;
62
+ const hoveredData = data === null || data === void 0 ? void 0 : data[0].data;
63
+ rectSelection.datum((d, index, list) => {
64
+ const currentRect = select(list[index]);
65
+ const hovered = Boolean(hoverEnabled && hoveredData === d.data);
66
+ const inactive = Boolean(inactiveEnabled && hoveredData && !hovered);
67
+ currentRect
68
+ .attr('fill', (currentD) => {
69
+ var _a, _b;
70
+ const levelOptions = (_a = series.levels) === null || _a === void 0 ? void 0 : _a.find((l) => l.index === currentD.depth);
71
+ const initialColor = (levelOptions === null || levelOptions === void 0 ? void 0 : levelOptions.color) || d.data.color || series.color;
72
+ if (hovered) {
73
+ return (((_b = color(initialColor)) === null || _b === void 0 ? void 0 : _b.brighter(hoverOptions === null || hoverOptions === void 0 ? void 0 : hoverOptions.brightness).toString()) || initialColor);
74
+ }
75
+ return initialColor;
76
+ })
77
+ .attr('opacity', () => {
78
+ if (inactive) {
79
+ return (inactiveOptions === null || inactiveOptions === void 0 ? void 0 : inactiveOptions.opacity) || null;
80
+ }
81
+ return null;
82
+ });
83
+ return d;
84
+ });
85
+ labelSelection.datum((d, index, list) => {
86
+ const currentLabel = select(list[index]);
87
+ const hovered = Boolean(hoverEnabled && hoveredData === d.nodeData);
88
+ const inactive = Boolean(inactiveEnabled && hoveredData && !hovered);
89
+ currentLabel.attr('opacity', () => {
90
+ if (inactive) {
91
+ return (inactiveOptions === null || inactiveOptions === void 0 ? void 0 : inactiveOptions.opacity) || null;
92
+ }
93
+ return null;
94
+ });
95
+ return d;
96
+ });
97
+ });
98
+ return () => {
99
+ dispatcher.on(eventName, null);
100
+ };
101
+ }, [dispatcher, preparedData, seriesOptions, svgContainer]);
102
+ return React.createElement("g", { ref: ref, className: b() });
103
+ };
@@ -0,0 +1,7 @@
1
+ import type { PreparedTreemapSeries } from '../../useSeries/types';
2
+ import type { PreparedTreemapData } from './types';
3
+ export declare function prepareTreemapData(args: {
4
+ series: PreparedTreemapSeries;
5
+ width: number;
6
+ height: number;
7
+ }): PreparedTreemapData;
@@ -0,0 +1,65 @@
1
+ import { stratify, treemap, treemapBinary, treemapDice, treemapSlice, treemapSliceDice, treemapSquarify, } from 'd3';
2
+ import { LayoutAlgorithm } from '../../../../../../constants';
3
+ const DEFAULT_PADDING = 1;
4
+ function getLabelData(data) {
5
+ return data.map((d) => {
6
+ const text = d.data.name;
7
+ return {
8
+ text,
9
+ x: d.x0,
10
+ y: d.y0,
11
+ width: d.x1 - d.x0,
12
+ nodeData: d.data,
13
+ };
14
+ });
15
+ }
16
+ export function prepareTreemapData(args) {
17
+ var _a;
18
+ const { series, width, height } = args;
19
+ const dataWithRootNode = getSeriesDataWithRootNode(series);
20
+ const hierarchy = stratify()
21
+ .id((d) => d.id || d.name)
22
+ .parentId((d) => d.parentId)(dataWithRootNode)
23
+ .sum((d) => d.value || 0);
24
+ const treemapInstance = treemap();
25
+ switch (series.layoutAlgorithm) {
26
+ case LayoutAlgorithm.Binary: {
27
+ treemapInstance.tile(treemapBinary);
28
+ break;
29
+ }
30
+ case LayoutAlgorithm.Dice: {
31
+ treemapInstance.tile(treemapDice);
32
+ break;
33
+ }
34
+ case LayoutAlgorithm.Slice: {
35
+ treemapInstance.tile(treemapSlice);
36
+ break;
37
+ }
38
+ case LayoutAlgorithm.SliceDice: {
39
+ treemapInstance.tile(treemapSliceDice);
40
+ break;
41
+ }
42
+ case LayoutAlgorithm.Squarify: {
43
+ treemapInstance.tile(treemapSquarify);
44
+ break;
45
+ }
46
+ }
47
+ const root = treemapInstance.size([width, height]).paddingInner((d) => {
48
+ var _a, _b;
49
+ const levelOptions = (_a = series.levels) === null || _a === void 0 ? void 0 : _a.find((l) => l.index === d.depth + 1);
50
+ return (_b = levelOptions === null || levelOptions === void 0 ? void 0 : levelOptions.padding) !== null && _b !== void 0 ? _b : DEFAULT_PADDING;
51
+ })(hierarchy);
52
+ const leaves = root.leaves();
53
+ const labelData = ((_a = series.dataLabels) === null || _a === void 0 ? void 0 : _a.enabled) ? getLabelData(leaves) : [];
54
+ return { labelData, leaves, series };
55
+ }
56
+ function getSeriesDataWithRootNode(series) {
57
+ return series.data.reduce((acc, d) => {
58
+ const dataChunk = Object.assign({}, d);
59
+ if (!dataChunk.parentId) {
60
+ dataChunk.parentId = series.id;
61
+ }
62
+ acc.push(dataChunk);
63
+ return acc;
64
+ }, [{ name: series.name, id: series.id }]);
65
+ }
@@ -0,0 +1,15 @@
1
+ import type { HierarchyRectangularNode } from 'd3';
2
+ import type { TreemapSeriesData } from '../../../../../../types';
3
+ import type { PreparedTreemapSeries } from '../../useSeries/types';
4
+ export type TreemapLabelData = {
5
+ text: string;
6
+ x: number;
7
+ y: number;
8
+ width: number;
9
+ nodeData: TreemapSeriesData;
10
+ };
11
+ export type PreparedTreemapData = {
12
+ labelData: TreemapLabelData[];
13
+ leaves: HierarchyRectangularNode<TreemapSeriesData<any>>[];
14
+ series: PreparedTreemapSeries;
15
+ };
@@ -13,7 +13,7 @@ export * from './time';
13
13
  export * from './axis';
14
14
  export * from './labels';
15
15
  export * from './symbol';
16
- const CHARTS_WITHOUT_AXIS = ['pie'];
16
+ const CHARTS_WITHOUT_AXIS = ['pie', 'treemap'];
17
17
  /**
18
18
  * Checks whether the series should be drawn with axes.
19
19
  *
@@ -108,6 +108,35 @@ const validateStacking = ({ series }) => {
108
108
  });
109
109
  }
110
110
  };
111
+ const validateTreemapSeries = ({ series }) => {
112
+ const parentIds = {};
113
+ series.data.forEach((d) => {
114
+ if (d.parentId && !parentIds[d.parentId]) {
115
+ parentIds[d.parentId] = true;
116
+ }
117
+ });
118
+ series.data.forEach((d) => {
119
+ const idOrName = d.id || d.name;
120
+ if (parentIds[idOrName] && typeof d.value === 'number') {
121
+ throw new ChartKitError({
122
+ code: CHARTKIT_ERROR_CODE.INVALID_DATA,
123
+ message: i18n('error', 'label_invalid-treemap-redundant-value', {
124
+ id: d.id,
125
+ name: d.name,
126
+ }),
127
+ });
128
+ }
129
+ if (!parentIds[idOrName] && typeof d.value !== 'number') {
130
+ throw new ChartKitError({
131
+ code: CHARTKIT_ERROR_CODE.INVALID_DATA,
132
+ message: i18n('error', 'label_invalid-treemap-missing-value', {
133
+ id: d.id,
134
+ name: d.name,
135
+ }),
136
+ });
137
+ }
138
+ });
139
+ };
111
140
  const validateSeries = (args) => {
112
141
  const { series, xAxis, yAxis } = args;
113
142
  if (!AVAILABLE_SERIES_TYPES.includes(series.type)) {
@@ -133,9 +162,23 @@ const validateSeries = (args) => {
133
162
  }
134
163
  case 'pie': {
135
164
  validatePieSeries({ series });
165
+ break;
166
+ }
167
+ case 'treemap': {
168
+ validateTreemapSeries({ series });
136
169
  }
137
170
  }
138
171
  };
172
+ const countSeriesByType = (args) => {
173
+ const { series, type } = args;
174
+ let count = 0;
175
+ series.forEach((s) => {
176
+ if (s.type === type) {
177
+ count += 1;
178
+ }
179
+ });
180
+ return count;
181
+ };
139
182
  export const validateData = (data) => {
140
183
  if (isEmpty(data) || isEmpty(data.series) || isEmpty(data.series.data)) {
141
184
  throw new ChartKitError({
@@ -149,6 +192,16 @@ export const validateData = (data) => {
149
192
  message: 'You should specify data for all series',
150
193
  });
151
194
  }
195
+ const treemapSeriesCount = countSeriesByType({
196
+ series: data.series.data,
197
+ type: SeriesType.Treemap,
198
+ });
199
+ if (treemapSeriesCount > 1) {
200
+ throw new ChartKitError({
201
+ code: CHARTKIT_ERROR_CODE.INVALID_DATA,
202
+ message: 'It looks like you are trying to define more than one "treemap" series.',
203
+ });
204
+ }
152
205
  data.series.data.forEach((series) => {
153
206
  var _a;
154
207
  validateSeries({ series, yAxis: (_a = data.yAxis) === null || _a === void 0 ? void 0 : _a[0], xAxis: data.xAxis });
@@ -94,7 +94,7 @@ export const getUplotTimezoneAligner = (chart, timeZone) => (ts) => {
94
94
  const browserTimezone = browserDate.utcOffset();
95
95
  const timestampRealTimezone = dateTime({ input: dt, timeZone }).utcOffset();
96
96
  const uPlotOffset = (browserTimezone - timestampRealTimezone) * 60 * 1000;
97
- return new Date(browserDate.valueOf() + uPlotOffset);
97
+ return new Date(browserDate.valueOf() - uPlotOffset);
98
98
  };
99
99
  export const shapeYagrConfig = (args) => {
100
100
  var _a, _b;
@@ -18,6 +18,7 @@ export * from './series';
18
18
  export * from './title';
19
19
  export * from './tooltip';
20
20
  export * from './halo';
21
+ export * from './treemap';
21
22
  export type ChartKitWidgetData<T = any> = {
22
23
  chart?: ChartKitWidgetChart;
23
24
  legend?: ChartKitWidgetLegend;
@@ -12,3 +12,4 @@ export * from './series';
12
12
  export * from './title';
13
13
  export * from './tooltip';
14
14
  export * from './halo';
15
+ export * from './treemap';
@@ -6,10 +6,11 @@ import type { LineSeries, LineSeriesData } from './line';
6
6
  import type { BarYSeries, BarYSeriesData } from './bar-y';
7
7
  import type { PointMarkerOptions } from './marker';
8
8
  import type { AreaSeries, AreaSeriesData } from './area';
9
+ import type { TreemapSeries, TreemapSeriesData } from './treemap';
9
10
  import type { Halo } from './halo';
10
11
  import { DashStyle, LineCap } from '../../constants';
11
- export type ChartKitWidgetSeries<T = any> = ScatterSeries<T> | PieSeries<T> | BarXSeries<T> | BarYSeries<T> | LineSeries<T> | AreaSeries<T>;
12
- export type ChartKitWidgetSeriesData<T = any> = ScatterSeriesData<T> | PieSeriesData<T> | BarXSeriesData<T> | BarYSeriesData<T> | LineSeriesData<T> | AreaSeriesData<T>;
12
+ export type ChartKitWidgetSeries<T = any> = ScatterSeries<T> | PieSeries<T> | BarXSeries<T> | BarYSeries<T> | LineSeries<T> | AreaSeries<T> | TreemapSeries<T>;
13
+ export type ChartKitWidgetSeriesData<T = any> = ScatterSeriesData<T> | PieSeriesData<T> | BarXSeriesData<T> | BarYSeriesData<T> | LineSeriesData<T> | AreaSeriesData<T> | TreemapSeriesData<T>;
13
14
  export type DataLabelRendererData<T = any> = {
14
15
  data: ChartKitWidgetSeriesData<T>;
15
16
  };
@@ -194,5 +195,12 @@ export type ChartKitWidgetSeriesOptions = {
194
195
  /** Options for the point markers of line series */
195
196
  marker?: PointMarkerOptions;
196
197
  };
198
+ treemap?: {
199
+ /** Options for the series states that provide additional styling information to the series. */
200
+ states?: {
201
+ hover?: BasicHoverState;
202
+ inactive?: BasicInactiveState;
203
+ };
204
+ };
197
205
  };
198
206
  export {};
@@ -5,6 +5,7 @@ import type { ScatterSeries, ScatterSeriesData } from './scatter';
5
5
  import type { LineSeries, LineSeriesData } from './line';
6
6
  import type { BarYSeries, BarYSeriesData } from './bar-y';
7
7
  import type { AreaSeries, AreaSeriesData } from './area';
8
+ import type { TreemapSeries, TreemapSeriesData } from './treemap';
8
9
  export type TooltipDataChunkBarX<T = any> = {
9
10
  data: BarXSeriesData<T>;
10
11
  series: BarXSeries<T>;
@@ -45,7 +46,11 @@ export type TooltipDataChunkArea<T = any> = {
45
46
  name: string;
46
47
  };
47
48
  };
48
- export type TooltipDataChunk<T = any> = TooltipDataChunkBarX<T> | TooltipDataChunkBarY<T> | TooltipDataChunkPie<T> | TooltipDataChunkScatter<T> | TooltipDataChunkLine<T> | TooltipDataChunkArea<T>;
49
+ export type TooltipDataChunkTreemap<T = any> = {
50
+ data: TreemapSeriesData<T>;
51
+ series: TreemapSeries<T>;
52
+ };
53
+ export type TooltipDataChunk<T = any> = TooltipDataChunkBarX<T> | TooltipDataChunkBarY<T> | TooltipDataChunkPie<T> | TooltipDataChunkScatter<T> | TooltipDataChunkLine<T> | TooltipDataChunkArea<T> | TooltipDataChunkTreemap<T>;
49
54
  export type ChartKitWidgetTooltip<T = any> = {
50
55
  enabled?: boolean;
51
56
  /** Specifies the renderer for the tooltip. If returned null default tooltip renderer will be used. */
@@ -0,0 +1,38 @@
1
+ import { LayoutAlgorithm, SeriesType } from '../../constants';
2
+ import type { BaseSeries, BaseSeriesData } from './base';
3
+ import { ChartKitWidgetLegend, RectLegendSymbolOptions } from './legend';
4
+ export type TreemapSeriesData<T = any> = BaseSeriesData<T> & {
5
+ /** The name of the node (used in legend, tooltip etc). */
6
+ name: string;
7
+ /** The value of the node. All nodes should have this property except nodes that have children. */
8
+ value?: number;
9
+ /** An id for the node. Used to group children. */
10
+ id?: string;
11
+ /**
12
+ * Parent id. Used to build a tree structure. The value should be the id of the node which is the parent.
13
+ * If no nodes has a matching id, or this option is undefined, then the parent will be set to the root.
14
+ */
15
+ parentId?: string;
16
+ };
17
+ export type TreemapSeries<T = any> = BaseSeries & {
18
+ type: typeof SeriesType.Treemap;
19
+ data: TreemapSeriesData<T>[];
20
+ /** The name of the series (used in legend, tooltip etc). */
21
+ name: string;
22
+ /** The main color of the series (hex, rgba). */
23
+ color?: string;
24
+ /** Individual series legend options. Has higher priority than legend options in widget data. */
25
+ legend?: ChartKitWidgetLegend & {
26
+ symbol?: RectLegendSymbolOptions;
27
+ };
28
+ /** Set options on specific levels. Takes precedence over series options, but not point options. */
29
+ levels?: {
30
+ /** Decides which level takes effect from the options set in the levels object. */
31
+ index: number;
32
+ /** Can set the padding between all points which lies on the same level. */
33
+ padding?: number;
34
+ /** Can set a color on all points which lies on the same level. */
35
+ color?: string;
36
+ }[];
37
+ layoutAlgorithm?: `${LayoutAlgorithm}`;
38
+ };
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gravity-ui/chartkit",
3
- "version": "4.19.1",
3
+ "version": "4.20.1",
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",