@gravity-ui/charts 1.35.1 → 1.36.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 (101) hide show
  1. package/dist/cjs/components/AxisX/AxisX.js +18 -8
  2. package/dist/cjs/components/AxisX/prepare-axis-data.js +12 -0
  3. package/dist/cjs/components/AxisX/styles.css +7 -2
  4. package/dist/cjs/components/AxisX/types.d.ts +5 -0
  5. package/dist/cjs/components/AxisY/AxisY.js +16 -8
  6. package/dist/cjs/components/AxisY/prepare-axis-data.js +14 -0
  7. package/dist/cjs/components/AxisY/styles.css +7 -2
  8. package/dist/cjs/components/AxisY/types.d.ts +5 -0
  9. package/dist/cjs/components/ChartInner/index.js +2 -0
  10. package/dist/cjs/components/ChartInner/styles.css +1 -0
  11. package/dist/cjs/components/ChartInner/useChartInnerHandlers.d.ts +3 -0
  12. package/dist/cjs/components/ChartInner/useChartInnerHandlers.js +26 -3
  13. package/dist/cjs/components/ChartInner/useChartInnerProps.d.ts +1 -0
  14. package/dist/cjs/components/Tooltip/ChartTooltipContent.d.ts +3 -1
  15. package/dist/cjs/components/Tooltip/ChartTooltipContent.js +2 -2
  16. package/dist/cjs/components/Tooltip/DefaultTooltipContent/Row.js +1 -1
  17. package/dist/cjs/components/Tooltip/index.js +5 -2
  18. package/dist/cjs/components/Tooltip/styles.css +7 -0
  19. package/dist/cjs/constants/datetime.d.ts +8 -0
  20. package/dist/cjs/constants/datetime.js +8 -0
  21. package/dist/cjs/constants/defaults/axis.d.ts +4 -0
  22. package/dist/cjs/constants/defaults/axis.js +4 -0
  23. package/dist/cjs/constants/index.d.ts +1 -1
  24. package/dist/cjs/constants/index.js +1 -1
  25. package/dist/cjs/hooks/useAxis/types.d.ts +8 -1
  26. package/dist/cjs/hooks/useAxis/utils.d.ts +1 -1
  27. package/dist/cjs/hooks/useAxis/x-axis.js +9 -3
  28. package/dist/cjs/hooks/useAxis/y-axis.js +7 -1
  29. package/dist/cjs/hooks/useAxisScales/x-scale.d.ts +1 -1
  30. package/dist/cjs/hooks/useAxisScales/y-scale.d.ts +1 -1
  31. package/dist/cjs/hooks/useNormalizedOriginalData/index.d.ts +1 -0
  32. package/dist/cjs/hooks/useTooltip/index.d.ts +3 -1
  33. package/dist/cjs/hooks/useTooltip/index.js +7 -3
  34. package/dist/cjs/types/chart/axis.d.ts +17 -3
  35. package/dist/cjs/types/chart/base.d.ts +1 -1
  36. package/dist/cjs/types/chart/tooltip.d.ts +5 -1
  37. package/dist/cjs/utils/chart/axis/common.d.ts +1 -3
  38. package/dist/cjs/utils/chart/axis/common.js +2 -10
  39. package/dist/cjs/utils/chart/axis/x-axis.js +3 -2
  40. package/dist/cjs/utils/chart/format.js +17 -2
  41. package/dist/cjs/utils/chart/get-hovered-plots.d.ts +14 -0
  42. package/dist/cjs/utils/chart/get-hovered-plots.js +67 -0
  43. package/dist/cjs/utils/chart/ticks/datetime.d.ts +8 -0
  44. package/dist/cjs/utils/chart/ticks/datetime.js +50 -0
  45. package/dist/cjs/utils/chart/ticks/index.d.ts +6 -0
  46. package/dist/cjs/utils/chart/ticks/index.js +17 -0
  47. package/dist/cjs/utils/chart/time.d.ts +1 -0
  48. package/dist/cjs/utils/chart/time.js +9 -0
  49. package/dist/esm/components/AxisX/AxisX.js +18 -8
  50. package/dist/esm/components/AxisX/prepare-axis-data.js +12 -0
  51. package/dist/esm/components/AxisX/styles.css +7 -2
  52. package/dist/esm/components/AxisX/types.d.ts +5 -0
  53. package/dist/esm/components/AxisY/AxisY.js +16 -8
  54. package/dist/esm/components/AxisY/prepare-axis-data.js +14 -0
  55. package/dist/esm/components/AxisY/styles.css +7 -2
  56. package/dist/esm/components/AxisY/types.d.ts +5 -0
  57. package/dist/esm/components/ChartInner/index.js +2 -0
  58. package/dist/esm/components/ChartInner/styles.css +1 -0
  59. package/dist/esm/components/ChartInner/useChartInnerHandlers.d.ts +3 -0
  60. package/dist/esm/components/ChartInner/useChartInnerHandlers.js +26 -3
  61. package/dist/esm/components/ChartInner/useChartInnerProps.d.ts +1 -0
  62. package/dist/esm/components/Tooltip/ChartTooltipContent.d.ts +3 -1
  63. package/dist/esm/components/Tooltip/ChartTooltipContent.js +2 -2
  64. package/dist/esm/components/Tooltip/DefaultTooltipContent/Row.js +1 -1
  65. package/dist/esm/components/Tooltip/index.js +5 -2
  66. package/dist/esm/components/Tooltip/styles.css +7 -0
  67. package/dist/esm/constants/datetime.d.ts +8 -0
  68. package/dist/esm/constants/datetime.js +8 -0
  69. package/dist/esm/constants/defaults/axis.d.ts +4 -0
  70. package/dist/esm/constants/defaults/axis.js +4 -0
  71. package/dist/esm/constants/index.d.ts +1 -1
  72. package/dist/esm/constants/index.js +1 -1
  73. package/dist/esm/hooks/useAxis/types.d.ts +8 -1
  74. package/dist/esm/hooks/useAxis/utils.d.ts +1 -1
  75. package/dist/esm/hooks/useAxis/x-axis.js +9 -3
  76. package/dist/esm/hooks/useAxis/y-axis.js +7 -1
  77. package/dist/esm/hooks/useAxisScales/x-scale.d.ts +1 -1
  78. package/dist/esm/hooks/useAxisScales/y-scale.d.ts +1 -1
  79. package/dist/esm/hooks/useNormalizedOriginalData/index.d.ts +1 -0
  80. package/dist/esm/hooks/useTooltip/index.d.ts +3 -1
  81. package/dist/esm/hooks/useTooltip/index.js +7 -3
  82. package/dist/esm/types/chart/axis.d.ts +17 -3
  83. package/dist/esm/types/chart/base.d.ts +1 -1
  84. package/dist/esm/types/chart/tooltip.d.ts +5 -1
  85. package/dist/esm/utils/chart/axis/common.d.ts +1 -3
  86. package/dist/esm/utils/chart/axis/common.js +2 -10
  87. package/dist/esm/utils/chart/axis/x-axis.js +3 -2
  88. package/dist/esm/utils/chart/format.js +17 -2
  89. package/dist/esm/utils/chart/get-hovered-plots.d.ts +14 -0
  90. package/dist/esm/utils/chart/get-hovered-plots.js +67 -0
  91. package/dist/esm/utils/chart/ticks/datetime.d.ts +8 -0
  92. package/dist/esm/utils/chart/ticks/datetime.js +50 -0
  93. package/dist/esm/utils/chart/ticks/index.d.ts +6 -0
  94. package/dist/esm/utils/chart/ticks/index.js +17 -0
  95. package/dist/esm/utils/chart/time.d.ts +1 -0
  96. package/dist/esm/utils/chart/time.js +9 -0
  97. package/package.json +1 -1
  98. package/dist/cjs/constants/date.d.ts +0 -1
  99. package/dist/cjs/constants/date.js +0 -1
  100. package/dist/esm/constants/date.d.ts +0 -1
  101. package/dist/esm/constants/date.js +0 -1
