@gravity-ui/chartkit 5.4.0 → 5.5.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 (38) hide show
  1. package/build/constants/widget-data.d.ts +1 -0
  2. package/build/constants/widget-data.js +1 -0
  3. package/build/plugins/d3/renderer/components/Tooltip/DefaultContent.js +19 -2
  4. package/build/plugins/d3/renderer/constants/defaults/series-options.d.ts +7 -1
  5. package/build/plugins/d3/renderer/constants/defaults/series-options.js +14 -0
  6. package/build/plugins/d3/renderer/hooks/useChartOptions/y-axis.js +13 -3
  7. package/build/plugins/d3/renderer/hooks/useSeries/prepare-waterfall.d.ts +10 -0
  8. package/build/plugins/d3/renderer/hooks/useSeries/prepare-waterfall.js +36 -0
  9. package/build/plugins/d3/renderer/hooks/useSeries/prepareSeries.js +8 -0
  10. package/build/plugins/d3/renderer/hooks/useSeries/types.d.ts +14 -2
  11. package/build/plugins/d3/renderer/hooks/useShapes/index.d.ts +2 -1
  12. package/build/plugins/d3/renderer/hooks/useShapes/index.js +16 -0
  13. package/build/plugins/d3/renderer/hooks/useShapes/styles.css +4 -0
  14. package/build/plugins/d3/renderer/hooks/useShapes/waterfall/index.d.ts +12 -0
  15. package/build/plugins/d3/renderer/hooks/useShapes/waterfall/index.js +125 -0
  16. package/build/plugins/d3/renderer/hooks/useShapes/waterfall/prepare-data.d.ts +12 -0
  17. package/build/plugins/d3/renderer/hooks/useShapes/waterfall/prepare-data.js +132 -0
  18. package/build/plugins/d3/renderer/hooks/useShapes/waterfall/types.d.ts +14 -0
  19. package/build/plugins/d3/renderer/hooks/useShapes/waterfall/types.js +1 -0
  20. package/build/plugins/d3/renderer/utils/get-closest-data.js +16 -5
  21. package/build/plugins/d3/renderer/utils/index.d.ts +1 -0
  22. package/build/plugins/d3/renderer/utils/index.js +11 -0
  23. package/build/plugins/d3/renderer/utils/series/index.d.ts +1 -0
  24. package/build/plugins/d3/renderer/utils/series/index.js +1 -0
  25. package/build/plugins/d3/renderer/utils/series/waterfall.d.ts +4 -0
  26. package/build/plugins/d3/renderer/utils/series/waterfall.js +25 -0
  27. package/build/plugins/highcharts/renderer/components/HighchartsComponent.d.ts +5 -0
  28. package/build/plugins/highcharts/renderer/helpers/config/config.d.ts +5 -0
  29. package/build/plugins/highcharts/renderer/helpers/config/config.js +8 -0
  30. package/build/plugins/highcharts/renderer/helpers/config/options.js +1 -1
  31. package/build/plugins/highcharts/renderer/helpers/graph.d.ts +5 -0
  32. package/build/types/widget-data/index.d.ts +1 -0
  33. package/build/types/widget-data/index.js +1 -0
  34. package/build/types/widget-data/series.d.ts +21 -2
  35. package/build/types/widget-data/tooltip.d.ts +6 -1
  36. package/build/types/widget-data/waterfall.d.ts +39 -0
  37. package/build/types/widget-data/waterfall.js +1 -0
  38. package/package.json +1 -1
@@ -6,6 +6,7 @@ export declare const SeriesType: {
6
6
  readonly Pie: "pie";
7
7
  readonly Scatter: "scatter";
8
8
  readonly Treemap: "treemap";
9
+ readonly Waterfall: "waterfall";
9
10
  };
