@gravity-ui/charts 1.42.4 → 1.43.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 (75) hide show
  1. package/dist/cjs/components/AxisX/AxisX.js +27 -0
  2. package/dist/cjs/components/AxisX/prepare-axis-data.js +41 -0
  3. package/dist/cjs/components/AxisX/types.d.ts +18 -1
  4. package/dist/cjs/components/AxisY/AxisY.js +27 -0
  5. package/dist/cjs/components/AxisY/prepare-axis-data.js +41 -0
  6. package/dist/cjs/components/AxisY/types.d.ts +18 -1
  7. package/dist/cjs/components/ChartInner/index.js +16 -0
  8. package/dist/cjs/components/ChartInner/useChartInnerHandlers.js +6 -5
  9. package/dist/cjs/components/ChartInner/useChartInnerProps.d.ts +2 -4
  10. package/dist/cjs/components/ChartInner/useChartInnerProps.js +1 -1
  11. package/dist/cjs/components/ChartInner/useDefaultState.js +4 -3
  12. package/dist/cjs/components/ChartInner/utils/chart.js +1 -1
  13. package/dist/cjs/components/ChartInner/utils/normalized-original-data.d.ts +1 -0
  14. package/dist/cjs/components/ChartInner/utils/title.d.ts +2 -1
  15. package/dist/cjs/components/ChartInner/utils/title.js +69 -11
  16. package/dist/cjs/components/Title/index.js +3 -5
  17. package/dist/cjs/components/Tooltip/ChartTooltipContent.d.ts +2 -1
  18. package/dist/cjs/components/Tooltip/ChartTooltipContent.js +3 -2
  19. package/dist/cjs/components/Tooltip/index.js +2 -2
  20. package/dist/cjs/core/axes/types.d.ts +26 -9
  21. package/dist/cjs/core/axes/x-axis.js +14 -1
  22. package/dist/cjs/core/axes/y-axis.js +20 -7
  23. package/dist/cjs/core/constants/defaults/axis.d.ts +1 -0
  24. package/dist/cjs/core/constants/defaults/axis.js +1 -0
  25. package/dist/cjs/core/scales/y-scale.js +37 -13
  26. package/dist/cjs/core/types/chart/axis.d.ts +43 -1
  27. package/dist/cjs/core/types/chart/title.d.ts +10 -0
  28. package/dist/cjs/core/types/chart/tooltip.d.ts +3 -1
  29. package/dist/cjs/core/utils/common.js +1 -1
  30. package/dist/cjs/core/utils/get-hovered-plots.d.ts +3 -2
  31. package/dist/cjs/core/utils/get-hovered-plots.js +28 -4
  32. package/dist/cjs/core/utils/text.js +12 -2
  33. package/dist/cjs/hooks/types.d.ts +5 -2
  34. package/dist/cjs/hooks/useShapes/area/prepare-data.js +11 -6
  35. package/dist/cjs/hooks/useShapes/bar-x/prepare-data.js +12 -4
  36. package/dist/cjs/hooks/useTooltip/index.d.ts +3 -2
  37. package/dist/cjs/hooks/useTooltip/index.js +5 -3
  38. package/dist/esm/components/AxisX/AxisX.js +27 -0
  39. package/dist/esm/components/AxisX/prepare-axis-data.js +41 -0
  40. package/dist/esm/components/AxisX/types.d.ts +18 -1
  41. package/dist/esm/components/AxisY/AxisY.js +27 -0
  42. package/dist/esm/components/AxisY/prepare-axis-data.js +41 -0
  43. package/dist/esm/components/AxisY/types.d.ts +18 -1
  44. package/dist/esm/components/ChartInner/index.js +16 -0
  45. package/dist/esm/components/ChartInner/useChartInnerHandlers.js +6 -5
  46. package/dist/esm/components/ChartInner/useChartInnerProps.d.ts +2 -4
  47. package/dist/esm/components/ChartInner/useChartInnerProps.js +1 -1
  48. package/dist/esm/components/ChartInner/useDefaultState.js +4 -3
  49. package/dist/esm/components/ChartInner/utils/chart.js +1 -1
  50. package/dist/esm/components/ChartInner/utils/normalized-original-data.d.ts +1 -0
  51. package/dist/esm/components/ChartInner/utils/title.d.ts +2 -1
  52. package/dist/esm/components/ChartInner/utils/title.js +69 -11
  53. package/dist/esm/components/Title/index.js +3 -5
  54. package/dist/esm/components/Tooltip/ChartTooltipContent.d.ts +2 -1
  55. package/dist/esm/components/Tooltip/ChartTooltipContent.js +3 -2
  56. package/dist/esm/components/Tooltip/index.js +2 -2
  57. package/dist/esm/core/axes/types.d.ts +26 -9
  58. package/dist/esm/core/axes/x-axis.js +14 -1
  59. package/dist/esm/core/axes/y-axis.js +20 -7
  60. package/dist/esm/core/constants/defaults/axis.d.ts +1 -0
  61. package/dist/esm/core/constants/defaults/axis.js +1 -0
  62. package/dist/esm/core/scales/y-scale.js +37 -13
  63. package/dist/esm/core/types/chart/axis.d.ts +43 -1
  64. package/dist/esm/core/types/chart/title.d.ts +10 -0
  65. package/dist/esm/core/types/chart/tooltip.d.ts +3 -1
  66. package/dist/esm/core/utils/common.js +1 -1
  67. package/dist/esm/core/utils/get-hovered-plots.d.ts +3 -2
  68. package/dist/esm/core/utils/get-hovered-plots.js +28 -4
  69. package/dist/esm/core/utils/text.js +12 -2
  70. package/dist/esm/hooks/types.d.ts +5 -2
  71. package/dist/esm/hooks/useShapes/area/prepare-data.js +11 -6
  72. package/dist/esm/hooks/useShapes/bar-x/prepare-data.js +12 -4
  73. package/dist/esm/hooks/useTooltip/index.d.ts +3 -2
  74. package/dist/esm/hooks/useTooltip/index.js +5 -3
  75. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import type { AxisCrosshair, AxisPlotBand, BaseTextStyle, ChartAxis, ChartAxisLabels, ChartAxisRangeSlider, ChartAxisTitleAlignment, ChartAxisTitleRotation, ChartAxisType, DeepRequired, MeaningfulAny, PlotLayerPlacement } from '../../types';