@@ -1,6 +1,6 @@
1
1
  import get from 'lodash/get';
2
2
  import { getTickValues } from '../../components/AxisY/utils';
3
- import { DASH_STYLE, DEFAULT_AXIS_LABEL_FONT_SIZE, DEFAULT_AXIS_TYPE, SERIES_TYPE, axisCrosshairDefaults, axisLabelsDefaults, yAxisTitleDefaults, } from '../../constants';
3
+ import { DASH_STYLE, DEFAULT_AXIS_LABEL_FONT_SIZE, DEFAULT_AXIS_TYPE, SERIES_TYPE, axisCrosshairDefaults, axisLabelsDefaults, axisTickMarksDefaults, yAxisTitleDefaults, } from '../../constants';
4
4
  import { calculateNumericProperty, formatAxisTickLabel, getDefaultDateFormat, getDefaultMinYAxisValue, getHorizontalHtmlTextHeight, getHorizontalSvgTextHeight, getLabelsSize, getMinSpaceBetween, getTextSizeFn, isAxisRelatedSeries, shouldSyncAxisWithPrimary, wrapText, } from '../../utils';
5
5
  import { createYScale } from '../useAxisScales';
6
6
  import { prepareAxisPlotLabel } from './utils';
@@ -160,6 +160,10 @@ export const getPreparedYAxis = ({ height, boundsHeight, width, seriesData, yAxi
160
160
  })
161
161
  : (_q = axisItem.ticks) === null || _q === void 0 ? void 0 : _q.pixelInterval,
162
162
  },
163
+ tickMarks: {
164
+ enabled: get(axisItem, 'tickMarks.enabled', axisTickMarksDefaults.enabled),
165
+ length: get(axisItem, 'tickMarks.length', axisTickMarksDefaults.length),
166
+ },
163
167
  position: axisPosition,
164
168
  plotIndex: get(axisItem, 'plotIndex', 0),