10
11
  export declare enum DashStyle {
11
12
  Dash = "Dash",
@@ -6,6 +6,7 @@ export const SeriesType = {
6
6
  Pie: 'pie',
7
7
  Scatter: 'scatter',
8
8
  Treemap: 'treemap',
9
+ Waterfall: 'waterfall',
9
10
  };
10
11
  export var DashStyle;
11
12
  (function (DashStyle) {
@@ -3,7 +3,7 @@ import { dateTime } from '@gravity-ui/date-utils';
3
3
  import get from 'lodash/get';
4
4
  import { block } from '../../../../../utils/cn';
5
5
  import { formatNumber } from '../../../../shared';
6
- import { getDataCategoryValue } from '../../utils';
6
+ import { getDataCategoryValue, getWaterfallPointSubtotal } from '../../utils';
7
7
  const b = block('d3-tooltip');
8
8
  const DEFAULT_DATE_FORMAT = 'DD.MM.YY';
9
9
  const getRowData = (fieldName, axis, data) => {
@@ -30,7 +30,7 @@ const getXRowData = (xAxis, data) => getRowData('x', xAxis, data);
30
30
  const getYRowData = (yAxis, data) => getRowData('y', yAxis, data);
31
31
  const getMeasureValue = (data, xAxis, yAxis) => {
32
32
  var _a, _b;
33
- if (data.every((item) => item.series.type === 'pie' || item.series.type === 'treemap')) {
33
+ if (data.every((item) => ['pie', 'treemap', 'waterfall'].includes(item.series.type))) {
34
34
  return null;
35
35
  }
36
36
  if (data.some((item) => item.series.type === 'bar-y')) {
@@ -58,6 +58,23 @@ export const DefaultContent = ({ hovered, xAxis, yAxis }) => {
58
58
  React.createElement("div", { className: b('color'), style: { backgroundColor: color } }),
59
59
  React.createElement("div", null, closest ? React.createElement("b", null, value) : React.createElement("span", null, value))));
60
60
  }
61
+ case 'waterfall': {
62
+ const isTotal = get(data, 'total', false);
63
+ const subTotal = getWaterfallPointSubtotal(data, series);
64
+ return (React.createElement("div", { key: `${id}_${get(data, 'x')}` },
65
+ !isTotal && (React.createElement(React.Fragment, null,
66
+ React.createElement("div", { key: id, className: b('content-row') },
67
+ React.createElement("b", null, getXRowData(xAxis, data))),
68
+ React.createElement("div", { className: b('content-row') },
69
+ React.createElement("span", null,
70
+ series.name,
71
+ "\u00A0"),
72
+ React.createElement("span", null, getYRowData(yAxis, data))))),
73
+ React.createElement("div", { key: id, className: b('content-row') },
74
+ isTotal ? 'Total' : 'Subtotal',
75
+ ": ",
76
+ subTotal)));
77
+ }
61
78
  case 'bar-y': {
62
79
  const value = (React.createElement(React.Fragment, null,
63
80
  series.name,
@@ -13,6 +13,12 @@ type DefaultBarYSeriesOptions = Partial<ChartKitWidgetSeriesOptions['bar-x']> &
13
13
  groupPadding: number;
14
14
  };
15
15
  };
16
- export type SeriesOptionsDefaults = Partial<ChartKitWidgetSeriesOptions> & DefaultBarXSeriesOptions & DefaultBarYSeriesOptions;
16
+ type DefaultWaterfallSeriesOptions = Partial<ChartKitWidgetSeriesOptions['waterfall']> & {
17
+ waterfall: {
18
+ barMaxWidth: number;
19
+ barPadding: number;
20
+ };
21
+ };
22
+ export type SeriesOptionsDefaults = Partial<ChartKitWidgetSeriesOptions> & DefaultBarXSeriesOptions & DefaultBarYSeriesOptions & DefaultWaterfallSeriesOptions;
17
23
  export declare const seriesOptionsDefaults: SeriesOptionsDefaults;
18
24
  export {};
@@ -89,4 +89,18 @@ export const seriesOptionsDefaults = {
89
89
  },
90
90
  },
91
91
  },
92
+ waterfall: {
93
+ barMaxWidth: 50,
94
+ barPadding: 0.1,
95
+ states: {
96
+ hover: {
97
+ enabled: true,
98
+ brightness: 0.3,
99
+ },
100
+ inactive: {
101
+ enabled: false,
102
+ opacity: 0.5,
103
+ },
104
+ },
105
+ },
92
106
  };
@@ -1,6 +1,6 @@
1
1
  import get from 'lodash/get';
2
2
  import { DEFAULT_AXIS_LABEL_FONT_SIZE, axisLabelsDefaults, yAxisTitleDefaults, } from '../../constants';
3
- import { formatAxisTickLabel, getClosestPointsRange, getHorisontalSvgTextHeight, getLabelsSize, getScaleTicks, } from '../../utils';
3
+ import { formatAxisTickLabel, getClosestPointsRange, getHorisontalSvgTextHeight, getLabelsSize, getScaleTicks, getWaterfallPointSubtotal, } from '../../utils';
4
4
  import { createYScale } from '../useAxisScales';
5
5
  const getAxisLabelMaxWidth = (args) => {
6
6
  const { axis, series } = args;
@@ -24,9 +24,19 @@ const getAxisLabelMaxWidth = (args) => {
24
24
  };
25
25
  function getAxisMin(axis, series) {
26
26
  const min = axis === null || axis === void 0 ? void 0 : axis.min;
27
- const seriesWithVolume = ['bar-x', 'area'];
27
+ const seriesWithVolume = ['bar-x', 'area', 'waterfall'];
28
28
  if (typeof min === 'undefined' && (series === null || series === void 0 ? void 0 : series.some((s) => seriesWithVolume.includes(s.type)))) {
29
- return 0;
29
+ return series.reduce((minValue, s) => {
30
+ switch (s.type) {
31
+ case 'waterfall': {
32
+ const minSubTotal = s.data.reduce((res, d) => Math.min(res, getWaterfallPointSubtotal(d, s) || 0), 0);
33
+ return Math.min(minValue, minSubTotal);
34
+ }
35
+ default: {
36
+ return minValue;
37
+ }
38
+ }
39
+ }, 0);
30
40
  }
31
41
  return min;
32
42
  }
@@ -0,0 +1,10 @@
1
+ import type { ScaleOrdinal } from 'd3';
2
+ import type { WaterfallSeries } from '../../../../../types';
3
+ import type { PreparedLegend, PreparedSeries } from './types';
4
+ type PrepareWaterfallSeriesArgs = {
5
+ colorScale: ScaleOrdinal<string, string>;
6
+ series: WaterfallSeries[];
7
+ legend: PreparedLegend;
8
+ };
9
+ export declare function prepareWaterfallSeries(args: PrepareWaterfallSeriesArgs): PreparedSeries[];
10
+ export {};
@@ -0,0 +1,36 @@
1
+ import get from 'lodash/get';
2
+ import { getRandomCKId } from '../../../../../utils';
3
+ import { DEFAULT_PALETTE } from '../../constants';
4
+ import { DEFAULT_DATALABELS_PADDING, DEFAULT_DATALABELS_STYLE } from './constants';
5
+ import { prepareLegendSymbol } from './utils';
6
+ export function prepareWaterfallSeries(args) {
7
+ const { colorScale, series: seriesList, legend } = args;
8
+ const [, negativeColor, positiveColor] = DEFAULT_PALETTE;
9
+ return seriesList.map((series) => {
10
+ var _a, _b, _c;
11
+ const name = series.name || '';
12
+ const color = series.color || colorScale(name);
13
+ const prepared = {
14
+ type: series.type,
15
+ color,
16
+ positiveColor: positiveColor,
17
+ negativeColor: negativeColor,
18
+ name,
19
+ id: getRandomCKId(),
20
+ visible: get(series, 'visible', true),
21
+ legend: {
22
+ enabled: get(series, 'legend.enabled', legend.enabled),
23
+ symbol: prepareLegendSymbol(series),
24
+ },
25
+ data: series.data,
26
+ dataLabels: {
27
+ enabled: ((_a = series.dataLabels) === null || _a === void 0 ? void 0 : _a.enabled) || false,
28
+ style: Object.assign({}, DEFAULT_DATALABELS_STYLE, (_b = series.dataLabels) === null || _b === void 0 ? void 0 : _b.style),
29
+ allowOverlap: ((_c = series.dataLabels) === null || _c === void 0 ? void 0 : _c.allowOverlap) || false,
30
+ padding: get(series, 'dataLabels.padding', DEFAULT_DATALABELS_PADDING),
31
+ },
32
+ cursor: get(series, 'cursor', null),
33
+ };
34
+ return prepared;
35
+ }, []);
36
+ }
@@ -6,6 +6,7 @@ import { prepareLineSeries } from './prepare-line';
6
6
  import { preparePieSeries } from './prepare-pie';
7
7
  import { prepareScatterSeries } from './prepare-scatter';
8
8
  import { prepareTreemap } from './prepare-treemap';
9
+ import { prepareWaterfallSeries } from './prepare-waterfall';
9
10
  export function prepareSeries(args) {
10
11
  const { type, series, seriesOptions, legend, colorScale } = args;
11
12
  switch (type) {
@@ -48,6 +49,13 @@ export function prepareSeries(args) {
48
49
  colorScale,
49
50
  });
50
51
  }
52
+ case 'waterfall': {
53
+ return prepareWaterfallSeries({
54
+ series: series,
55
+ legend,
56
+ colorScale,
57
+ });
58
+ }
51
59
  default: {
52
60
  throw new ChartKitError({
53
61
  message: `Series type "${type}" does not support data preparation for series that do not support the presence of axes`,
@@ -1,5 +1,5 @@
1
1
  import { DashStyle, LayoutAlgorithm, LineCap, SymbolType } from '../../../../../constants';
2
- import { AreaSeries, AreaSeriesData, BarXSeries, BarXSeriesData, BarYSeries, BarYSeriesData, BaseTextStyle, ChartKitWidgetLegend, ConnectorCurve, ConnectorShape, LineSeries, LineSeriesData, PathLegendSymbolOptions, PieSeries, PieSeriesData, RectLegendSymbolOptions, ScatterSeries, ScatterSeriesData, SymbolLegendSymbolOptions, TreemapSeries, TreemapSeriesData } from '../../../../../types';
2
+ import type { AreaSeries, AreaSeriesData, BarXSeries, BarXSeriesData, BarYSeries, BarYSeriesData, BaseTextStyle, ChartKitWidgetLegend, ConnectorCurve, ConnectorShape, LineSeries, LineSeriesData, PathLegendSymbolOptions, PieSeries, PieSeriesData, RectLegendSymbolOptions, ScatterSeries, ScatterSeriesData, SymbolLegendSymbolOptions, TreemapSeries, TreemapSeriesData, WaterfallSeries, WaterfallSeriesData } from '../../../../../types';
3
3
  import type { SeriesOptionsDefaults } from '../../constants';
4
4
  export type RectLegendSymbol = {
5
5
  shape: 'rect';
@@ -208,7 +208,19 @@ export type PreparedTreemapSeries = {
208
208
  };
209
209
  layoutAlgorithm: `${LayoutAlgorithm}`;
210
210
  } & BasePreparedSeries & Omit<TreemapSeries, keyof BasePreparedSeries>;
211
- export type PreparedSeries = PreparedScatterSeries | PreparedBarXSeries | PreparedBarYSeries | PreparedPieSeries | PreparedLineSeries | PreparedAreaSeries | PreparedTreemapSeries;
211
+ export type PreparedWaterfallSeries = {
212
+ type: WaterfallSeries['type'];
213
+ data: WaterfallSeriesData[];
214
+ dataLabels: {
215
+ enabled: boolean;
216
+ style: BaseTextStyle;
217
+ allowOverlap: boolean;
218
+ padding: number;
219
+ };
220
+ positiveColor: string;
221
+ negativeColor: string;
222
+ } & BasePreparedSeries;
223
+ export type PreparedSeries = PreparedScatterSeries | PreparedBarXSeries | PreparedBarYSeries | PreparedPieSeries | PreparedLineSeries | PreparedAreaSeries | PreparedTreemapSeries | PreparedWaterfallSeries;
212
224
  export type PreparedSeriesOptions = SeriesOptionsDefaults;
213
225
  export type StackedSeries = BarXSeries | AreaSeries | BarYSeries;
214
226
  export {};
@@ -11,8 +11,9 @@ import type { PreparedPieData } from './pie/types';
11
11
  import type { PreparedScatterData } from './scatter/types';
12
12
  export type { PreparedBarXData } from './bar-x';
13
13
  export type { PreparedScatterData } from './scatter/types';
14
+ import { PreparedWaterfallData } from './waterfall';
14
15
  import './styles.css';
15
- export type ShapeData = PreparedBarXData | PreparedBarYData | PreparedScatterData | PreparedLineData | PreparedPieData | PreparedAreaData;
16
+ export type ShapeData = PreparedBarXData | PreparedBarYData | PreparedScatterData | PreparedLineData | PreparedPieData | PreparedAreaData | PreparedWaterfallData;
16
17
  type Args = {
17
18
  boundsWidth: number;
18
19
  boundsHeight: number;
@@ -12,6 +12,7 @@ import { preparePieData } from './pie/prepare-data';
12
12
  import { ScatterSeriesShape, prepareScatterData } from './scatter';
13
13
  import { TreemapSeriesShape } from './treemap';
14
14
  import { prepareTreemapData } from './treemap/prepare-data';
15
+ import { WaterfallSeriesShapes, prepareWaterfallData } from './waterfall';
15
16
  import './styles.css';
16
17
  export const useShapes = (args) => {
17
18
  const { boundsWidth, boundsHeight, dispatcher, series, seriesOptions, xAxis, xScale, yAxis, yScale, } = args;
@@ -52,6 +53,21 @@ export const useShapes = (args) => {
52
53
  }
53
54
  break;
54
55
  }
56
+ case 'waterfall': {
57
+ if (xScale && yScale) {
58
+ const preparedData = prepareWaterfallData({
59
+ series: chartSeries,
60
+ seriesOptions,
61
+ xAxis,
62
+ xScale,
63
+ yAxis,
64
+ yScale,
65
+ });
66
+ acc.push(React.createElement(WaterfallSeriesShapes, { key: "waterfall", dispatcher: dispatcher, seriesOptions: seriesOptions, preparedData: preparedData }));
67
+ shapesData.push(...preparedData);
68
+ }
69
+ break;
70
+ }
55
71
  case 'line': {
56
72
  if (xScale && yScale) {
57
73
  const preparedData = prepareLineData({
@@ -29,4 +29,8 @@
29
29
  alignment-baseline: text-before-edge;
30
30
  user-select: none;
31
31
  pointer-events: none;
32
+ }
33
+
34
+ .chartkit-d3-waterfall__connector {
35
+ stroke: var(--g-color-line-generic-active);
32
36
  }
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ import type { Dispatch } from 'd3';
3
+ import type { PreparedSeriesOptions } from '../../useSeries/types';
4
+ import type { PreparedWaterfallData } from './types';
5
+ export { prepareWaterfallData } from './prepare-data';
6
+ export * from './types';
7
+ type Args = {
8
+ dispatcher: Dispatch<object>;
9
+ preparedData: PreparedWaterfallData[];
10
+ seriesOptions: PreparedSeriesOptions;
11
+ };
12
+ export declare const WaterfallSeriesShapes: (args: Args) => React.JSX.Element;
@@ -0,0 +1,125 @@
1
+ import React from 'react';
2
+ import { color, line as lineGenerator, select } from 'd3';
3
+ import get from 'lodash/get';
4
+ import { DashStyle } from '../../../../../../constants';
5
+ import { block } from '../../../../../../utils/cn';
6
+ import { filterOverlappingLabels, getWaterfallPointColor } from '../../../utils';
7
+ import { getLineDashArray } from '../utils';
8
+ export { prepareWaterfallData } from './prepare-data';
9
+ export * from './types';
10
+ const b = block('d3-waterfall');
11
+ export const WaterfallSeriesShapes = (args) => {
12
+ const { dispatcher, preparedData, seriesOptions } = args;
13
+ const ref = React.useRef(null);
14
+ const connectorSelector = `.${b('connector')}`;
15
+ React.useEffect(() => {
16
+ var _a;
17
+ if (!ref.current) {
18
+ return () => { };
19
+ }
20
+ const svgElement = select(ref.current);
21
+ const hoverOptions = get(seriesOptions, 'waterfall.states.hover');
22
+ const inactiveOptions = get(seriesOptions, 'waterfall.states.inactive');
23
+ svgElement.selectAll('*').remove();
24
+ const rectSelection = svgElement
25
+ .selectAll('allRects')
26
+ .data(preparedData)
27
+ .join('rect')
28
+ .attr('class', b('segment'))
29
+ .attr('x', (d) => d.x)
30
+ .attr('y', (d) => d.y)
31
+ .attr('height', (d) => d.height)
32
+ .attr('width', (d) => d.width)
33
+ .attr('fill', (d) => getWaterfallPointColor(d.data, d.series))
34
+ .attr('opacity', (d) => d.opacity)
35
+ .attr('cursor', (d) => d.series.cursor);
36
+ let dataLabels = preparedData.map((d) => d.label).filter(Boolean);
37
+ if (!((_a = preparedData[0]) === null || _a === void 0 ? void 0 : _a.series.dataLabels.allowOverlap)) {
38
+ dataLabels = filterOverlappingLabels(dataLabels);
39
+ }
40
+ const labelSelection = svgElement
41
+ .selectAll('text')
42
+ .data(dataLabels)
43
+ .join('text')
44
+ .text((d) => d.text)
45
+ .attr('class', b('label'))
46
+ .attr('x', (d) => d.x)
47
+ .attr('y', (d) => d.y)
48
+ .attr('text-anchor', (d) => d.textAnchor)
49
+ .style('font-size', (d) => d.style.fontSize)
50
+ .style('font-weight', (d) => d.style.fontWeight || null)
51
+ .style('fill', (d) => d.style.fontColor || null);
52
+ // Add the connector line between bars
53
+ svgElement
54
+ .selectAll(connectorSelector)
55
+ .data(preparedData)
56
+ .join('path')
57
+ .attr('class', b('connector'))
58
+ .attr('d', (d, index) => {
59
+ const line = lineGenerator();
60
+ const prev = preparedData[index - 1];
61
+ if (!prev) {
62
+ return null;
63
+ }
64
+ const points = [];
65
+ if (Number(prev.data.y) > 0) {
66
+ points.push([prev.x, prev.y]);
67
+ }
68
+ else {
69
+ points.push([prev.x, prev.y + prev.height]);
70
+ }
71
+ if (Number(d.data.y) > 0 && !d.data.total) {
72
+ points.push([d.x + d.width, d.y + d.height]);
73
+ }
74
+ else {
75
+ points.push([d.x + d.width, d.y]);
76
+ }
77
+ return line(points);
78
+ })
79
+ .attr('stroke-width', 1)
80
+ .attr('stroke-dasharray', () => getLineDashArray(DashStyle.Dash, 1));
81
+ dispatcher.on('hover-shape.waterfall', (data) => {
82
+ const hoverEnabled = hoverOptions === null || hoverOptions === void 0 ? void 0 : hoverOptions.enabled;
83
+ const inactiveEnabled = inactiveOptions === null || inactiveOptions === void 0 ? void 0 : inactiveOptions.enabled;
84
+ if (!data) {
85
+ if (hoverEnabled) {
86
+ rectSelection.attr('fill', (d) => getWaterfallPointColor(d.data, d.series));
87
+ }
88
+ if (inactiveEnabled) {
89
+ rectSelection.attr('opacity', null);
90
+ labelSelection.attr('opacity', null);
91
+ }
92
+ return;
93
+ }
94
+ if (hoverEnabled) {
95
+ const hoveredValues = data.map((d) => d.data.x);
96
+ rectSelection.attr('fill', (d) => {
97
+ var _a;
98
+ const fillColor = getWaterfallPointColor(d.data, d.series);
99
+ if (hoveredValues.includes(d.data.x)) {
100
+ const brightness = hoverOptions === null || hoverOptions === void 0 ? void 0 : hoverOptions.brightness;
101
+ return ((_a = color(fillColor)) === null || _a === void 0 ? void 0 : _a.brighter(brightness).toString()) || fillColor;
102
+ }
103
+ return fillColor;
104
+ });
105
+ }
106
+ if (inactiveEnabled) {
107
+ const hoveredSeries = data.map((d) => d.series.id);
108
+ rectSelection.attr('opacity', (d) => {
109
+ return hoveredSeries.includes(d.series.id)
110
+ ? null
111
+ : (inactiveOptions === null || inactiveOptions === void 0 ? void 0 : inactiveOptions.opacity) || null;
112
+ });
113
+ labelSelection.attr('opacity', (d) => {
114
+ return hoveredSeries.includes(d.series.id)
115
+ ? null
116
+ : (inactiveOptions === null || inactiveOptions === void 0 ? void 0 : inactiveOptions.opacity) || null;
117
+ });
118
+ }
119
+ });
120
+ return () => {
121
+ dispatcher.on('hover-shape.waterfall', null);
122
+ };
123
+ }, [dispatcher, preparedData, seriesOptions]);
124
+ return React.createElement("g", { ref: ref, className: b() });
125
+ };
@@ -0,0 +1,12 @@
1
+ import type { ChartScale } from '../../useAxisScales';
2
+ import type { PreparedAxis } from '../../useChartOptions/types';
3
+ import type { PreparedSeriesOptions, PreparedWaterfallSeries } from '../../useSeries/types';
4
+ import type { PreparedWaterfallData } from './types';
5
+ export declare const prepareWaterfallData: (args: {
6
+ series: PreparedWaterfallSeries[];
7
+ seriesOptions: PreparedSeriesOptions;
8
+ xAxis: PreparedAxis;
9
+ xScale: ChartScale;
10
+ yAxis: PreparedAxis[];
11
+ yScale: ChartScale;
12
+ }) => PreparedWaterfallData[];
@@ -0,0 +1,132 @@
1
+ import get from 'lodash/get';
2
+ import sortBy from 'lodash/sortBy';
3
+ import { getLabelsSize } from '../../../utils';
4
+ import { MIN_BAR_GAP, MIN_BAR_WIDTH } from '../constants';
5
+ import { getXValue, getYValue } from '../utils';
6
+ function getLabelData(d, plotHeight) {
7
+ if (!d.series.dataLabels.enabled) {
8
+ return undefined;
9
+ }
10
+ const text = String(d.data.label || d.subTotal);
11
+ const style = d.series.dataLabels.style;
12
+ const { maxHeight: height, maxWidth: width } = getLabelsSize({ labels: [text], style });
13
+ let y;
14
+ if (Number(d.data.y) > 0 || d.data.total) {
15
+ y = Math.max(height, d.y - d.series.dataLabels.padding);
16
+ }
17
+ else {
18
+ y = Math.min(plotHeight - d.series.dataLabels.padding, d.y + d.height + d.series.dataLabels.padding + height);
19
+ }
20
+ return {
21
+ text,
22
+ x: d.x + d.width / 2,
23
+ y,
24
+ style,
25
+ size: { width, height },
26
+ textAnchor: 'middle',
27
+ series: d.series,
28
+ };
29
+ }
30
+ function getBandWidth(args) {
31
+ const { series, xAxis, xScale } = args;
32
+ if (xAxis.type === 'category') {
33
+ const xBandScale = xScale;
34
+ return xBandScale.bandwidth();
35
+ }
36
+ const xLinearScale = xScale;
37
+ const xValues = series.reduce((acc, s) => {
38
+ s.data.forEach((dataItem) => acc.push(Number(dataItem.x)));
39
+ return acc;
40
+ }, []);
41
+ let bandWidth = Infinity;
42
+ xValues.sort().forEach((xValue, index) => {
43
+ if (index > 0 && xValue !== xValues[index - 1]) {
44
+ const dist = xLinearScale(xValue) - xLinearScale(xValues[index - 1]);
45
+ if (dist < bandWidth) {
46
+ bandWidth = dist;
47
+ }
48
+ }
49
+ });
50
+ return bandWidth;
51
+ }
52
+ export const prepareWaterfallData = (args) => {
53
+ const { series, seriesOptions, xAxis, xScale, yAxis: [yAxis], yScale, } = args;
54
+ const yLinearScale = yScale;
55
+ const plotHeight = yLinearScale(yLinearScale.domain()[0]);
56
+ const barMaxWidth = get(seriesOptions, 'waterfall.barMaxWidth');
57
+ const barPadding = get(seriesOptions, 'waterfall.barPadding');
58
+ const flattenData = series.reduce((acc, s) => {
59
+ acc.push(...s.data.map((d) => ({ data: d, series: s })));
60
+ return acc;
61
+ }, []);
62
+ const data = sortBy(flattenData, (d) => d.data.x);
63
+ const bandWidth = getBandWidth({
64
+ series,
65
+ xAxis,
66
+ xScale,
67
+ });
68
+ const rectGap = Math.max(bandWidth * barPadding, MIN_BAR_GAP);
69
+ const rectWidth = Math.max(MIN_BAR_WIDTH, Math.min(bandWidth - rectGap, barMaxWidth));
70
+ const yZero = getYValue({
71
+ point: { y: 0 },
72
+ yScale,
73
+ yAxis,
74
+ });
75
+ let totalValue = 0;
76
+ const result = [];
77
+ data.forEach((item, _index) => {
78
+ if (typeof item.data.y !== 'number' && !item.data.total) {
79
+ return;
80
+ }
81
+ if (!item.data.total) {
82
+ totalValue += Number(item.data.y);
83
+ }
84
+ const prevPoint = result[result.length - 1];
85
+ const xCenter = getXValue({ point: item.data, xAxis, xScale });
86
+ const x = xCenter - rectWidth / 2;
87
+ const yValue = Number(item.data.total ? totalValue : item.data.y);
88
+ const height = yZero -
89
+ getYValue({
90
+ point: { y: Math.abs(yValue) },
91
+ yScale,
92
+ yAxis,
93
+ });
94
+ let y;
95
+ if (!prevPoint || item.data.total) {
96
+ y = getYValue({
97
+ point: {
98
+ y: yValue > 0 ? yValue : 0,
99
+ },
100
+ yScale,
101
+ yAxis,
102
+ });
103
+ }
104
+ else if (Number(prevPoint.data.y) < 0) {
105
+ if (Number(item.data.y) > 0) {
106
+ y = prevPoint.y + prevPoint.height - height;
107
+ }
108
+ else {
109
+ y = prevPoint.y + prevPoint.height;
110
+ }
111
+ }
112
+ else if (Number(item.data.y) < 0) {
113
+ y = prevPoint.y;
114
+ }
115
+ else {
116
+ y = prevPoint.y - height;
117
+ }
118
+ const preparedData = {
119
+ x,
120
+ y,
121
+ width: rectWidth,
122
+ height,
123
+ opacity: get(item.data, 'opacity', null),
124
+ data: item.data,
125
+ series: item.series,
126
+ subTotal: totalValue,
127
+ };
128
+ preparedData.label = getLabelData(preparedData, plotHeight);
129
+ result.push(preparedData);
130
+ });
131
+ return result;
132
+ };
@@ -0,0 +1,14 @@
1
+ import { WaterfallSeriesData } from '../../../../../../types';
2
+ import { LabelData } from '../../../types';
3
+ import { PreparedWaterfallSeries } from '../../useSeries/types';
4
+ export type PreparedWaterfallData = {
5
+ x: number;
6
+ y: number;
7
+ width: number;
8
+ height: number;
9
+ opacity: number | null;
10
+ series: PreparedWaterfallSeries;
11
+ data: WaterfallSeriesData;
12
+ label?: LabelData;
13
+ subTotal: number;
14
+ };
@@ -45,7 +45,18 @@ export function getClosestPoints(args) {
45
45
  y0: d.y,
46
46
  y1: d.y + d.height,
47
47
  }));
48
- Array.prototype.push.apply(result, getClosestPointsByXValue(pointerX, pointerY, points));
48
+ result.push(...getClosestPointsByXValue(pointerX, pointerY, points));
49
+ break;
50
+ }
51
+ case 'waterfall': {
52
+ const points = list.map((d) => ({
53
+ data: d.data,
54
+ series: d.series,
55
+ x: d.x + d.width / 2,
56
+ y0: d.y,
57
+ y1: d.y + d.height,
58
+ }));
59
+ result.push(...getClosestPointsByXValue(pointerX, pointerY, points));
49
60
  break;
50
61
  }
51
62
  case 'area': {
@@ -59,12 +70,12 @@ export function getClosestPoints(args) {
59
70
  })));
60
71
  return acc;
61
72
  }, []);
62
- Array.prototype.push.apply(result, getClosestPointsByXValue(pointerX, pointerY, points));
73
+ result.push(...getClosestPointsByXValue(pointerX, pointerY, points));
63
74
  break;
64
75
  }
65
76
  case 'line': {
66
77
  const points = list.reduce((acc, d) => {
67
- Array.prototype.push.apply(acc, d.points.map((p) => ({
78
+ acc.push(...d.points.map((p) => ({
68
79
  data: p.data,
69
80
  series: p.series,
70
81
  x: p.x,
@@ -73,7 +84,7 @@ export function getClosestPoints(args) {
73
84
  })));
74
85
  return acc;
75
86
  }, []);
76
- Array.prototype.push.apply(result, getClosestPointsByXValue(pointerX, pointerY, points));
87
+ result.push(...getClosestPointsByXValue(pointerX, pointerY, points));
77
88
  break;
78
89
  }
79
90
  case 'bar-y': {
@@ -96,7 +107,7 @@ export function getClosestPoints(args) {
96
107
  closestXIndex = closestPoints.findIndex((p) => pointerX > p.x && pointerX < p.x + p.width);
97
108
  }
98
109
  }
99
- Array.prototype.push.apply(result, closestPoints.map((p, i) => ({
110
+ result.push(...closestPoints.map((p, i) => ({
100
111
  data: p.data,
101
112
  series: p.series,
102
113
  closest: i === closestXIndex,
@@ -7,6 +7,7 @@ export * from './time';
7
7
  export * from './axis';
8
8
  export * from './labels';
9
9
  export * from './symbol';
10
+ export * from './series';
10
11
  export type AxisDirection = 'x' | 'y';
11
12
  type UnknownSeries = {
12
13
  type: ChartKitWidgetSeries['type'];
@@ -13,6 +13,7 @@ export * from './time';
13
13
  export * from './axis';
14
14
  export * from './labels';
15
15
  export * from './symbol';
16
+ export * from './series';
16
17
  const CHARTS_WITHOUT_AXIS = ['pie', 'treemap'];
17
18
  /**
18
19
  * Checks whether the series should be drawn with axes.
@@ -70,6 +71,16 @@ export const getDomainDataYBySeries = (series) => {
70
71
  });
71
72
  break;
72
73
  }
74
+ case 'waterfall': {
75
+ let yValue = 0;
76
+ seriesList.forEach((s) => {
77
+ s.data.forEach((d) => {
78
+ yValue += Number(d.y) || 0;
79
+ acc.push(yValue);
80
+ });
81
+ });
82
+ break;
83
+ }
73
84
  default: {
74
85
  seriesList.filter(isSeriesWithNumericalYValues).forEach((s) => {
75
86
  acc.push(...s.data.map((d) => d.y));
@@ -0,0 +1 @@
1
+ export * from './waterfall';
@@ -0,0 +1 @@
1
+ export * from './waterfall';
@@ -0,0 +1,4 @@
1
+ import { WaterfallSeriesData } from '../../../../../types';
2
+ import { PreparedWaterfallSeries } from '../../hooks';
3
+ export declare function getWaterfallPointColor(point: WaterfallSeriesData, series: PreparedWaterfallSeries): string;
4
+ export declare function getWaterfallPointSubtotal(point: WaterfallSeriesData, series: PreparedWaterfallSeries): number | null;
@@ -0,0 +1,25 @@
1
+ export function getWaterfallPointColor(point, series) {
2
+ if (point.color) {
3
+ return point.color;
4
+ }
5
+ if (point.total) {
6
+ return series.color;
7
+ }
8
+ if (Number(point.y) > 0) {
9
+ return series.positiveColor;
10
+ }
11
+ return series.negativeColor;
12
+ }
13
+ export function getWaterfallPointSubtotal(point, series) {
14
+ const pointIndex = series.data.indexOf(point);
15
+ if (pointIndex === -1) {
16
+ return null;
17
+ }
18
+ return series.data.reduce((sum, d, index) => {
19
+ if (index <= pointIndex) {
20
+ const value = d.total ? 0 : Number(d.y);
21
+ return sum + value;
22
+ }
23
+ return sum;
24
+ }, 0);
25
+ }
@@ -363,6 +363,11 @@ export declare class HighchartsComponent extends React.PureComponent<Props, Stat
363
363
  mouseOut: () => void;
364
364
  click: (event: any) => void;
365
365
  };
366
+ point: {
367
+ events: {
368
+ click: () => false;
369
+ };
370
+ };
366
371
  marker: {
367
372
  states: {
368
373
  hover: {
@@ -526,6 +526,11 @@ export function prepareConfig(data: any, options: any, isMobile: any, holidays:
526
526
  mouseOut: () => void;
527
527
  click: (event: any) => void;
528
528
  };
529
+ point: {
530
+ events: {
531
+ click: () => false;
532
+ };
533
+ };
529
534
  marker: {
530
535
  states: {
531
536
  hover: {
@@ -1264,6 +1264,14 @@ export function prepareConfig(data, options, isMobile, holidays) {
1264
1264
  }
1265
1265
  },
1266
1266
  },
1267
+ point: {
1268
+ events: {
1269
+ click: function () {
1270
+ // Prevent slicing of pie segment after clicking it
1271
+ return false;
1272
+ },
1273
+ },
1274
+ },
1267
1275
  marker: options.splitTooltip
1268
1276
  ? {
1269
1277
  states: {
@@ -57,7 +57,7 @@ const first = {
57
57
  };
58
58
  const second = {
59
59
  allowPointSelect: true,
60
- slicedOffset: 0,
60
+ slicedOffset: 20,
61
61
  cursor: 'pointer',
62
62
  showInLegend: true,
63
63
  };
@@ -357,6 +357,11 @@ declare function getGraph({ options, data, comments, isMobile, holidays }: GetGr
357
357
  mouseOut: () => void;
358
358
  click: (event: any) => void;
359
359
  };
360
+ point: {
361
+ events: {
362
+ click: () => false;
363
+ };
364
+ };
360
365
  marker: {
361
366
  states: {
362
367
  hover: {
@@ -19,6 +19,7 @@ export * from './title';
19
19
  export * from './tooltip';
20
20
  export * from './halo';
21
21
  export * from './treemap';
22
+ export * from './waterfall';
22
23
  export type ChartKitWidgetData<T = any> = {
23
24
  chart?: ChartKitWidgetChart;
24
25
  legend?: ChartKitWidgetLegend;
@@ -13,3 +13,4 @@ export * from './title';
13
13
  export * from './tooltip';
14
14
  export * from './halo';
15
15
  export * from './treemap';
16
+ export * from './waterfall';
@@ -9,8 +9,9 @@ import type { PointMarkerOptions } from './marker';
9
9
  import type { PieSeries, PieSeriesData } from './pie';
10
10
  import type { ScatterSeries, ScatterSeriesData } from './scatter';
11
11
  import type { TreemapSeries, TreemapSeriesData } from './treemap';
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>;
12
+ import type { WaterfallSeries, WaterfallSeriesData } from './waterfall';
13
+ export type ChartKitWidgetSeries<T = any> = ScatterSeries<T> | PieSeries<T> | BarXSeries<T> | BarYSeries<T> | LineSeries<T> | AreaSeries<T> | TreemapSeries<T> | WaterfallSeries<T>;
14
+ export type ChartKitWidgetSeriesData<T = any> = ScatterSeriesData<T> | PieSeriesData<T> | BarXSeriesData<T> | BarYSeriesData<T> | LineSeriesData<T> | AreaSeriesData<T> | TreemapSeriesData<T> | WaterfallSeriesData<T>;
14
15
  export type DataLabelRendererData<T = any> = {
15
16
  data: ChartKitWidgetSeriesData<T>;
16
17
  };
@@ -202,5 +203,23 @@ export type ChartKitWidgetSeriesOptions = {
202
203
  inactive?: BasicInactiveState;
203
204
  };
204
205
  };
206
+ waterfall?: {
207
+ /** The maximum allowed pixel width for a column.
208
+ * This prevents the columns from becoming too wide when there is a small number of points in the chart.
209
+ *
210
+ * @default 50
211
+ */
212
+ barMaxWidth?: number;
213
+ /** Padding between each column or bar, in x axis units.
214
+ *
215
+ * @default 0.1
216
+ * */
217
+ barPadding?: number;
218
+ /** Options for the series states that provide additional styling information to the series. */
219
+ states?: {
220
+ hover?: BasicHoverState;
221
+ inactive?: BasicInactiveState;
222
+ };
223
+ };
205
224
  };
206
225
  export {};
@@ -6,6 +6,7 @@ import type { LineSeries, LineSeriesData } from './line';
6
6
  import type { PieSeries, PieSeriesData } from './pie';
7
7
  import type { ScatterSeries, ScatterSeriesData } from './scatter';
8
8
  import type { TreemapSeries, TreemapSeriesData } from './treemap';
9
+ import type { WaterfallSeries, WaterfallSeriesData } from './waterfall';
9
10
  export type TooltipDataChunkBarX<T = any> = {
10
11
  data: BarXSeriesData<T>;
11
12
  series: BarXSeries<T>;
@@ -50,7 +51,11 @@ export type TooltipDataChunkTreemap<T = any> = {
50
51
  data: TreemapSeriesData<T>;
51
52
  series: TreemapSeries<T>;
52
53
  };
53
- export type TooltipDataChunk<T = any> = (TooltipDataChunkBarX<T> | TooltipDataChunkBarY<T> | TooltipDataChunkPie<T> | TooltipDataChunkScatter<T> | TooltipDataChunkLine<T> | TooltipDataChunkArea<T> | TooltipDataChunkTreemap<T>) & {
54
+ export type TooltipDataChunkWaterfall<T = any> = {
55
+ data: WaterfallSeriesData<T>;
56
+ series: WaterfallSeries<T>;
57
+ };
58
+ export type TooltipDataChunk<T = any> = (TooltipDataChunkBarX<T> | TooltipDataChunkBarY<T> | TooltipDataChunkPie<T> | TooltipDataChunkScatter<T> | TooltipDataChunkLine<T> | TooltipDataChunkArea<T> | TooltipDataChunkTreemap<T> | TooltipDataChunkWaterfall<T>) & {
54
59
  closest?: boolean;
55
60
  };
56
61
  export type ChartKitWidgetTooltip<T = any> = {
@@ -0,0 +1,39 @@
1
+ import { SeriesType } from '../../constants';
2
+ import type { BaseSeries, BaseSeriesData } from './base';
3
+ import { ChartKitWidgetLegend, RectLegendSymbolOptions } from './legend';
4
+ export type WaterfallSeriesData<T = any> = BaseSeriesData<T> & {
5
+ /**
6
+ * The `x` value. Depending on the context , it may represents:
7
+ * - numeric value (for `linear` x axis)
8
+ * - timestamp value (for `datetime` x axis)
9
+ * - x axis category value (for `category` x axis). If the type is a string, then it is a category value itself. If the type is a number, then it is the index of an element in the array of categories described in `xAxis.categories`
10
+ */
11
+ x?: string | number;
12
+ /**
13
+ * The `y` value. Depending on the context , it may represents:
14
+ * - numeric value (for `linear` y axis)
15
+ */
16
+ y?: number;
17
+ /** Data label value of the point. If not specified, the y value is used. */
18
+ label?: string | number;
19
+ /** Individual opacity for the point. */
20
+ opacity?: number;
21
+ /** When this property is true, the point display the total sum across the entire series. */
22
+ total?: boolean;
23
+ };
24
+ export type WaterfallSeries<T = any> = BaseSeries & {
25
+ type: typeof SeriesType.Waterfall;
26
+ data: WaterfallSeriesData<T>[];
27
+ /** The name of the series (used in legend, tooltip etc). */
28
+ name: string;
29
+ /** The main color of the series (hex, rgba). */
30
+ color?: string;
31
+ /** The color used for positive values. If it is not specified, the general color of the series is used. */
32
+ positiveColor?: string;
33
+ /** The color used for negative values. If it is not specified, the general color of the series is used. */
34
+ negativeColor?: string;
35
+ /** Individual series legend options. Has higher priority than legend options in widget data. */
36
+ legend?: ChartKitWidgetLegend & {
37
+ symbol?: RectLegendSymbolOptions;
38
+ };
39
+ };
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gravity-ui/chartkit",
3
- "version": "5.4.0",
3
+ "version": "5.5.0",
4
4
  "description": "React component used to render charts based on any sources you need",
5
5
  "license": "MIT",
6
6
  "repository": "git@github.com:gravity-ui/ChartKit.git",