1
+ import type { AxisCrosshair, AxisPlotBand, AxisPlotShape, BaseTextStyle, ChartAxis, ChartAxisLabels, ChartAxisRangeSlider, ChartAxisTitleAlignment, ChartAxisTitleRotation, ChartAxisType, DeepRequired, MeaningfulAny, PlotLayerPlacement } from '../../types';
2
2
  import type { DashStyle } from '../constants';
3
3
  type PreparedAxisLabels = Omit<ChartAxisLabels, 'enabled' | 'padding' | 'style' | 'autoRotation'> & Required<Pick<ChartAxisLabels, 'enabled' | 'padding' | 'margin' | 'rotation' | 'html'>> & {
4
4
  style: BaseTextStyle;
@@ -19,19 +19,35 @@ export type PreparedAxisPlotBand = Required<AxisPlotBand> & {
19
19
  };
20
20
  type PreparedAxisCrosshair = Required<AxisCrosshair>;
21
21
  export type PreparedAxisPlotLine = {
22
- value: number;
23
22
  color: string;
24
- width: number;
25
- dashStyle: DashStyle;
26
- opacity: number;
27
- layerPlacement: PlotLayerPlacement;
28
23
  custom?: MeaningfulAny;
24
+ dashStyle: DashStyle;
25
+ hoverThreshold: number;
29
26
  label: {
30
- text: string;
31
- style: BaseTextStyle;
32
27
  padding: number;
33
28
  qa?: string;
29
+ style: BaseTextStyle;
30
+ text: string;
34
31
  };
32
+ layerPlacement: PlotLayerPlacement;
33
+ opacity: number;
34
+ value: number;
35
+ width: number;
36
+ };
37
+ export type PreparedAxisPlotShape = {
38
+ custom?: MeaningfulAny;
39
+ hitbox: {
40
+ height: number;
41
+ width: number;
42
+ x: number;
43
+ y: number;
44
+ };
45
+ layerPlacement: PlotLayerPlacement;
46
+ opacity: number;
47
+ renderer: AxisPlotShape['renderer'];
48
+ value: number | string;
49
+ x: number;
50
+ y: number;
35
51
  };
36
52
  export type PreparedRangeSlider = DeepRequired<Omit<ChartAxisRangeSlider, 'defaultRange'>> & {
37
53
  defaultRange?: ChartAxisRangeSlider['defaultRange'];
@@ -40,7 +56,7 @@ export type PreparedAxisTickMarks = {
40
56
  enabled: boolean;
41
57
  length: number;
42
58
  };
43
- type PreparedBaseAxis = Omit<ChartAxis, 'type' | 'labels' | 'plotLines' | 'plotBands'> & {
59
+ type PreparedBaseAxis = Omit<ChartAxis, 'type' | 'labels' | 'plotLines' | 'plotBands' | 'plotShapes'> & {
44
60
  type: ChartAxisType;
45
61
  labels: PreparedAxisLabels;
46
62
  title: {
@@ -67,6 +83,7 @@ type PreparedBaseAxis = Omit<ChartAxis, 'type' | 'labels' | 'plotLines' | 'plotB
67
83
  plotIndex: number;
68
84
  plotLines: PreparedAxisPlotLine[];
69
85
  plotBands: PreparedAxisPlotBand[];
86
+ plotShapes: PreparedAxisPlotShape[];
70
87
  crosshair: PreparedAxisCrosshair;
71
88
  };
72
89
  export type PreparedXAxis = PreparedBaseAxis & {
@@ -1,6 +1,6 @@
1
1
  import get from 'lodash/get';
2
2
  import { TIME_UNITS, calculateCos, calculateNumericProperty, calculateSin, formatAxisTickLabel, getDefaultDateFormat, getHorizontalHtmlTextHeight, getLabelsSize, getMinSpaceBetween, getTextSizeFn, isAxisRelatedSeries, wrapText, } from '../utils';
3
- import { DASH_STYLE, DEFAULT_AXIS_LABEL_FONT_SIZE, SERIES_TYPE, axisCrosshairDefaults, axisLabelsDefaults, axisTickMarksDefaults, xAxisTitleDefaults, } from '../constants';
3
+ import { DASH_STYLE, DEFAULT_AXIS_LABEL_FONT_SIZE, PLOT_LINE_HOVER_THRESHOLD, SERIES_TYPE, axisCrosshairDefaults, axisLabelsDefaults, axisTickMarksDefaults, xAxisTitleDefaults, } from '../constants';
4
4
  import { createXScale } from '../scales/x-scale';
5
5
  import { getXAxisTickValues } from '../utils/axis/x-axis';
6
6
  import { getPreparedRangeSlider } from './range-slider';
@@ -155,6 +155,7 @@ export const getPreparedXAxis = async ({ xAxis, seriesData, width, boundsWidth,
155
155
  color: get(d, 'color', 'var(--g-color-base-brand)'),
156
156
  width: get(d, 'width', 1),
157
157
  dashStyle: get(d, 'dashStyle', DASH_STYLE.Solid),
158
+ hoverThreshold: get(d, 'hoverThreshold', PLOT_LINE_HOVER_THRESHOLD),
158
159
  opacity: get(d, 'opacity', 1),
159
160
  layerPlacement: get(d, 'layerPlacement', 'before'),
160
161
  custom: d.custom,
@@ -169,6 +170,18 @@ export const getPreparedXAxis = async ({ xAxis, seriesData, width, boundsWidth,
169
170
  custom: d.custom,
170
171
  label: prepareAxisPlotLabel(d),
171
172
  })),
173
+ // x, y and hitbox are populated later in prepare-axis-data
174
+ // after pixel coordinates and element dimensions are computed
175
+ plotShapes: get(xAxis, 'plotShapes', []).map((d) => ({
176
+ custom: d.custom,
177
+ hitbox: { x: 0, y: 0, width: 0, height: 0 },
178
+ layerPlacement: get(d, 'layerPlacement', 'before'),
179
+ opacity: get(d, 'opacity', 1),
180
+ renderer: d.renderer,
181
+ value: d.value,
182
+ x: 0,
183
+ y: 0,
184
+ })),
172
185
  crosshair: {
173
186
  enabled: get(xAxis, 'crosshair.enabled', axisCrosshairDefaults.enabled),
174
187
  color: get(xAxis, 'crosshair.color', axisCrosshairDefaults.color),
@@ -1,7 +1,7 @@
1
1
  import get from 'lodash/get';
2
- import { calculateNumericProperty, formatAxisTickLabel, getDefaultDateFormat, getDefaultMinYAxisValue, getHorizontalHtmlTextHeight, getLabelsSize, getMinSpaceBetween, getTextSizeFn, isAxisRelatedSeries, shouldSyncAxisWithPrimary, wrapText, } from '../utils';
2
+ import { calculateNumericProperty, formatAxisTickLabel, getDefaultDateFormat, getHorizontalHtmlTextHeight, getLabelsSize, getMinSpaceBetween, getTextSizeFn, isAxisRelatedSeries, shouldSyncAxisWithPrimary, wrapText, } from '../utils';
3
3
  import { getTickValues } from '../../components/AxisY/utils';
4
- import { DASH_STYLE, DEFAULT_AXIS_LABEL_FONT_SIZE, DEFAULT_AXIS_TYPE, SERIES_TYPE, axisCrosshairDefaults, axisLabelsDefaults, axisTickMarksDefaults, yAxisTitleDefaults, } from '../constants';
4
+ import { DASH_STYLE, DEFAULT_AXIS_LABEL_FONT_SIZE, DEFAULT_AXIS_TYPE, PLOT_LINE_HOVER_THRESHOLD, SERIES_TYPE, axisCrosshairDefaults, axisLabelsDefaults, axisTickMarksDefaults, yAxisTitleDefaults, } from '../constants';
5
5
  import { createYScale } from '../scales/y-scale';
6
6
  import { prepareAxisPlotLabel } from './utils';
7
7
  export const getYAxisLabelMaxWidth = async (args) => {
@@ -50,7 +50,7 @@ export const getPreparedYAxis = ({ height, boundsHeight, width, seriesData, yAxi
50
50
  return Promise.resolve([]);
51
51
  }
52
52
  return Promise.all(axisItems.map(async (axisItem, axisIndex) => {
53
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
53
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
54
54
  const plotIndex = get(axisItem, 'plotIndex', 0);
55
55
  const firstPlotAxis = !axisByPlot[plotIndex];
56
56
  if (firstPlotAxis) {
@@ -142,7 +142,7 @@ export const getPreparedYAxis = ({ height, boundsHeight, width, seriesData, yAxi
142
142
  maxWidth: titleMaxWidth !== null && titleMaxWidth !== void 0 ? titleMaxWidth : Infinity,
143
143
  rotation: titleRotation,
144
144
  },
145
- min: (_o = get(axisItem, 'min')) !== null && _o !== void 0 ? _o : getDefaultMinYAxisValue(axisSeriesData),
145
+ min: get(axisItem, 'min'),
146
146
  max: get(axisItem, 'max'),
147
147
  startOnTick: get(axisItem, 'startOnTick'),
148
148
  endOnTick: get(axisItem, 'endOnTick'),
@@ -151,12 +151,12 @@ export const getPreparedYAxis = ({ height, boundsHeight, width, seriesData, yAxi
151
151
  enabled: gridEnabled,
152
152
  },
153
153
  ticks: {
154
- pixelInterval: ((_p = axisItem.ticks) === null || _p === void 0 ? void 0 : _p.interval)
154
+ pixelInterval: ((_o = axisItem.ticks) === null || _o === void 0 ? void 0 : _o.interval)
155
155
  ? calculateNumericProperty({
156
156
  base: height,
157
- value: (_q = axisItem.ticks) === null || _q === void 0 ? void 0 : _q.interval,
157
+ value: (_p = axisItem.ticks) === null || _p === void 0 ? void 0 : _p.interval,
158
158
  })
159
- : (_r = axisItem.ticks) === null || _r === void 0 ? void 0 : _r.pixelInterval,
159
+ : (_q = axisItem.ticks) === null || _q === void 0 ? void 0 : _q.pixelInterval,
160
160
  },
161
161
  tickMarks: {
162
162
  enabled: isAxisVisible &&
@@ -170,6 +170,7 @@ export const getPreparedYAxis = ({ height, boundsHeight, width, seriesData, yAxi
170
170
  color: get(d, 'color', 'var(--g-color-base-brand)'),
171
171
  width: get(d, 'width', 1),
172
172
  dashStyle: get(d, 'dashStyle', DASH_STYLE.Solid),
173
+ hoverThreshold: get(d, 'hoverThreshold', PLOT_LINE_HOVER_THRESHOLD),
173
174
  opacity: get(d, 'opacity', 1),
174
175
  layerPlacement: get(d, 'layerPlacement', 'before'),
175
176
  custom: d.custom,
@@ -184,6 +185,18 @@ export const getPreparedYAxis = ({ height, boundsHeight, width, seriesData, yAxi
184
185
  custom: d.custom,
185
186
  label: prepareAxisPlotLabel(d),
186
187
  })),
188
+ // x, y and hitbox are populated later in prepare-axis-data
189
+ // after pixel coordinates and element dimensions are computed
190
+ plotShapes: get(axisItem, 'plotShapes', []).map((d) => ({
191
+ custom: d.custom,
192
+ hitbox: { x: 0, y: 0, width: 0, height: 0 },
193
+ layerPlacement: get(d, 'layerPlacement', 'before'),
194
+ opacity: get(d, 'opacity', 1),
195
+ renderer: d.renderer,
196
+ value: d.value,
197
+ x: 0,
198
+ y: 0,
199
+ })),
187
200
  crosshair: {
188
201
  enabled: get(axisItem, 'crosshair.enabled', axisCrosshairDefaults.enabled),
189
202
  color: get(axisItem, 'crosshair.color', axisCrosshairDefaults.color),
@@ -17,4 +17,5 @@ export declare const axisTickMarksDefaults: {
17
17
  length: number;
18
18
  };
19
19
  export declare const DEFAULT_AXIS_TYPE: ChartAxisType;
20
+ export declare const PLOT_LINE_HOVER_THRESHOLD = 4;
20
21
  export {};
@@ -30,3 +30,4 @@ export const axisTickMarksDefaults = {
30
30
  length: 6,
31
31
  };
32
32
  export const DEFAULT_AXIS_TYPE = 'linear';
33
+ export const PLOT_LINE_HOVER_THRESHOLD = 4;
@@ -1,7 +1,7 @@
1
1
  import { extent, tickStep, ticks } from 'd3-array';
2
2
  import { scaleBand, scaleLinear, scaleLog, scaleUtc } from 'd3-scale';
3
3
  import get from 'lodash/get';
4
- import { CHART_SERIES_WITH_VOLUME_ON_Y_AXIS, getDomainDataYBySeries, shouldSyncAxisWithPrimary, } from '../utils';
4
+ import { CHART_SERIES_WITH_VOLUME_ON_Y_AXIS, getDefaultMinYAxisValue, getDomainDataYBySeries, shouldSyncAxisWithPrimary, } from '../utils';
5
5
  import { getTickValues } from '../../components/AxisY/utils';
6
6
  import { getBandSize } from '../../hooks/utils/get-band-size';
7
7
  import { SERIES_TYPE } from '../constants';
@@ -83,14 +83,21 @@ function getDomainMinAlignedToStartTick(args) {
83
83
  const isStartOnTick = tickValues[0].y === range[0];
84
84
  let dNewMin = dMin;
85
85
  if (!isStartOnTick) {
86
- let step;
87
- if (typeof ((_a = tickValues[0]) === null || _a === void 0 ? void 0 : _a.value) === 'number' && typeof ((_b = tickValues[1]) === null || _b === void 0 ? void 0 : _b.value) === 'number') {
88
- step = tickValues[1].value - tickValues[0].value;
86
+ if (axis.type === 'logarithmic') {
87
+ const [nicedMin, _nicedMax] = scale.copy().nice().domain();
88
+ dNewMin = nicedMin;
89
89
  }
90
90
  else {
91
- step = tickStep(dMin, dMax, 1);
91
+ let step;
92
+ if (typeof ((_a = tickValues[0]) === null || _a === void 0 ? void 0 : _a.value) === 'number' &&
93
+ typeof ((_b = tickValues[1]) === null || _b === void 0 ? void 0 : _b.value) === 'number') {
94
+ step = tickValues[1].value - tickValues[0].value;
95
+ }
96
+ else {
97
+ step = tickStep(dMin, dMax, 1);
98
+ }
99
+ dNewMin = tickValues[0].value - step;
92
100
  }
93
- dNewMin = tickValues[0].value - step;
94
101
  }
95
102
  return dNewMin;
96
103
  }
@@ -104,17 +111,24 @@ function getDomainMaxAlignedToEndTick(args) {
104
111
  labelLineHeight: axis.labels.lineHeight,
105
112
  series,
106
113
  });
107
- const isEndOnTick = tickValues[tickValues.length - 1].y === range[1];
108
114
  let dNewMax = dMax;
115
+ const isEndOnTick = tickValues[tickValues.length - 1].y === range[1];
109
116
  if (!isEndOnTick) {
110
- let step;
111
- if (typeof ((_a = tickValues[0]) === null || _a === void 0 ? void 0 : _a.value) === 'number' && typeof ((_b = tickValues[1]) === null || _b === void 0 ? void 0 : _b.value) === 'number') {
112
- step = tickValues[1].value - tickValues[0].value;
117
+ if (axis.type === 'logarithmic') {
118
+ const [_nicedMin, nicedMax] = scale.copy().nice().domain();
119
+ dNewMax = nicedMax;
113
120
  }
114
121
  else {
115
- step = tickStep(dMin, dMax, 1);
122
+ let step;
123
+ if (typeof ((_a = tickValues[0]) === null || _a === void 0 ? void 0 : _a.value) === 'number' &&
124
+ typeof ((_b = tickValues[1]) === null || _b === void 0 ? void 0 : _b.value) === 'number') {
125
+ step = tickValues[1].value - tickValues[0].value;
126
+ }
127
+ else {
128
+ step = tickStep(dMin, dMax, 1);
129
+ }
130
+ dNewMax = tickValues[tickValues.length - 1].value + step;
116
131
  }
117
- dNewMax = tickValues[tickValues.length - 1].value + step;
118
132
  }
119
133
  return dNewMax;
120
134
  }
@@ -141,7 +155,17 @@ export function createYScale(args) {
141
155
  }
142
156
  if (hasNumberAndNullValues) {
143
157
  const [yMinDomain, yMaxDomain] = extent(domain);
144
- const yMin = typeof yMinPropsOrState === 'number' ? yMinPropsOrState : yMinDomain;
158
+ let yMin;
159
+ if (typeof yMinPropsOrState === 'number') {
160
+ yMin = yMinPropsOrState;
161
+ }
162
+ else if (axis.type === 'logarithmic') {
163
+ yMin = yMinDomain;
164
+ }
165
+ else {
166
+ const yMinDefault = getDefaultMinYAxisValue(series);
167
+ yMin = typeof yMinDefault === 'number' ? yMinDefault : yMinDomain;
168
+ }
145
169
  let yMax;
146
170
  if (typeof yMaxPropsOrState === 'number') {
147
171
  yMax = yMaxPropsOrState;
@@ -187,6 +187,8 @@ export interface ChartAxis {
187
187
  plotLines?: AxisPlotLine[];
188
188
  /** An array of colored bands stretching across the plot area marking an interval on the axis. */
189
189
  plotBands?: AxisPlotBand[];
190
+ /** An array of custom SVG elements placed at specific axis values. */
191
+ plotShapes?: AxisPlotShape[];
190
192
  /**
191
193
  * Small perpendicular marks on the axis line at each tick position.
192
194
  *
@@ -275,6 +277,46 @@ export interface AxisPlotLine extends AxisPlot {
275
277
  width?: number;
276
278
  /** Option for line stroke style. */
277
279
  dashStyle?: DashStyle;
280
+ /**
281
+ * Extra pixels added to each side of the line for hover detection.
282
+ * The total hoverable area equals `line.width + hoverThreshold * 2`.
283
+ * @default 4
284
+ */
285
+ hoverThreshold?: number;
286
+ }
287
+ export interface AxisPlotShape extends AxisPlot {
288
+ /**
289
+ * The position of the shape in axis units.
290
+ *
291
+ * Can be a number or a string (e.g., a category).
292
+ * When representing a date, the value **must be a timestamp** (number of milliseconds since Unix epoch).
293
+ */
294
+ value: number | string;
295
+ /**
296
+ * Custom SVG content renderer.
297
+ *
298
+ * Called with the pixel coordinates of the shape and the plot area dimensions.
299
+ * Must return a string of valid SVG markup that will be inserted inside a `<g>` container
300
+ * positioned at the shape's axis value.
301
+ *
302
+ * @example
303
+ * ```
304
+ * renderer: ({plotHeight}) =>
305
+ * `<circle cx="0" cy="${plotHeight}" r="4" fill="red"/>
306
+ * <line x1="0" y1="0" x2="0" y2="${plotHeight}" stroke="red" stroke-width="1"/>
307
+ * <text x="4" y="12" font-size="11" fill="currentColor">Label</text>`
308
+ * ```
309
+ */
310
+ renderer: (args: {
311
+ /** Pixel X coordinate in the plot area coordinate system */
312
+ x: number;
313
+ /** Pixel Y coordinate in the plot area coordinate system */
314
+ y: number;
315
+ /** Width of the plot area in pixels */
316
+ plotWidth: number;
317
+ /** Height of the plot area in pixels */
318
+ plotHeight: number;
319
+ }) => string;
278
320
  }
279
321
  export interface AxisPlotBand extends AxisPlot {
280
322
  /**
@@ -296,7 +338,7 @@ export interface AxisPlotBand extends AxisPlot {
296
338
  */
297
339
  to: number | string | null;
298
340
  }
299
- export interface AxisCrosshair extends Omit<AxisPlotLine, 'value' | 'label' | 'custom'> {
341
+ export interface AxisCrosshair extends Pick<AxisPlotLine, 'color' | 'dashStyle' | 'opacity' | 'layerPlacement' | 'width'> {
300
342
  /**
301
343
  * Whether the crosshair should snap to the point or follow the pointer independent of points.
302
344
  * @default true
@@ -2,6 +2,16 @@ import type { BaseTextStyle } from './base';
2
2
  export interface ChartTitle {
3
3
  text: string;
4
4
  style?: Partial<BaseTextStyle>;
5
+ /**
6
+ * Maximum number of text rows. If the text exceeds this limit, it is truncated with an ellipsis.
7
+ * Default: 1
8
+ */
9
+ maxRowCount?: number;
10
+ /**
11
+ * Space between the title and the chart area (in pixels).
12
+ * Default: 10
13
+ */
14
+ margin?: number;
5
15
  /**
6
16
  * Can be used for the UI automated test.
7
17
  * It is assigned as a data-qa attribute to an element.
@@ -2,7 +2,7 @@ import type { TOOLTIP_TOTALS_BUILT_IN_AGGREGATION } from '../../constants';
2
2
  import type { DateTimeLabelFormats } from '../../utils/time';
3
3
  import type { MeaningfulAny } from '../misc';
4
4
  import type { AreaSeries, AreaSeriesData } from './area';
5
- import type { AxisPlotBand, AxisPlotLine, ChartXAxis, ChartYAxis } from './axis';
5
+ import type { AxisPlotBand, AxisPlotLine, AxisPlotShape, ChartXAxis, ChartYAxis } from './axis';
6
6
  import type { BarXSeries, BarXSeriesData } from './bar-x';
7
7
  import type { BarYSeries, BarYSeriesData } from './bar-y';
8
8
  import type { CustomFormat, ValueFormat } from './base';
@@ -96,6 +96,8 @@ export interface ChartTooltipRendererArgs<T = MeaningfulAny> {
96
96
  hoveredPlotLines?: AxisPlotLine[];
97
97
  /** Plot bands that contain the current pointer position. */
98
98
  hoveredPlotBands?: AxisPlotBand[];
99
+ /** Plot shapes that contain the current pointer position. */
100
+ hoveredPlotShapes?: AxisPlotShape[];
99
101
  xAxis?: ChartXAxis | null;
100
102
  yAxis?: ChartYAxis;
101
103
  /** Formatting settings for tooltip header row (includes computed default). */
@@ -88,7 +88,7 @@ export const getDomainDataYBySeries = (series) => {
88
88
  switch (type) {
89
89
  case 'area':
90
90
  case 'bar-x': {
91
- acc.push(0, ...getDomainDataForStackedSeries(seriesList));
91
+ acc.push(...getDomainDataForStackedSeries(seriesList));
92
92
  break;
93
93
  }
94
94
  case 'waterfall': {
@@ -1,4 +1,4 @@
1
- import type { AxisPlotBand, AxisPlotLine } from '../../types';
1
+ import type { AxisPlotBand, AxisPlotLine, AxisPlotShape } from '../../types';
2
2
  import type { PreparedXAxis, PreparedYAxis } from '../axes/types';
3
3
  import type { ChartScale } from '../scales/types';
4
4
  export declare function getHoveredPlots(args: {
@@ -9,6 +9,7 @@ export declare function getHoveredPlots(args: {
9
9
  xScale?: ChartScale;
10
10
  yScale?: (ChartScale | undefined)[];
11
11
  }): {
12
- plotLines: AxisPlotLine[];
13
12
  plotBands: AxisPlotBand[];
13
+ plotLines: AxisPlotLine[];
14
+ plotShapes: AxisPlotShape[];
14
15
  };
@@ -1,5 +1,4 @@
1
1
  import { getBandsPosition, isBandScale } from './axis/common';
2
- const PLOT_LINE_HIT_THRESHOLD_PX = 4;
3
2
  function getHoveredAxisPlotBands(args) {
4
3
  const { pointerPx, plotBands, scale, axis } = args;
5
4
  const axisScale = scale;
@@ -22,16 +21,31 @@ function getHoveredAxisPlotLines(args) {
22
21
  }
23
22
  for (const line of plotLines) {
24
23
  const linePx = Number(scale(line.value));
25
- if (Math.abs(pointerPx - linePx) <= PLOT_LINE_HIT_THRESHOLD_PX + line.width / 2) {
24
+ if (Math.abs(pointerPx - linePx) <= line.hoverThreshold + line.width / 2) {
26
25
  result.push(line);
27
26
  }
28
27
  }
29
28
  return result;
30
29
  }
30
+ function getHoveredAxisPlotShapes(args) {
31
+ const { pointerX, pointerY, plotShapes } = args;
32
+ const result = [];
33
+ for (const shape of plotShapes) {
34
+ const left = shape.x + shape.hitbox.x;
35
+ const top = shape.y + shape.hitbox.y;
36
+ const inX = pointerX >= left && pointerX <= left + shape.hitbox.width;
37
+ const inY = pointerY >= top && pointerY <= top + shape.hitbox.height;
38
+ if (inX && inY) {
39
+ result.push(shape);
40
+ }
41
+ }
42
+ return result;
43
+ }
31
44
  export function getHoveredPlots(args) {
32
45
  const { pointerX, pointerY, xAxis, yAxis, xScale, yScale } = args;
33
- const plotLines = [];
34
46
  const plotBands = [];
47
+ const plotLines = [];
48
+ const plotShapes = [];
35
49
  if (xAxis && xScale) {
36
50
  plotBands.push(...getHoveredAxisPlotBands({
37
51
  pointerPx: pointerX,
@@ -44,6 +58,11 @@ export function getHoveredPlots(args) {
44
58
  plotLines: xAxis.plotLines,
45
59
  scale: xScale,
46
60
  }));
61
+ plotShapes.push(...getHoveredAxisPlotShapes({
62
+ pointerX,
63
+ pointerY,
64
+ plotShapes: xAxis.plotShapes,
65
+ }));
47
66
  }
48
67
  for (let i = 0; i < yAxis.length; i++) {
49
68
  const yAxisItem = yAxis[i];
@@ -62,6 +81,11 @@ export function getHoveredPlots(args) {
62
81
  plotLines: yAxisItem.plotLines,
63
82
  scale: yScaleItem,
64
83
  }));
84
+ plotShapes.push(...getHoveredAxisPlotShapes({
85
+ pointerX,
86
+ pointerY,
87
+ plotShapes: yAxisItem.plotShapes,
88
+ }));
65
89
  }
66
- return { plotLines, plotBands };
90
+ return { plotBands, plotLines, plotShapes };
67
91
  }
@@ -189,10 +189,20 @@ export function getTextSizeFn({ style }) {
189
189
  const defaultFontFamily = computedStyle.getPropertyValue('font-family');
190
190
  const defaultFontSize = computedStyle.getPropertyValue('font-size');
191
191
  const defaultFontWeight = computedStyle.getPropertyValue('font-weight');
192
+ const resolveCSSVar = (value) => {
193
+ const match = value.match(/^var\(\s*([\w-]+)/);
194
+ if (match) {
195
+ return computedStyle.getPropertyValue(match[1]).trim() || value;
196
+ }
197
+ return value;
198
+ };
192
199
  return async (str) => {
193
- var _a, _b;
194
200
  await document.fonts.ready;
195
- context.font = `${(_a = style === null || style === void 0 ? void 0 : style.fontWeight) !== null && _a !== void 0 ? _a : defaultFontWeight} ${(_b = style === null || style === void 0 ? void 0 : style.fontSize) !== null && _b !== void 0 ? _b : defaultFontSize} ${defaultFontFamily}`;
201
+ const fontWeight = (style === null || style === void 0 ? void 0 : style.fontWeight)
202
+ ? resolveCSSVar(String(style.fontWeight))
203
+ : defaultFontWeight;
204
+ const fontSize = (style === null || style === void 0 ? void 0 : style.fontSize) ? resolveCSSVar(style.fontSize) : defaultFontSize;
205
+ context.font = `${fontWeight} ${fontSize} ${defaultFontFamily}`;
196
206
  const textMetric = context.measureText(unescapeHtml(str));
197
207
  // we calculate hanging based on an approximate algorithm from chromium
198
208
  // https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/html/canvas/text_metrics.cc;l=32;drc=7cf6ac3dd6dca800fbc0d28e80a7732d4ea90340?q=member_hanging_&ss=chromium%2Fchromium%2Fsrc
@@ -1,4 +1,5 @@
1
- import type { ChartBrush, ChartData, ChartMargin, ChartZoom, DeepRequired } from '../types';
1
+ import type { TextRowData } from '../components/types';
2
+ import type { ChartBrush, ChartData, ChartMargin, ChartTitle, ChartZoom, DeepRequired } from '../types';
2
3
  export type PreparedZoom = DeepRequired<Omit<ChartZoom, 'enabled' | 'brush'>> & DeepRequired<{
3
4
  brush: ChartBrush;
4
5
  }>;
@@ -6,8 +7,10 @@ export type PreparedChart = {
6
7
  margin: ChartMargin;
7
8
  zoom: PreparedZoom | null;
8
9
  };
9
- export type PreparedTitle = ChartData['title'] & {
10
+ export type PreparedTitle = Omit<ChartTitle, 'margin'> & {
10
11
  height: number;
12
+ margin: number;
13
+ contentRows: TextRowData[];
11
14
  };
12
15
  export type PreparedTooltip = ChartData['tooltip'] & {
13
16
  enabled: boolean;
@@ -1,4 +1,4 @@
1
- import { group } from 'd3-array';
1
+ import { group, min } from 'd3-array';
2
2
  import isNil from 'lodash/isNil';
3
3
  import round from 'lodash/round';
4
4
  import { getDataCategoryValue, getLabelsSize, getTextSizeFn } from '../../../core/utils';
@@ -76,7 +76,7 @@ async function prepareDataLabels({ series, points, xMax, yAxisTop, isOutsideBoun
76
76
  return { svgLabels, htmlLabels };
77
77
  }
78
78
  export const prepareAreaData = async (args) => {
79
- var _a, _b, _c;
79
+ var _a, _b, _c, _d;
80
80
  const { series, xAxis, xScale, yAxis, yScale, split, isOutsideBounds, isRangeSlider } = args;
81
81
  const [_xMin, xRangeMax] = xScale.range();
82
82
  const xMax = xRangeMax;
@@ -144,12 +144,17 @@ export const prepareAreaData = async (args) => {
144
144
  continue;
145
145
  }
146
146
  const yAxisTop = ((_a = split.plots[plotIndex]) === null || _a === void 0 ? void 0 : _a.top) || 0;
147
- const yMin = (_b = getYValue({
148
- point: { y: 0 },
147
+ let base = 0;
148
+ if (seriesYAxis.type === 'logarithmic') {
149
+ const domainData = seriesYScale.domain();
150
+ base = (_b = min(domainData)) !== null && _b !== void 0 ? _b : 0;
151
+ }
152
+ const yMin = (_c = getYValue({
153
+ point: { y: base },
149
154
  points: s.data,
150
155
  yAxis: seriesYAxis,
151
156
  yScale: seriesYScale,
152
- })) !== null && _b !== void 0 ? _b : 0;
157
+ })) !== null && _c !== void 0 ? _c : 0;
153
158
  const seriesData = s.data.reduce((m, d) => {
154
159
  const key = String(xAxis.type === 'category'
155
160
  ? getDataCategoryValue({
@@ -309,7 +314,7 @@ export const prepareAreaData = async (args) => {
309
314
  for (let itemIndex = 0; itemIndex < seriesStackData.length; itemIndex++) {
310
315
  const item = seriesStackData[itemIndex];
311
316
  const currentYAxis = yAxis[item.series.yAxis];
312
- const itemYAxisTop = ((_c = split.plots[currentYAxis.plotIndex]) === null || _c === void 0 ? void 0 : _c.top) || 0;
317
+ const itemYAxisTop = ((_d = split.plots[currentYAxis.plotIndex]) === null || _d === void 0 ? void 0 : _d.top) || 0;
313
318
  if (item.series.dataLabels.enabled && !isRangeSlider) {
314
319
  const labelsData = await prepareDataLabels({
315
320
  series: item.series,
@@ -1,4 +1,4 @@
1
- import { ascending, descending, max, reverse, sort } from 'd3-array';
1
+ import { ascending, descending, max, min, reverse, sort } from 'd3-array';
2
2
  import get from 'lodash/get';
3
3
  import { getDataCategoryValue, getLabelsSize } from '../../../core/utils';
4
4
  import { getFormattedValue } from '../../../core/utils/format';
@@ -35,7 +35,7 @@ async function getLabelData(d, xMax) {
35
35
  };
36
36
  }
37
37
  export const prepareBarXData = async (args) => {
38
- var _a, _b, _c, _d, _e;
38
+ var _a, _b, _c, _d, _e, _f;
39
39
  const { series, seriesOptions, xAxis, xScale, yAxis, yScale, boundsHeight: plotHeight, split, isRangeSlider, } = args;
40
40
  const stackGap = seriesOptions['bar-x'].stackGap;
41
41
  const categories = (_a = xAxis === null || xAxis === void 0 ? void 0 : xAxis.categories) !== null && _a !== void 0 ? _a : [];
@@ -145,7 +145,15 @@ export const prepareBarXData = async (args) => {
145
145
  const x = xCenter - currentGroupWidth / 2 + (rectWidth + rectGap) * groupItemIndex;
146
146
  const yDataValue = ((_d = yValue.data.y) !== null && _d !== void 0 ? _d : 0);
147
147
  const y = seriesYScale(yDataValue);
148
- const base = seriesYScale(0);
148
+ let base = 0;
149
+ if (seriesYAxis.type === 'logarithmic') {
150
+ const domainData = seriesYScale.domain();
151
+ const yMinValue = (_e = min(domainData)) !== null && _e !== void 0 ? _e : 0;
152
+ base = seriesYScale(yMinValue);
153
+ }
154
+ else {
155
+ base = seriesYScale(0);
156
+ }
149
157
  const isLastStackItem = yValueIndex === sortedData.length - 1;
150
158
  const height = Math.abs(base - y);
151
159
  let shapeHeight = height - (stackItems.length ? stackGap : 0);
@@ -198,7 +206,7 @@ export const prepareBarXData = async (args) => {
198
206
  barData.x >= xMax ||
199
207
  barData.y + barData.height <= 0 ||
200
208
  barData.y >= plotHeight;
201
- const isZeroValue = ((_e = barData.data.y) !== null && _e !== void 0 ? _e : 0) === 0;
209
+ const isZeroValue = ((_f = barData.data.y) !== null && _f !== void 0 ? _f : 0) === 0;
202
210
  if (barData.series.dataLabels.enabled &&
203
211
  !isRangeSlider &&
204
212
  (!isBarOutsideBounds || isZeroValue)) {
@@ -1,5 +1,5 @@
1
1
  import type { Dispatch } from 'd3-dispatch';
2
- import type { AxisPlotBand, AxisPlotLine, PointPosition, TooltipDataChunk } from '../../types';
2
+ import type { AxisPlotBand, AxisPlotLine, AxisPlotShape, PointPosition, TooltipDataChunk } from '../../types';
3
3
  import type { PreparedTooltip } from '../types';
4
4
  import type { PreparedXAxis, PreparedYAxis } from '../useAxis/types';
5
5
  type Args = {
@@ -10,8 +10,9 @@ type Args = {
10
10
  };
11
11
  export declare const useTooltip: ({ dispatcher, tooltip, xAxis, yAxis }: Args) => {
12
12
  hovered: TooltipDataChunk[] | undefined;
13
- hoveredPlotLines: AxisPlotLine[] | undefined;
14
13
  hoveredPlotBands: AxisPlotBand[] | undefined;
14
+ hoveredPlotLines: AxisPlotLine[] | undefined;
15
+ hoveredPlotShapes: AxisPlotShape[] | undefined;
15
16
  pointerPosition: PointPosition | undefined;
16
17
  };
17
18
  export {};
@@ -2,7 +2,7 @@ import React from 'react';
2
2
  import isEqual from 'lodash/isEqual';
3
3
  import { getSortedHovered } from '../../components/Tooltip/DefaultTooltipContent/utils';
4
4
  export const useTooltip = ({ dispatcher, tooltip, xAxis, yAxis }) => {
5
- const [{ hovered, hoveredPlotLines, hoveredPlotBands, pointerPosition }, setTooltipState] = React.useState({});
5
+ const [{ hovered, hoveredPlotBands, hoveredPlotLines, hoveredPlotShapes, pointerPosition }, setTooltipState,] = React.useState({});
6
6
  const prevHovered = React.useRef(hovered);
7
7
  React.useEffect(() => {
8
8
  if (tooltip === null || tooltip === void 0 ? void 0 : tooltip.enabled) {
@@ -17,8 +17,9 @@ export const useTooltip = ({ dispatcher, tooltip, xAxis, yAxis }) => {
17
17
  const isHoveredChanged = !isEqual(prevHovered.current, sortedHovered);
18
18
  const newTooltipState = {
19
19
  hovered: isHoveredChanged ? sortedHovered : prevHovered.current,
20
- hoveredPlotLines: nextHoveredPlots === null || nextHoveredPlots === void 0 ? void 0 : nextHoveredPlots.lines,
21
20
  hoveredPlotBands: nextHoveredPlots === null || nextHoveredPlots === void 0 ? void 0 : nextHoveredPlots.bands,
21
+ hoveredPlotLines: nextHoveredPlots === null || nextHoveredPlots === void 0 ? void 0 : nextHoveredPlots.lines,
22
+ hoveredPlotShapes: nextHoveredPlots === null || nextHoveredPlots === void 0 ? void 0 : nextHoveredPlots.shapes,
22
23
  pointerPosition: nextPointerPosition,
23
24
  };
24
25
  if (isHoveredChanged) {
@@ -35,8 +36,9 @@ export const useTooltip = ({ dispatcher, tooltip, xAxis, yAxis }) => {
35
36
  }, [dispatcher, tooltip, xAxis, yAxis]);
36
37
  return {
37
38
  hovered,
38
- hoveredPlotLines,
39
39
  hoveredPlotBands,
40
+ hoveredPlotLines,
41
+ hoveredPlotShapes,
40
42
  pointerPosition,
41
43
  };
42
44
  };