165
169
  plotLines: get(axisItem, 'plotLines', []).map((d) => ({
@@ -169,6 +173,7 @@ export const getPreparedYAxis = ({ height, boundsHeight, width, seriesData, yAxi
169
173
  dashStyle: get(d, 'dashStyle', DASH_STYLE.Solid),
170
174
  opacity: get(d, 'opacity', 1),
171
175
  layerPlacement: get(d, 'layerPlacement', 'before'),
176
+ custom: d.custom,
172
177
  label: prepareAxisPlotLabel(d),
173
178
  })),
174
179
  plotBands: get(axisItem, 'plotBands', []).map((d) => ({
@@ -177,6 +182,7 @@ export const getPreparedYAxis = ({ height, boundsHeight, width, seriesData, yAxi
177
182
  from: get(d, 'from', 0),
178
183
  to: get(d, 'to', 0),
179
184
  layerPlacement: get(d, 'layerPlacement', 'before'),
185
+ custom: d.custom,
180
186
  label: prepareAxisPlotLabel(d),
181
187
  })),
182
188
  crosshair: {
@@ -6,4 +6,4 @@ export declare function createXScale(args: {
6
6
  series: (PreparedSeries | ChartSeries)[];
7
7
  rangeSliderState?: RangeSliderState;
8
8
  zoomStateX?: [number, number];
9
- }): import("d3-scale").ScaleBand<string> | import("d3-scale").ScaleLinear<number, number, never> | import("d3-scale").ScaleTime<number, number, never> | undefined;
9
+ }): import("d3-scale").ScaleLinear<number, number, never> | import("d3-scale").ScaleBand<string> | import("d3-scale").ScaleTime<number, number, never> | undefined;
@@ -7,4 +7,4 @@ export declare function createYScale(args: {
7
7
  primaryAxis?: PreparedAxis;
8
8
  primaryTicksCount?: number;
9
9
  zoomStateY?: [number, number];
10
- }): import("d3-scale").ScaleBand<string> | import("d3-scale").ScaleLinear<number, number, never> | import("d3-scale").ScaleTime<number, number, never> | undefined;
10
+ }): import("d3-scale").ScaleLinear<number, number, never> | import("d3-scale").ScaleBand<string> | import("d3-scale").ScaleTime<number, number, never> | undefined;
@@ -27,6 +27,7 @@ export declare function useNormalizedOriginalData(props: UseOriginalDataProps):
27
27
  maxPadding?: number;
28
28
  plotLines?: import("../../types").AxisPlotLine[];
29
29
  plotBands?: import("../../types").AxisPlotBand[];
30
+ tickMarks?: import("../../types").ChartAxisTickMarks;
30
31
  visible?: boolean;
31
32
  order?: "sortAsc" | "sortDesc" | "reverse";
32
33
  startOnTick?: boolean;
@@ -1,5 +1,5 @@
1
1
  import type { Dispatch } from 'd3';
2
- import type { PointPosition, TooltipDataChunk } from '../../types';
2
+ import type { AxisPlotBand, AxisPlotLine, PointPosition, TooltipDataChunk } from '../../types';
3
3
  import type { PreparedTooltip } from '../useChartOptions/types';
4
4
  type Args = {
5
5
  dispatcher: Dispatch<object>;
@@ -7,6 +7,8 @@ type Args = {
7
7
  };
8
8
  export declare const useTooltip: ({ dispatcher, tooltip }: Args) => {
9
9
  hovered: TooltipDataChunk[] | undefined;
10
+ hoveredPlotLines: AxisPlotLine[] | undefined;
11
+ hoveredPlotBands: AxisPlotBand[] | undefined;
10
12
  pointerPosition: PointPosition | undefined;
11
13
  };
12
14
  export {};
@@ -1,16 +1,18 @@
1
1
  import React from 'react';
2
2
  import isEqual from 'lodash/isEqual';
3
3
  export const useTooltip = ({ dispatcher, tooltip }) => {
4
- const [{ hovered, pointerPosition }, setTooltipState] = React.useState({});
4
+ const [{ hovered, hoveredPlotLines, hoveredPlotBands, pointerPosition }, setTooltipState] = React.useState({});
5
5
  const prevHovered = React.useRef(hovered);
6
6
  React.useEffect(() => {
7
7
  if (tooltip === null || tooltip === void 0 ? void 0 : tooltip.enabled) {
8
- dispatcher.on('hover-shape.tooltip', (nextHovered, nextPointerPosition) => {
8
+ dispatcher.on('hover-shape.tooltip', (nextHovered, nextPointerPosition, nextHoveredPlots) => {
9
9
  const filteredNextHovered = nextHovered === null || nextHovered === void 0 ? void 0 : nextHovered.filter((item) => 'y' in item.data ? item.data.y !== null : true);
10
10
  const isHoveredChanged = !isEqual(prevHovered.current, filteredNextHovered);
11
11
  const newTooltipState = {
12
- pointerPosition: nextPointerPosition,
13
12
  hovered: isHoveredChanged ? filteredNextHovered : prevHovered.current,
13
+ hoveredPlotLines: nextHoveredPlots === null || nextHoveredPlots === void 0 ? void 0 : nextHoveredPlots.lines,
14
+ hoveredPlotBands: nextHoveredPlots === null || nextHoveredPlots === void 0 ? void 0 : nextHoveredPlots.bands,
15
+ pointerPosition: nextPointerPosition,
14
16
  };
15
17
  if (isHoveredChanged) {
16
18
  prevHovered.current = filteredNextHovered;
@@ -26,6 +28,8 @@ export const useTooltip = ({ dispatcher, tooltip }) => {
26
28
  }, [dispatcher, tooltip]);
27
29
  return {
28
30
  hovered,
31
+ hoveredPlotLines,
32
+ hoveredPlotBands,
29
33
  pointerPosition,
30
34
  };
31
35
  };
@@ -1,6 +1,7 @@
1
1
  import type { DurationInput } from '@gravity-ui/date-utils';
2
2
  import type { AXIS_TYPE, DashStyle } from '../../constants';
3
3
  import type { FormatNumberOptions } from '../formatter';
4
+ import type { MeaningfulAny } from '../misc';
4
5
  import type { BaseTextStyle } from './base';
5
6
  import type { ChartBrush } from './brush';
6
7
  export type ChartAxisType = (typeof AXIS_TYPE)[keyof typeof AXIS_TYPE];
@@ -94,6 +95,16 @@ export interface ChartAxisTitle {
94
95
  */
95
96
  maxRowCount?: number;
96
97
  }
98
+ export interface ChartAxisTickMarks {
99
+ /** Enable or disable the tick marks on the axis.
100
+ * @default false
101
+ */
102
+ enabled?: boolean;
103
+ /** The length of the tick marks in pixels (perpendicular to the axis).
104
+ * @default 6
105
+ */
106
+ length?: number;
107
+ }
97
108
  export interface ChartAxis {
98
109
  categories?: string[];
99
110
  /** Configure a crosshair that follows either the mouse pointer or the hovered point. */
@@ -161,6 +172,8 @@ export interface ChartAxis {
161
172
  plotLines?: AxisPlotLine[];
162
173
  /** An array of colored bands stretching across the plot area marking an interval on the axis. */
163
174
  plotBands?: AxisPlotBand[];
175
+ /** Small perpendicular marks on the axis line at each tick position. */
176
+ tickMarks?: ChartAxisTickMarks;
164
177
  /** Whether axis, including axis title, line, ticks and labels, should be visible. */
165
178
  visible?: boolean;
166
179
  /** Setting the order of the axis values. It is not applied by default.
@@ -226,6 +239,8 @@ export interface AxisPlot {
226
239
  * It is assigned as a data-qa attribute to an element. */
227
240
  qa?: string;
228
241
  };
242
+ /** Custom data associated with the plot line/band, accessible in tooltip renderer args. */
243
+ custom?: MeaningfulAny;
229
244
  }
230
245
  export interface AxisPlotLine extends AxisPlot {
231
246
  /** The position of the line in axis units. */
@@ -259,7 +274,7 @@ export interface AxisPlotBand extends AxisPlot {
259
274
  */
260
275
  to: number | string | null;
261
276
  }
262
- export interface AxisCrosshair extends Omit<AxisPlotLine, 'value' | 'label'> {
277
+ export interface AxisCrosshair extends Omit<AxisPlotLine, 'value' | 'label' | 'custom'> {
263
278
  /** Whether the crosshair should snap to the point or follow the pointer independent of points.
264
279
  * @default true
265
280
  */
@@ -269,7 +284,7 @@ export interface AxisCrosshair extends Omit<AxisPlotLine, 'value' | 'label'> {
269
284
  */
270
285
  enabled?: boolean;
271
286
  }
272
- interface ChartYAxisTitle extends ChartAxisTitle {
287
+ export interface ChartYAxisTitle extends ChartAxisTitle {
273
288
  /** Rotation of the title in degrees.
274
289
  * Currently, the available values are only for rotation in multiples of 90 degrees.
275
290
  *
@@ -297,4 +312,3 @@ export interface ChartYAxis extends ChartAxis {
297
312
  plotIndex?: number;
298
313
  title?: ChartYAxisTitle;
299
314
  }
300
- export {};
@@ -84,7 +84,7 @@ export interface BaseSeriesLegend extends ChartLegendItem {
84
84
  }
85
85
  export interface BaseTextStyle {
86
86
  fontSize: string;
87
- fontWeight?: string;
87
+ fontWeight?: string | number;
88
88
  fontColor?: string;
89
89
  }
90
90
  export {};
@@ -1,7 +1,7 @@
1
1
  import type { TOOLTIP_TOTALS_BUILT_IN_AGGREGATION } from '../../constants';
2
2
  import type { MeaningfulAny } from '../misc';
3
3
  import type { AreaSeries, AreaSeriesData } from './area';
4
- import type { ChartXAxis, ChartYAxis } from './axis';
4
+ import type { AxisPlotBand, AxisPlotLine, ChartXAxis, ChartYAxis } from './axis';
5
5
  import type { BarXSeries, BarXSeriesData } from './bar-x';
6
6
  import type { BarYSeries, BarYSeriesData } from './bar-y';
7
7
  import type { CustomFormat, ValueFormat } from './base';
@@ -91,6 +91,10 @@ export type TooltipDataChunk<T = MeaningfulAny> = (TooltipDataChunkBarX<T> | Too
91
91
  };
92
92
  export interface ChartTooltipRendererArgs<T = MeaningfulAny> {
93
93
  hovered: TooltipDataChunk<T>[];
94
+ /** Plot lines that intersect with the current pointer position. */
95
+ hoveredPlotLines?: AxisPlotLine[];
96
+ /** Plot bands that contain the current pointer position. */
97
+ hoveredPlotBands?: AxisPlotBand[];
94
98
  xAxis?: ChartXAxis | null;
95
99
  yAxis?: ChartYAxis;
96
100
  }
@@ -2,14 +2,12 @@ import type { AxisDomain, AxisScale, ScaleBand, ScaleTime } from 'd3';
2
2
  import type { ChartScale, PreparedAxis, PreparedAxisPlotBand, PreparedSplit } from '../../../hooks';
3
3
  import type { ChartAxis } from '../../../types';
4
4
  import type { AxisDirection } from '../types';
5
- type Ticks = number[] | string[] | Date[];
6
5
  export declare function getTicksCountByPixelInterval({ axis, axisWidth, }: {
7
6
  axis: PreparedAxis;
8
7
  axisWidth: number;
9
8
  }): number | undefined;
10
9
  export declare function isBandScale(scale?: ChartScale | AxisScale<AxisDomain>): scale is ScaleBand<string>;
11
10
  export declare function isTimeScale(scale?: ChartScale | AxisScale<AxisDomain>): scale is ScaleTime<number, number>;
12
- export declare function getScaleTicks(scale: ChartScale | AxisScale<AxisDomain>, ticksCount?: number): Ticks;
13
11
  export declare function getXAxisOffset(): 0 | 0.5;
14
12
  export declare function getXTickPosition({ scale, offset }: {
15
13
  scale: AxisScale<AxisDomain>;
@@ -19,7 +17,7 @@ export declare function getAxisItems({ scale, count, maxCount, }: {
19
17
  scale: ChartScale | AxisScale<AxisDomain>;
20
18
  count?: number;
21
19
  maxCount?: number;
22
- }): Ticks;
20
+ }): number[] | string[] | Date[];
23
21
  export declare function getAxisHeight(args: {
24
22
  split: PreparedSplit;
25
23
  boundsHeight: number;
@@ -1,5 +1,6 @@
1
1
  import { ascending, descending, reverse, sort } from 'd3';
2
2
  import clamp from 'lodash/clamp';
3
+ import { getScaleTicks } from '../ticks';
3
4
  export function getTicksCountByPixelInterval({ axis, axisWidth, }) {
4
5
  let ticksCount;
5
6
  if (axis.ticks.pixelInterval) {
@@ -16,15 +17,6 @@ export function isTimeScale(scale) {
16
17
  }
17
18
  return scale.domain()[0] instanceof Date;
18
19
  }
19
- export function getScaleTicks(scale, ticksCount) {
20
- if ('ticks' in scale && typeof scale.ticks === 'function') {
21
- return scale.ticks(ticksCount);
22
- }
23
- if (isBandScale(scale)) {
24
- return scale.domain();
25
- }
26
- return [];
27
- }
28
20
  export function getXAxisOffset() {
29
21
  return typeof window !== 'undefined' && window.devicePixelRatio > 1 ? 0 : 0.5;
30
22
  }
@@ -42,7 +34,7 @@ export function getXTickPosition({ scale, offset }) {
42
34
  return isBandScale(scale) ? center(scale, offset) : number(scale);
43
35
  }
44
36
  export function getAxisItems({ scale, count, maxCount, }) {
45
- let values = getScaleTicks(scale, count);
37
+ let values = getScaleTicks({ scale, ticksCount: count });
46
38
  if (maxCount && values.length > maxCount) {
47
39
  const step = Math.ceil(values.length / maxCount);
48
40
  values = values.filter((_, i) => i % step === 0);
@@ -1,5 +1,6 @@
1
1
  import { getMinSpaceBetween } from '../array';
2
2
  import { isSeriesWithNumericalXValues } from '../series-type-guards';
3
+ import { getScaleTicks } from '../ticks';
3
4
  import { getTicksCountByPixelInterval, isBandScale, thinOut } from './common';
4
5
  const DEFAULT_TICKS_COUNT = 10;
5
6
  function getTicksCount(args) {
@@ -29,7 +30,7 @@ export function getXAxisTickValues({ axis, labelLineHeight, scale, series, }) {
29
30
  return [];
30
31
  }
31
32
  const scaleTicksCount = getTicksCount({ axis, axisWidth, series });
32
- const scaleTicks = scale.ticks(scaleTicksCount);
33
+ const scaleTicks = getScaleTicks({ scale, ticksCount: scaleTicksCount });
33
34
  const originalTickValues = scaleTicks.map((t) => ({
34
35
  x: scale(t),
35
36
  value: t,
@@ -43,7 +44,7 @@ export function getXAxisTickValues({ axis, labelLineHeight, scale, series, }) {
43
44
  let ticksCount = result.length - 1;
44
45
  while (availableSpaceForLabel < labelLineHeight && result.length > 1) {
45
46
  ticksCount = ticksCount ? ticksCount - 1 : result.length - 1;
46
- const newScaleTicks = scale.ticks(ticksCount);
47
+ const newScaleTicks = getScaleTicks({ scale, ticksCount });
47
48
  result = newScaleTicks.map((t) => ({
48
49
  x: scale(t),
49
50
  value: t,
@@ -2,7 +2,7 @@ import { dateTimeUtc } from '@gravity-ui/date-utils';
2
2
  import capitalize from 'lodash/capitalize';
3
3
  import { DEFAULT_DATE_FORMAT } from '../../constants';
4
4
  import { formatNumber, getDefaultUnit } from '../../libs';
5
- import { getDefaultDateFormat } from './time';
5
+ import { DATETIME_LABEL_FORMATS, TIME_UNITS, getDefaultDateFormat, getDefaultTimeOnlyFormat, } from './time';
6
6
  const LETTER_MOUNTH_AT_START_FORMAT_REGEXP = /^M{3,}/;
7
7
  function getFormattedDate(args) {
8
8
  const { value, format = DEFAULT_DATE_FORMAT } = args;
@@ -43,7 +43,22 @@ export function formatAxisTickLabel(args) {
43
43
  }
44
44
  case 'datetime': {
45
45
  const date = value;
46
- const format = axis.labels.dateFormat || getDefaultDateFormat(step);
46
+ let format;
47
+ if (axis.labels.dateFormat) {
48
+ format = axis.labels.dateFormat;
49
+ }
50
+ else if (step !== undefined && step < TIME_UNITS.day) {
51
+ const d = dateTimeUtc({ input: date });
52
+ const isMidnight = d.isValid() &&
53
+ d.hour() === 0 &&
54
+ d.minute() === 0 &&
55
+ d.second() === 0 &&
56
+ d.millisecond() === 0;
57
+ format = isMidnight ? DATETIME_LABEL_FORMATS.day : getDefaultTimeOnlyFormat(step);
58
+ }
59
+ else {
60
+ format = getDefaultDateFormat(step);
61
+ }
47
62
  return getFormattedDate({ value: date, format });
48
63
  }
49
64
  case 'linear':
@@ -0,0 +1,14 @@
1
+ import type { PreparedXAxis, PreparedYAxis } from '../../hooks';
2
+ import type { ChartScale } from '../../hooks/useAxisScales/types';
3
+ import type { AxisPlotBand, AxisPlotLine } from '../../types';
4
+ export declare function getHoveredPlots(args: {
5
+ pointerX: number;
6
+ pointerY: number;
7
+ xAxis?: PreparedXAxis | null;
8
+ yAxis: PreparedYAxis[];
9
+ xScale?: ChartScale;
10
+ yScale?: (ChartScale | undefined)[];
11
+ }): {
12
+ plotLines: AxisPlotLine[];
13
+ plotBands: AxisPlotBand[];
14
+ };
@@ -0,0 +1,67 @@
1
+ import { getBandsPosition, isBandScale } from './axis/common';
2
+ const PLOT_LINE_HIT_THRESHOLD_PX = 4;
3
+ function getHoveredAxisPlotBands(args) {
4
+ const { pointerPx, plotBands, scale, axis } = args;
5
+ const axisScale = scale;
6
+ const result = [];
7
+ for (const band of plotBands) {
8
+ const { from, to } = getBandsPosition({ band, axisScale, axis });
9
+ const startPx = Math.min(from, to);
10
+ const endPx = Math.max(from, to);
11
+ if (pointerPx >= startPx && pointerPx <= endPx) {
12
+ result.push(band);
13
+ }
14
+ }
15
+ return result;
16
+ }
17
+ function getHoveredAxisPlotLines(args) {
18
+ const { pointerPx, plotLines, scale } = args;
19
+ const result = [];
20
+ if (isBandScale(scale)) {
21
+ return result;
22
+ }
23
+ for (const line of plotLines) {
24
+ const linePx = Number(scale(line.value));
25
+ if (Math.abs(pointerPx - linePx) <= PLOT_LINE_HIT_THRESHOLD_PX + line.width / 2) {
26
+ result.push(line);
27
+ }
28
+ }
29
+ return result;
30
+ }
31
+ export function getHoveredPlots(args) {
32
+ const { pointerX, pointerY, xAxis, yAxis, xScale, yScale } = args;
33
+ const plotLines = [];
34
+ const plotBands = [];
35
+ if (xAxis && xScale) {
36
+ plotBands.push(...getHoveredAxisPlotBands({
37
+ pointerPx: pointerX,
38
+ plotBands: xAxis.plotBands,
39
+ scale: xScale,
40
+ axis: 'x',
41
+ }));
42
+ plotLines.push(...getHoveredAxisPlotLines({
43
+ pointerPx: pointerX,
44
+ plotLines: xAxis.plotLines,
45
+ scale: xScale,
46
+ }));
47
+ }
48
+ for (let i = 0; i < yAxis.length; i++) {
49
+ const yAxisItem = yAxis[i];
50
+ const yScaleItem = yScale === null || yScale === void 0 ? void 0 : yScale[i];
51
+ if (!yAxisItem || !yScaleItem) {
52
+ continue;
53
+ }
54
+ plotBands.push(...getHoveredAxisPlotBands({
55
+ pointerPx: pointerY,
56
+ plotBands: yAxisItem.plotBands,
57
+ scale: yScaleItem,
58
+ axis: 'y',
59
+ }));
60
+ plotLines.push(...getHoveredAxisPlotLines({
61
+ pointerPx: pointerY,
62
+ plotLines: yAxisItem.plotLines,
63
+ scale: yScaleItem,
64
+ }));
65
+ }
66
+ return { plotLines, plotBands };
67
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Generates time ticks for the given interval.
3
+ *
4
+ * Based on d3's utcTicks algorithm (https://github.com/d3/d3-time/blob/main/src/ticks.js) with one modification:
5
+ * weeks are considered to start on Monday (ISO 8601 standard)
6
+ * instead of Sunday (d3 default).
7
+ */
8
+ export declare function getDateTimeTicks(start: Date, stop: Date, count?: number): Date[];
@@ -0,0 +1,50 @@
1
+ import { bisector, tickStep, utcDay, utcHour, utcMillisecond, utcMinute, utcMonth, utcSecond, utcMonday as utcWeek, utcYear, } from 'd3';
2
+ import { DAY, HOUR, MINUTE, MONTH, SECOND, WEEK, YEAR } from '../../../constants';
3
+ const tickIntervals = [
4
+ [utcSecond, 1, SECOND],
5
+ [utcSecond, 5, 5 * SECOND],
6
+ [utcSecond, 15, 15 * SECOND],
7
+ [utcSecond, 30, 30 * SECOND],
8
+ [utcMinute, 1, MINUTE],
9
+ [utcMinute, 5, 5 * MINUTE],
10
+ [utcMinute, 15, 15 * MINUTE],
11
+ [utcMinute, 30, 30 * MINUTE],
12
+ [utcHour, 1, HOUR],
13
+ [utcHour, 3, 3 * HOUR],
14
+ [utcHour, 6, 6 * HOUR],
15
+ [utcHour, 12, 12 * HOUR],
16
+ [utcDay, 1, DAY],
17
+ [utcDay, 2, 2 * DAY],
18
+ [utcWeek, 1, WEEK],
19
+ [utcMonth, 1, MONTH],
20
+ [utcMonth, 3, 3 * MONTH],
21
+ [utcYear, 1, YEAR],
22
+ ];
23
+ function getDateTimeTickInterval(start, stop, count) {
24
+ const target = Math.abs(stop - start) / count;
25
+ const i = bisector(([, , step]) => step).right(tickIntervals, target);
26
+ if (i === tickIntervals.length) {
27
+ return utcYear.every(tickStep(start / YEAR, stop / YEAR, count));
28
+ }
29
+ if (i === 0) {
30
+ return utcMillisecond.every(Math.max(tickStep(start, stop, count), 1));
31
+ }
32
+ const [t, step] = tickIntervals[target / tickIntervals[i - 1][2] < tickIntervals[i][2] / target ? i - 1 : i];
33
+ return t.every(step);
34
+ }
35
+ /**
36
+ * Generates time ticks for the given interval.
37
+ *
38
+ * Based on d3's utcTicks algorithm (https://github.com/d3/d3-time/blob/main/src/ticks.js) with one modification:
39
+ * weeks are considered to start on Monday (ISO 8601 standard)
40
+ * instead of Sunday (d3 default).
41
+ */
42
+ export function getDateTimeTicks(start, stop, count = 10) {
43
+ const reverse = stop < start;
44
+ if (reverse) {
45
+ [start, stop] = [stop, start];
46
+ }
47
+ const interval = getDateTimeTickInterval(start.getTime(), stop.getTime(), count);
48
+ const ticks = interval ? interval.range(start, new Date(Number(stop) + 1)) : [];
49
+ return reverse ? ticks.reverse() : ticks;
50
+ }
@@ -0,0 +1,6 @@
1
+ import type { AxisDomain, AxisScale } from 'd3';
2
+ import type { ChartScale } from '../../../hooks';
3
+ export declare function getScaleTicks({ scale, ticksCount, }: {
4
+ scale: ChartScale | AxisScale<AxisDomain>;
5
+ ticksCount?: number;
6
+ }): string[] | number[] | Date[];
@@ -0,0 +1,17 @@
1
+ import { getDateTimeTicks } from './datetime';
2
+ export function getScaleTicks({ scale, ticksCount, }) {
3
+ const scaleDomain = scale.domain();
4
+ switch (typeof scaleDomain[0]) {
5
+ case 'number': {
6
+ return scale.ticks(ticksCount);
7
+ }
8
+ // datetime scale
9
+ case 'object': {
10
+ return getDateTimeTicks(scaleDomain[0], scaleDomain[scaleDomain.length - 1], ticksCount);
11
+ }
12
+ case 'string': {
13
+ return scaleDomain;
14
+ }
15
+ }
16
+ return [];
17
+ }
@@ -1,3 +1,4 @@
1
1
  export declare const TIME_UNITS: Record<string, number>;
2
2
  export declare const DATETIME_LABEL_FORMATS: Record<keyof typeof TIME_UNITS, string>;
3
3
  export declare function getDefaultDateFormat(range?: number): string;
4
+ export declare function getDefaultTimeOnlyFormat(step: number): string;
@@ -32,3 +32,12 @@ export function getDefaultDateFormat(range) {
32
32
  }
33
33
  return DATETIME_LABEL_FORMATS.day;
34
34
  }
35
+ export function getDefaultTimeOnlyFormat(step) {
36
+ if (step < TIME_UNITS.second) {
37
+ return 'HH:mm:ss.SSS';
38
+ }
39
+ if (step < TIME_UNITS.minute) {
40
+ return 'HH:mm:ss';
41
+ }
42
+ return 'HH:mm';
43
+ }
@@ -10,6 +10,7 @@ export const AxisX = (props) => {
10
10
  const lineGenerator = line();
11
11
  const htmlLabels = preparedAxisData.ticks.map((d) => d.htmlLabel).filter(Boolean);
12
12
  React.useEffect(() => {
13
+ var _a, _b;
13
14
  if (!ref.current) {
14
15
  return () => { };
15
16
  }
@@ -32,14 +33,17 @@ export const AxisX = (props) => {
32
33
  .attr('class', b('title'))
33
34
  .append('text')
34
35
  .attr('text-anchor', 'start')
35
- .style('transform', `translate(${preparedAxisData.title.x}px, ${preparedAxisData.title.y}px) rotate(${preparedAxisData.title.rotate}deg) translate(0px, ${preparedAxisData.title.offset}px)`)
36
+ .attr('transform', `translate(${preparedAxisData.title.x},${preparedAxisData.title.y}) rotate(${preparedAxisData.title.rotate}) translate(0,${preparedAxisData.title.offset})`)
36
37
  .attr('font-size', preparedAxisData.title.style.fontSize)
38
+ .attr('font-weight', (_a = preparedAxisData.title.style.fontWeight) !== null && _a !== void 0 ? _a : null)
39
+ .attr('fill', (_b = preparedAxisData.title.style.fontColor) !== null && _b !== void 0 ? _b : null)
37
40
  .selectAll('tspan')
38
41
  .data(preparedAxisData.title.content)
39
42
  .join('tspan')
40
43
  .html((d) => d.text)
41
44
  .attr('x', (d) => d.x)
42
45
  .attr('y', (d) => d.y)
46
+ .attr('dominant-baseline', 'text-before-edge')
43
47
  .attr('text-anchor', 'start');
44
48
  }
45
49
  if (preparedAxisData.domain) {
@@ -64,13 +68,19 @@ export const AxisX = (props) => {
64
68
  if (tickData.line) {
65
69
  tickSelection.append('path').attr('d', lineGenerator(tickData.line.points));
66
70
  }
71
+ if (tickData.mark) {
72
+ tickSelection
73
+ .append('path')
74
+ .attr('class', b('mark', { grid: preparedAxisData.gridEnabled }))
75
+ .attr('d', lineGenerator(tickData.mark.points));
76
+ }
67
77
  if (tickData.svgLabel) {
68
78
  const label = tickData.svgLabel;
69
79
  const textSelection = tickSelection
70
80
  .append('text')
71
- .style('transform', [
72
- `translate(${label.x}px, ${label.y}px)`,
73
- label.angle ? `rotate(${label.angle}deg)` : '',
81
+ .attr('transform', [
82
+ `translate(${label.x}, ${label.y})`,
83
+ label.angle ? `rotate(${label.angle})` : '',
74
84
  ]
75
85
  .filter(Boolean)
76
86
  .join(' '));
@@ -103,7 +113,7 @@ export const AxisX = (props) => {
103
113
  .join('g')
104
114
  .attr(plotDataAttr, 1)
105
115
  .attr(plotBandDataAttr, 1)
106
- .style('transform', (d) => `translate(${d.x}px, ${d.y}px)`);
116
+ .attr('transform', (d) => `translate(${d.x}, ${d.y})`);
107
117
  plotBandsSelection
108
118
  .append('rect')
109
119
  .attr('width', (d) => d.width)
@@ -124,7 +134,7 @@ export const AxisX = (props) => {
124
134
  .style('font-weight', (_b = label.style.fontWeight) !== null && _b !== void 0 ? _b : '')
125
135
  .style('dominant-baseline', 'text-before-edge')
126
136
  .style('text-anchor', 'start')
127
- .style('transform', `translate(${label.x}px, ${label.y}px) rotate(${label.rotate}deg)`)
137
+ .attr('transform', `translate(${label.x}, ${label.y}) rotate(${label.rotate})`)
128
138
  .attr('data-qa', (_c = label.qa) !== null && _c !== void 0 ? _c : null);
129
139
  }
130
140
  });
@@ -144,7 +154,7 @@ export const AxisX = (props) => {
144
154
  .join('g')
145
155
  .attr(plotDataAttr, 1)
146
156
  .attr(plotLineDataAttr, 1)
147
- .style('transform', (d) => `translate(${d.x}px, ${d.y}px)`);
157
+ .attr('transform', (d) => `translate(${d.x}, ${d.y})`);
148
158
  plotLinesSelection
149
159
  .append('path')
150
160
  .attr('d', (d) => lineGenerator(d.points))
@@ -166,7 +176,7 @@ export const AxisX = (props) => {
166
176
  .style('font-weight', (_b = label.style.fontWeight) !== null && _b !== void 0 ? _b : '')
167
177
  .style('dominant-baseline', 'text-before-edge')
168
178
  .style('text-anchor', 'start')
169
- .style('transform', `translate(${label.x}px, ${label.y}px) rotate(${label.rotate}deg)`)
179
+ .attr('transform', `translate(${label.x}, ${label.y}) rotate(${label.rotate})`)
170
180
  .attr('data-qa', (_c = label.qa) !== null && _c !== void 0 ? _c : null);
171
181
  }
172
182
  });
@@ -178,8 +178,19 @@ export async function prepareXAxisData({ axis, boundsOffsetLeft, boundsOffsetRig
178
178
  ],
179
179
  };
180
180
  }
181
+ let mark = null;
182
+ if (isBottomPlot && axis.tickMarks.enabled) {
183
+ const axisBottom = axisTop + axisHeight;
184
+ mark = {
185
+ points: [
186
+ [tickValue.x, axisBottom],
187
+ [tickValue.x, axisBottom + axis.tickMarks.length],
188
+ ],
189
+ };
190
+ }
181
191
  ticks.push({
182
192
  line: tickLine,
193
+ mark,
183
194
  svgLabel,
184
195
  htmlLabel,
185
196
  });
@@ -315,6 +326,7 @@ export async function prepareXAxisData({ axis, boundsOffsetLeft, boundsOffsetRig
315
326
  }
316
327
  xAxisItems.push({
317
328
  id: getUniqId(),
329
+ gridEnabled: axis.grid.enabled,
318
330
  title,
319
331
  ticks,
320
332
  domain,
@@ -6,9 +6,14 @@
6
6
  stroke: none;
7
7
  }
8
8
  .gcharts-x-axis__tick {
9
- stroke: var(--g-color-line-generic);
9
+ stroke: var(--gcharts-axis-tick-color);
10
+ }
11
+ .gcharts-x-axis__mark {
12
+ stroke: var(--gcharts-axis-tick-mark-color, var(--g-color-line-generic-active));
13
+ }
14
+ .gcharts-x-axis__mark_grid {
15
+ stroke: var(--gcharts-axis-tick-mark-color, var(--gcharts-axis-tick-color));
10
16
  }
11
17
  .gcharts-x-axis__title {
12
- dominant-baseline: text-before-edge;
13
18
  fill: var(--g-color-text-secondary);
14
19
  }