@gravity-ui/charts 1.48.3 → 1.49.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 (45) hide show
  1. package/dist/cjs/components/AxisX/prepare-axis-data.js +11 -13
  2. package/dist/cjs/components/AxisY/prepare-axis-data.js +1 -1
  3. package/dist/cjs/components/ChartInner/utils/normalized-original-data.d.ts +1 -0
  4. package/dist/cjs/components/ChartInner/utils/zoom.js +3 -1
  5. package/dist/cjs/core/axes/x-axis.js +3 -1
  6. package/dist/cjs/core/axes/y-axis.js +1 -0
  7. package/dist/cjs/core/constants/defaults/series-options.js +5 -5
  8. package/dist/cjs/core/shapes/bar-x/prepare-data.js +3 -2
  9. package/dist/cjs/core/types/chart/axis.d.ts +38 -0
  10. package/dist/cjs/core/types/chart/series.d.ts +8 -8
  11. package/dist/cjs/core/types/chart/zoom.d.ts +2 -1
  12. package/dist/cjs/core/utils/axis/x-axis.js +7 -2
  13. package/dist/cjs/core/utils/axis-generators/bottom.d.ts +1 -0
  14. package/dist/cjs/core/utils/axis-generators/bottom.js +14 -10
  15. package/dist/cjs/core/utils/bar-y.js +3 -2
  16. package/dist/cjs/core/utils/format.js +39 -2
  17. package/dist/cjs/core/utils/ticks/datetime.d.ts +2 -1
  18. package/dist/cjs/core/utils/ticks/datetime.js +27 -7
  19. package/dist/cjs/core/utils/ticks/index.d.ts +3 -1
  20. package/dist/cjs/core/utils/ticks/index.js +2 -2
  21. package/dist/cjs/core/utils/time.d.ts +2 -0
  22. package/dist/cjs/core/utils/time.js +4 -0
  23. package/dist/esm/components/AxisX/prepare-axis-data.js +11 -13
  24. package/dist/esm/components/AxisY/prepare-axis-data.js +1 -1
  25. package/dist/esm/components/ChartInner/utils/normalized-original-data.d.ts +1 -0
  26. package/dist/esm/components/ChartInner/utils/zoom.js +3 -1
  27. package/dist/esm/core/axes/x-axis.js +3 -1
  28. package/dist/esm/core/axes/y-axis.js +1 -0
  29. package/dist/esm/core/constants/defaults/series-options.js +5 -5
  30. package/dist/esm/core/shapes/bar-x/prepare-data.js +3 -2
  31. package/dist/esm/core/types/chart/axis.d.ts +38 -0
  32. package/dist/esm/core/types/chart/series.d.ts +8 -8
  33. package/dist/esm/core/types/chart/zoom.d.ts +2 -1
  34. package/dist/esm/core/utils/axis/x-axis.js +7 -2
  35. package/dist/esm/core/utils/axis-generators/bottom.d.ts +1 -0
  36. package/dist/esm/core/utils/axis-generators/bottom.js +14 -10
  37. package/dist/esm/core/utils/bar-y.js +3 -2
  38. package/dist/esm/core/utils/format.js +39 -2
  39. package/dist/esm/core/utils/ticks/datetime.d.ts +2 -1
  40. package/dist/esm/core/utils/ticks/datetime.js +27 -7
  41. package/dist/esm/core/utils/ticks/index.d.ts +3 -1
  42. package/dist/esm/core/utils/ticks/index.js +2 -2
  43. package/dist/esm/core/utils/time.d.ts +2 -0
  44. package/dist/esm/core/utils/time.js +4 -0
  45. package/package.json +1 -1
@@ -74,7 +74,7 @@ async function getSvgAxisLabel({ getTextSize, text, axis, top, left, labelMaxWid
74
74
  return svgLabel;
75
75
  }
76
76
  export async function prepareXAxisData({ axis, boundsOffsetLeft, boundsOffsetRight, boundsWidth, chartMarginLeft, chartMarginRight, height, scale, series, split, yAxis, }) {
77
- var _a, _b, _c, _d, _e, _f, _g, _h, _j;
77
+ var _a, _b, _c, _d, _e, _f, _g;
78
78
  const xAxisItems = [];
79
79
  const splitPlots = (_a = split === null || split === void 0 ? void 0 : split.plots) !== null && _a !== void 0 ? _a : [];
80
80
  for (let plotIndex = 0; plotIndex < splitPlots.length; plotIndex++) {
@@ -84,18 +84,16 @@ export async function prepareXAxisData({ axis, boundsOffsetLeft, boundsOffsetRig
84
84
  const axisWidth = boundsWidth;
85
85
  const isBottomPlot = plotIndex === splitPlots.length - 1;
86
86
  const plotYAxes = yAxis.filter((a) => a.plotIndex === plotIndex);
87
- const yDomainLeftPosition = ((_b = plotYAxes.find((a) => a.position === 'left')) === null || _b === void 0 ? void 0 : _b.visible)
88
- ? 0
89
- : null;
90
- const yDomainRightPosition = ((_c = plotYAxes.find((a) => a.position === 'right')) === null || _c === void 0 ? void 0 : _c.visible)
91
- ? axisWidth
92
- : null;
87
+ const leftYAxis = plotYAxes.find((a) => a.position === 'left');
88
+ const yDomainLeftPosition = (leftYAxis === null || leftYAxis === void 0 ? void 0 : leftYAxis.visible) && (leftYAxis === null || leftYAxis === void 0 ? void 0 : leftYAxis.lineVisible) !== false ? 0 : null;
89
+ const rightYAxis = plotYAxes.find((a) => a.position === 'right');
90
+ const yDomainRightPosition = (rightYAxis === null || rightYAxis === void 0 ? void 0 : rightYAxis.visible) && (rightYAxis === null || rightYAxis === void 0 ? void 0 : rightYAxis.lineVisible) !== false ? axisWidth : null;
93
91
  let domain = null;
94
- if (isBottomPlot && axis.visible) {
92
+ if (isBottomPlot && axis.visible && axis.lineVisible !== false) {
95
93
  domain = {
96
94
  start: [0, axisTop + axisHeight],
97
95
  end: [axisWidth, axisTop + axisHeight],
98
- lineColor: (_d = axis.lineColor) !== null && _d !== void 0 ? _d : '',
96
+ lineColor: (_b = axis.lineColor) !== null && _b !== void 0 ? _b : '',
99
97
  };
100
98
  }
101
99
  const ticks = [];
@@ -264,14 +262,14 @@ export async function prepareXAxisData({ axis, boundsOffsetLeft, boundsOffsetRig
264
262
  axisScale,
265
263
  axis: 'x',
266
264
  });
267
- const halfBandwidth = ((_f = (_e = axisScale.bandwidth) === null || _e === void 0 ? void 0 : _e.call(axisScale)) !== null && _f !== void 0 ? _f : 0) / 2;
265
+ const halfBandwidth = ((_d = (_c = axisScale.bandwidth) === null || _c === void 0 ? void 0 : _c.call(axisScale)) !== null && _d !== void 0 ? _d : 0) / 2;
268
266
  const startPos = halfBandwidth + Math.min(from, to);
269
267
  const endPos = Math.min(Math.abs(to - from), axisWidth - Math.min(from, to));
270
268
  const plotBandWidth = Math.min(endPos, axisWidth);
271
269
  if (plotBandWidth < 0) {
272
270
  continue;
273
271
  }
274
- const perpExtent = (_g = calculateNumericProperty({ value: plotBand.size, base: axisHeight })) !== null && _g !== void 0 ? _g : axisHeight;
272
+ const perpExtent = (_e = calculateNumericProperty({ value: plotBand.size, base: axisHeight })) !== null && _e !== void 0 ? _e : axisHeight;
275
273
  // X axis is positioned at the bottom of the plot area, so 'start' = bottom edge.
276
274
  const bandY = plotBand.align === 'end' ? axisTop : axisTop + axisHeight - perpExtent;
277
275
  const getPlotLabelSize = getTextSizeFn({ style: plotBand.label.style });
@@ -290,8 +288,8 @@ export async function prepareXAxisData({ axis, boundsOffsetLeft, boundsOffsetRig
290
288
  ? {
291
289
  text: plotBand.label.text,
292
290
  style: plotBand.label.style,
293
- x: plotBand.label.padding + ((_h = labelSize === null || labelSize === void 0 ? void 0 : labelSize.hangingOffset) !== null && _h !== void 0 ? _h : 0),
294
- y: plotBand.label.padding + ((_j = labelSize === null || labelSize === void 0 ? void 0 : labelSize.width) !== null && _j !== void 0 ? _j : 0),
291
+ x: plotBand.label.padding + ((_f = labelSize === null || labelSize === void 0 ? void 0 : labelSize.hangingOffset) !== null && _f !== void 0 ? _f : 0),
292
+ y: plotBand.label.padding + ((_g = labelSize === null || labelSize === void 0 ? void 0 : labelSize.width) !== null && _g !== void 0 ? _g : 0),
295
293
  rotate: -90,
296
294
  qa: plotBand.label.qa,
297
295
  }
@@ -118,7 +118,7 @@ export async function prepareYAxisData({ axis, split, scale, top: topOffset, wid
118
118
  const axisHeight = ((_b = split === null || split === void 0 ? void 0 : split.plots[axis.plotIndex]) === null || _b === void 0 ? void 0 : _b.height) || height;
119
119
  const domainX = axis.position === 'left' ? 0 : width;
120
120
  let domain = null;
121
- if (axis.visible) {
121
+ if (axis.visible && axis.lineVisible !== false) {
122
122
  domain = {
123
123
  start: [domainX, axisPlotTopPosition],
124
124
  end: [domainX, axisPlotTopPosition + axisHeight],
@@ -9,6 +9,7 @@ export declare function getNormalizedXAxis(props: {
9
9
  type?: import("../../../types").ChartAxisType;
10
10
  labels?: import("../../../types").ChartAxisLabels;
11
11
  lineColor?: string;
12
+ lineVisible?: boolean;
12
13
  title?: import("../../../types").ChartAxisTitle;
13
14
  min?: number;
14
15
  max?: number;
@@ -6,7 +6,9 @@ function mapSeriesTypeToZoomType(seriesType) {
6
6
  case SERIES_TYPE.Area: {
7
7
  return [ZOOM_TYPE.X, ZOOM_TYPE.XY, ZOOM_TYPE.Y];
8
8
  }
9
- case SERIES_TYPE.BarX:
9
+ case SERIES_TYPE.BarX: {
10
+ return [ZOOM_TYPE.X, ZOOM_TYPE.XY];
11
+ }
10
12
  case SERIES_TYPE.XRange: {
11
13
  return [ZOOM_TYPE.X];
12
14
  }
@@ -18,7 +18,7 @@ async function setLabelSettings({ axis, seriesData, width, axisLabels, }) {
18
18
  const tickValues = getXAxisTickValues({ axis, scale, labelLineHeight, series: seriesData });
19
19
  const tickStep = getMinSpaceBetween(tickValues, (d) => Number(d.value));
20
20
  if (axis.type === 'datetime' && !(axisLabels === null || axisLabels === void 0 ? void 0 : axisLabels.dateFormat) && tickStep >= TIME_UNITS.day) {
21
- axis.labels.dateFormat = getDefaultDateFormat(tickStep);
21
+ axis.labels.dateFormat = getDefaultDateFormat(tickStep, axisLabels === null || axisLabels === void 0 ? void 0 : axisLabels.dateTimeLabelFormats);
22
22
  }
23
23
  const labels = tickValues.map((tick) => formatAxisTickLabel({
24
24
  axis,
@@ -104,6 +104,7 @@ export const getPreparedXAxis = async ({ xAxis, seriesData, width, boundsWidth,
104
104
  margin: isLabelsEnabled ? get(xAxis, 'labels.margin', axisLabelsDefaults.margin) : 0,
105
105
  padding: get(xAxis, 'labels.padding', axisLabelsDefaults.padding),
106
106
  dateFormat: get(xAxis, 'labels.dateFormat'),
107
+ dateTimeLabelFormats: get(xAxis, 'labels.dateTimeLabelFormats'),
107
108
  numberFormat: get(xAxis, 'labels.numberFormat'),
108
109
  rotation: get(xAxis, 'labels.rotation', 0),
109
110
  style: labelsStyle,
@@ -114,6 +115,7 @@ export const getPreparedXAxis = async ({ xAxis, seriesData, width, boundsWidth,
114
115
  html: labelsHtml,
115
116
  },
116
117
  lineColor: get(xAxis, 'lineColor'),
118
+ lineVisible: get(xAxis, 'lineVisible', true),
117
119
  categories: xAxis === null || xAxis === void 0 ? void 0 : xAxis.categories,
118
120
  timestamps: get(xAxis, 'timestamps'),
119
121
  title: {
@@ -128,6 +128,7 @@ export const getPreparedYAxis = ({ height, boundsHeight, width, seriesData, yAxi
128
128
  html: labelsHtml,
129
129
  },
130
130
  lineColor: get(axisItem, 'lineColor'),
131
+ lineVisible: get(axisItem, 'lineVisible', true),
131
132
  categories: axisItem.categories,
132
133
  timestamps: get(axisItem, 'timestamps'),
133
134
  title: {
@@ -1,8 +1,8 @@
1
1
  export const seriesOptionsDefaults = {
2
2
  'bar-x': {
3
3
  barMaxWidth: 50,
4
- barPadding: 0.1,
5
- groupPadding: 0.2,
4
+ barPadding: 0.2,
5
+ groupPadding: 0.1,
6
6
  stackGap: 1,
7
7
  states: {
8
8
  hover: {
@@ -17,8 +17,8 @@ export const seriesOptionsDefaults = {
17
17
  },
18
18
  'bar-y': {
19
19
  barMaxWidth: 50,
20
- barPadding: 0.1,
21
- groupPadding: 0.2,
20
+ barPadding: 0.2,
21
+ groupPadding: 0.1,
22
22
  stackGap: 1,
23
23
  states: {
24
24
  hover: {
@@ -105,7 +105,7 @@ export const seriesOptionsDefaults = {
105
105
  },
106
106
  waterfall: {
107
107
  barMaxWidth: 50,
108
- barPadding: 0.1,
108
+ barPadding: 0.2,
109
109
  states: {
110
110
  hover: {
111
111
  enabled: true,
@@ -121,8 +121,9 @@ export const prepareBarXData = async (args) => {
121
121
  });
122
122
  const groupGap = Math.max(bandSize * groupPadding, MIN_BAR_GROUP_GAP);
123
123
  const groupSize = bandSize - groupGap;
124
- const rectGap = Math.max(bandSize * barPadding, MIN_BAR_GAP);
125
- const rectWidth = Math.max(MIN_BAR_WIDTH, Math.min(groupSize / maxGroupSize - rectGap, barMaxWidth));
124
+ const barSlotSize = groupSize / maxGroupSize;
125
+ const rectGap = Math.max(barSlotSize * barPadding, MIN_BAR_GAP);
126
+ const rectWidth = Math.max(MIN_BAR_WIDTH, Math.min(barSlotSize - rectGap, barMaxWidth));
126
127
  const plotIndexes = Array.from(dataByPlots.keys());
127
128
  for (let plotDataIndex = 0; plotDataIndex < plotIndexes.length; plotDataIndex++) {
128
129
  const data = (_b = dataByPlots.get(plotIndexes[plotDataIndex])) !== null && _b !== void 0 ? _b : {};
@@ -1,5 +1,6 @@
1
1
  import type { DurationInput } from '@gravity-ui/date-utils';
2
2
  import type { AXIS_TYPE, DashStyle } from '../../constants';
3
+ import type { DateTimeLabelFormats } from '../../utils/time';
3
4
  import type { FormatNumberOptions } from '../formatter';
4
5
  import type { MeaningfulAny } from '../misc';
5
6
  import type { BaseTextStyle } from './base';
@@ -21,6 +22,38 @@ export interface ChartAxisLabels {
21
22
  */
22
23
  padding?: number;
23
24
  dateFormat?: string;
25
+ /**
26
+ * Per-granularity display formats for the x-axis datetime labels.
27
+ * Ignored when `dateFormat` is set.
28
+ *
29
+ * Each value is a format string in the same form as the `format` argument to
30
+ * [`DateTime#format`](https://gravity-ui.github.io/date-utils/pages/api/DateTime/overview.html) in `@gravity-ui/date-utils`
31
+ * (see the **`FormatInput`** type there): Day.js–style tokens, e.g. `YYYY`, `MM`, `DD`, `HH`, `mm`, `ss`, `SSS`, `MMM`.
32
+ *
33
+ * Additional custom token: `B` expands to the half-year digit (`1` or `2`) — `B` stands for *bi*-annual since `H` and `h` are reserved by Day.js for clock hours.
34
+ * Use Day.js escape syntax for the label prefix, e.g. `'YYYY [H]B'` → `"2024 H1"`.
35
+ * Mirrors the quarter pattern: `'YYYY [Q]Q'` → `"2024 Q2"`.
36
+ *
37
+ * Partial objects are merged with the built-in `DATETIME_LABEL_FORMATS`; omitted keys keep defaults.
38
+ * @example ISO-like date and time
39
+ * ```ts
40
+ * dateTimeLabelFormats: { day: 'YYYY-MM-DD', hour: 'YYYY-MM-DD HH:mm', minute: 'YYYY-MM-DD HH:mm' }
41
+ * ```
42
+ * @example US-style calendar date
43
+ * ```ts
44
+ * dateTimeLabelFormats: { day: 'MM/DD/YYYY', week: 'MM/DD/YYYY' }
45
+ * ```
46
+ * @example Quarter and half-year labels
47
+ * ```ts
48
+ * dateTimeLabelFormats: { quarter: 'YYYY [Q]Q', halfYear: 'YYYY [H]B' }
49
+ * ```
50
+ * @example Coarse ranges: short month and full year
51
+ * ```ts
52
+ * dateTimeLabelFormats: { month: 'YYYY-MM', year: 'YYYY' }
53
+ * ```
54
+ * @see https://gravity-ui.github.io/date-utils/pages/api/DateTime/overview.html
55
+ */
56
+ dateTimeLabelFormats?: DateTimeLabelFormats;
24
57
  numberFormat?: FormatNumberOptions;
25
58
  style?: Partial<BaseTextStyle>;
26
59
  /**
@@ -125,6 +158,11 @@ export interface ChartAxis {
125
158
  labels?: ChartAxisLabels;
126
159
  /** The color of the line marking the axis itself. */
127
160
  lineColor?: string;
161
+ /**
162
+ * Whether to display the line marking the axis itself.
163
+ * @default true
164
+ */
165
+ lineVisible?: boolean;
128
166
  title?: ChartAxisTitle;
129
167
  /**
130
168
  * The minimum value of the axis. If undefined the min value is automatically calculated.
@@ -65,13 +65,13 @@ export interface ChartSeriesOptions {
65
65
  */
66
66
  barMaxWidth?: number;
67
67
  /**
68
- * Padding between each column or bar, in x axis units.
69
- * @default 0.1
68
+ * Padding between each bar as a fraction of the space allocated per bar.
69
+ * @default 0.2
70
70
  */
71
71
  barPadding?: number;
72
72
  /**
73
73
  * Padding between each value groups, in x axis units
74
- * @default 0.2
74
+ * @default 0.1
75
75
  */
76
76
  groupPadding?: number;
77
77
  /**
@@ -113,13 +113,13 @@ export interface ChartSeriesOptions {
113
113
  */
114
114
  barMaxWidth?: number;
115
115
  /**
116
- * Padding between each column or bar, in x axis units.
117
- * @default 0.1
116
+ * Padding between each bar as a fraction of the space allocated per bar.
117
+ * @default 0.2
118
118
  */
119
119
  barPadding?: number;
120
120
  /**
121
121
  * Padding between each value groups, in x axis units
122
- * @default 0.2
122
+ * @default 0.1
123
123
  */
124
124
  groupPadding?: number;
125
125
  /**
@@ -254,8 +254,8 @@ export interface ChartSeriesOptions {
254
254
  */
255
255
  barMaxWidth?: number;
256
256
  /**
257
- * Padding between each column or bar, in x axis units.
258
- * @default 0.1
257
+ * Padding between each bar as a fraction of the space allocated per bar.
258
+ * @default 0.2
259
259
  */
260
260
  barPadding?: number;
261
261
  /** Options for the series states that provide additional styling information to the series. */
@@ -18,7 +18,8 @@ export interface ChartZoom {
18
18
  *
19
19
  * Supported zoom types by series type:
20
20
  * - `Area`, `Line`, `Scatter`: `x`, `y`, `xy`
21
- * - `BarX`, `XRange`: `x`
21
+ * - `BarX`: `x`, `xy`
22
+ * - `XRange`: `x`
22
23
  * - `BarY`: `y`
23
24
  *
24
25
  * Default zoom type by series type:
@@ -38,7 +38,12 @@ export function getXAxisTickValues({ axis, labelLineHeight, scale, series, }) {
38
38
  return [];
39
39
  }
40
40
  const scaleTicksCount = getTicksCount({ axis, axisWidth, series });
41
- const scaleTicks = getScaleTicks({ scale, ticksCount: scaleTicksCount });
41
+ const dateTimeLabelFormats = axis.type === 'datetime' ? axis.labels.dateTimeLabelFormats : undefined;
42
+ const scaleTicks = getScaleTicks({
43
+ scale,
44
+ ticksCount: scaleTicksCount,
45
+ dateTimeLabelFormats,
46
+ });
42
47
  const originalTickValues = scaleTicks.map((t) => ({
43
48
  x: scale(t),
44
49
  value: t,
@@ -52,7 +57,7 @@ export function getXAxisTickValues({ axis, labelLineHeight, scale, series, }) {
52
57
  let ticksCount = result.length - 1;
53
58
  while (availableSpaceForLabel < labelLineHeight && result.length > 1) {
54
59
  ticksCount = ticksCount ? ticksCount - 1 : result.length - 1;
55
- const newScaleTicks = getScaleTicks({ scale, ticksCount });
60
+ const newScaleTicks = getScaleTicks({ scale, ticksCount, dateTimeLabelFormats });
56
61
  result = newScaleTicks.map((t) => ({
57
62
  x: scale(t),
58
63
  value: t,
@@ -5,6 +5,7 @@ interface AxisBottomArgs {
5
5
  domain: {
6
6
  size: number;
7
7
  color?: string;
8
+ visible?: boolean;
8
9
  };
9
10
  htmlLayout: HTMLElement;
10
11
  scale: AxisScale<AxisDomain>;
@@ -194,14 +194,16 @@ export async function axisBottom(args) {
194
194
  .append('path')
195
195
  .attr('d', tickPath.toString())
196
196
  .attr('stroke', tickColor !== null && tickColor !== void 0 ? tickColor : 'currentColor');
197
- // Remove tick that has the same x coordinate like domain
198
- selection
199
- .selectAll('.tick')
200
- .filter((d) => {
201
- return position(d) === 0;
202
- })
203
- .select('path')
204
- .remove();
197
+ // Remove tick that has the same x coordinate like domain (only when domain line is visible)
198
+ if (domain.visible !== false) {
199
+ selection
200
+ .selectAll('.tick')
201
+ .filter((d) => {
202
+ return position(d) === 0;
203
+ })
204
+ .select('path')
205
+ .remove();
206
+ }
205
207
  if (labelsHtml) {
206
208
  appendHtmlLabels({
207
209
  htmlSelection,
@@ -236,7 +238,9 @@ export async function axisBottom(args) {
236
238
  x,
237
239
  });
238
240
  }
239
- const { size: domainSize, color: domainColor } = domain;
240
- selection.call(addDomain, { size: domainSize, color: domainColor });
241
+ const { size: domainSize, color: domainColor, visible: domainVisible } = domain;
242
+ if (domainVisible !== false) {
243
+ selection.call(addDomain, { size: domainSize, color: domainColor });
244
+ }
241
245
  };
242
246
  }
@@ -42,7 +42,8 @@ export function getBarYLayout(args) {
42
42
  const groupGap = Math.max(bandSize * groupPadding, MIN_BAR_GROUP_GAP);
43
43
  const maxGroupSize = max(Object.values(groupedData), (d) => Object.values(d).length) || 1;
44
44
  const groupSize = bandSize - groupGap;
45
- const barGap = Math.max(bandSize * barPadding, MIN_BAR_GAP);
46
- const barSize = Math.max(MIN_BAR_WIDTH, Math.min(groupSize / maxGroupSize - barGap, barMaxWidth));
45
+ const barSlotSize = groupSize / maxGroupSize;
46
+ const barGap = Math.max(barSlotSize * barPadding, MIN_BAR_GAP);
47
+ const barSize = Math.max(MIN_BAR_WIDTH, Math.min(barSlotSize - barGap, barMaxWidth));
47
48
  return { bandSize, barGap, barSize };
48
49
  }
@@ -4,11 +4,48 @@ import { formatNumber, getDefaultUnit } from '../../libs';
4
4
  import { DEFAULT_DATE_FORMAT } from '../constants';
5
5
  import { DATETIME_LABEL_FORMATS, TIME_UNITS, getDefaultDateFormat, getDefaultTimeOnlyFormat, } from './time';
6
6
  const LETTER_MOUNTH_AT_START_FORMAT_REGEXP = /^M{3,}/;
7
+ /**
8
+ * Expands the custom `B` token before passing the format string to `date.format()`.
9
+ *
10
+ * - `B` — half-year (*B*i-annual) digit: expands to `1` or `2`.
11
+ * `H` and `h` are already reserved by Day.js (24 h and 12 h clock hours respectively),
12
+ * so `B` (from Latin *bi*, "twice a year") is used as the token for half-year.
13
+ * No major date library defines a native half-year token, so this is a custom convention.
14
+ * Use Day.js escape syntax for the label prefix, e.g. `'YYYY [H]B'` → `"2024 H1"`.
15
+ * Mirrors the quarter pattern: `'YYYY [Q]Q'` → `"2024 Q2"`.
16
+ *
17
+ * Tokens inside `[…]` Day.js escape blocks are passed through unchanged.
18
+ */
19
+ function processCustomDateTokens(format, date) {
20
+ let result = '';
21
+ let i = 0;
22
+ while (i < format.length) {
23
+ if (format[i] === '[') {
24
+ const end = format.indexOf(']', i + 1);
25
+ if (end === -1) {
26
+ result += format[i++];
27
+ }
28
+ else {
29
+ result += format.slice(i, end + 1);
30
+ i = end + 1;
31
+ }
32
+ }
33
+ else if (format[i] === 'B') {
34
+ result += date.month() < 6 ? '1' : '2';
35
+ i++;
36
+ }
37
+ else {
38
+ result += format[i++];
39
+ }
40
+ }
41
+ return result;
42
+ }
7
43
  function getFormattedDate(args) {
8
44
  const { value, format = DEFAULT_DATE_FORMAT } = args;
9
45
  const date = dateTimeUtc({ input: value });
10
46
  if (date === null || date === void 0 ? void 0 : date.isValid()) {
11
- const formattedDate = date.format(format);
47
+ const processedFormat = processCustomDateTokens(format, date);
48
+ const formattedDate = date.format(processedFormat);
12
49
  if (LETTER_MOUNTH_AT_START_FORMAT_REGEXP.test(format)) {
13
50
  return capitalize(formattedDate);
14
51
  }
@@ -57,7 +94,7 @@ export function formatAxisTickLabel(args) {
57
94
  format = isMidnight ? DATETIME_LABEL_FORMATS.day : getDefaultTimeOnlyFormat(step);
58
95
  }
59
96
  else {
60
- format = getDefaultDateFormat(step);
97
+ format = getDefaultDateFormat(step, axis.labels.dateTimeLabelFormats);
61
98
  }
62
99
  return getFormattedDate({ value: date, format });
63
100
  }
@@ -1,3 +1,4 @@
1
+ import type { DateTimeLabelFormats } from '../time';
1
2
  /**
2
3
  * Generates time ticks for the given interval.
3
4
  *
@@ -5,4 +6,4 @@
5
6
  * weeks are considered to start on Monday (ISO 8601 standard)
6
7
  * instead of Sunday (d3 default).
7
8
  */
8
- export declare function getDateTimeTicks(start: Date, stop: Date, count?: number): Date[];
9
+ export declare function getDateTimeTicks(start: Date, stop: Date, count?: number, formats?: DateTimeLabelFormats): Date[];
@@ -1,7 +1,10 @@
1
1
  import { bisector, tickStep } from 'd3-array';
2
2
  import { utcDay, utcHour, utcMillisecond, utcMinute, utcMonth, utcSecond, utcMonday as utcWeek, utcYear, } from 'd3-time';
3
3
  import { DAY, HOUR, MINUTE, MONTH, SECOND, WEEK, YEAR } from '../../constants';
4
- const tickIntervals = [
4
+ // Base tick intervals matching the original D3 utcTicks algorithm
5
+ // (with Monday-start weeks). Extra granularities live in optionalTickIntervals
6
+ // and are injected only when the matching dateTimeLabelFormats key is provided.
7
+ const baseTickIntervals = [
5
8
  [utcSecond, 1, SECOND],
6
9
  [utcSecond, 5, 5 * SECOND],
7
10
  [utcSecond, 15, 15 * SECOND],
@@ -21,20 +24,37 @@ const tickIntervals = [
21
24
  [utcMonth, 3, 3 * MONTH],
22
25
  [utcYear, 1, YEAR],
23
26
  ];
27
+ const optionalTickIntervals = {
28
+ halfYear: [utcMonth, 6, 6 * MONTH],
29
+ };
24
30
  // utcDay.every(2) resets its day counter at the start of each month (field = getUTCDate() - 1),
25
31
  // so in a 31-day month the last tick lands on day 31 and the next tick is day 1 of the following
26
32
  // month — only 1 day apart. Filtering by absolute Unix day number avoids the monthly reset.
27
33
  const utcEvery2Days = utcDay.filter((d) => Math.floor(d.getTime() / DAY) % 2 === 0);
28
- function getDateTimeTickInterval(start, stop, count) {
34
+ function buildTickIntervals(formats) {
35
+ if (!formats) {
36
+ return baseTickIntervals;
37
+ }
38
+ const extras = Object.keys(formats).flatMap((key) => {
39
+ const entry = optionalTickIntervals[key];
40
+ return entry ? [entry] : [];
41
+ });
42
+ if (extras.length === 0) {
43
+ return baseTickIntervals;
44
+ }
45
+ return [...baseTickIntervals, ...extras].sort((a, b) => a[2] - b[2]);
46
+ }
47
+ function getDateTimeTickInterval(start, stop, count, formats) {
48
+ const intervals = buildTickIntervals(formats);
29
49
  const target = Math.abs(stop - start) / count;
30
- const i = bisector(([, , step]) => step).right(tickIntervals, target);
31
- if (i === tickIntervals.length) {
50
+ const i = bisector(([, , step]) => step).right(intervals, target);
51
+ if (i === intervals.length) {
32
52
  return utcYear.every(tickStep(start / YEAR, stop / YEAR, count));
33
53
  }
34
54
  if (i === 0) {
35
55
  return utcMillisecond.every(Math.max(tickStep(start, stop, count), 1));
36
56
  }
37
- const [t, step] = tickIntervals[target / tickIntervals[i - 1][2] < tickIntervals[i][2] / target ? i - 1 : i];
57
+ const [t, step] = intervals[target / intervals[i - 1][2] < intervals[i][2] / target ? i - 1 : i];
38
58
  if (t === utcDay && step === 2) {
39
59
  return utcEvery2Days;
40
60
  }
@@ -47,12 +67,12 @@ function getDateTimeTickInterval(start, stop, count) {
47
67
  * weeks are considered to start on Monday (ISO 8601 standard)
48
68
  * instead of Sunday (d3 default).
49
69
  */
50
- export function getDateTimeTicks(start, stop, count = 10) {
70
+ export function getDateTimeTicks(start, stop, count = 10, formats) {
51
71
  const reverse = stop < start;
52
72
  if (reverse) {
53
73
  [start, stop] = [stop, start];
54
74
  }
55
- const interval = getDateTimeTickInterval(start.getTime(), stop.getTime(), count);
75
+ const interval = getDateTimeTickInterval(start.getTime(), stop.getTime(), count, formats);
56
76
  const ticks = interval ? interval.range(start, new Date(Number(stop) + 1)) : [];
57
77
  return reverse ? ticks.reverse() : ticks;
58
78
  }
@@ -1,6 +1,8 @@
1
1
  import type { AxisDomain, AxisScale } from 'd3-axis';
2
2
  import type { ChartScale } from '../../../hooks';
3
- export declare function getScaleTicks({ scale, ticksCount, }: {
3
+ import { getDateTimeTicks } from './datetime';
4
+ export declare function getScaleTicks({ scale, ticksCount, dateTimeLabelFormats, }: {
4
5
  scale: ChartScale | AxisScale<AxisDomain>;
5
6
  ticksCount?: number;
7
+ dateTimeLabelFormats?: Parameters<typeof getDateTimeTicks>[3];
6
8
  }): string[] | number[] | Date[];
@@ -1,5 +1,5 @@
1
1
  import { getDateTimeTicks } from './datetime';
2
- export function getScaleTicks({ scale, ticksCount, }) {
2
+ export function getScaleTicks({ scale, ticksCount, dateTimeLabelFormats, }) {
3
3
  const scaleDomain = scale.domain();
4
4
  switch (typeof scaleDomain[0]) {
5
5
  case 'number': {
@@ -7,7 +7,7 @@ export function getScaleTicks({ scale, ticksCount, }) {
7
7
  }
8
8
  // datetime scale
9
9
  case 'object': {
10
- return getDateTimeTicks(scaleDomain[0], scaleDomain[scaleDomain.length - 1], ticksCount);
10
+ return getDateTimeTicks(scaleDomain[0], scaleDomain[scaleDomain.length - 1], ticksCount, dateTimeLabelFormats);
11
11
  }
12
12
  case 'string': {
13
13
  return scaleDomain;
@@ -6,6 +6,8 @@ export declare const TIME_UNITS: {
6
6
  readonly day: number;
7
7
  readonly week: number;
8
8
  readonly month: number;
9
+ readonly quarter: number;
10
+ readonly halfYear: number;
9
11
  readonly year: number;
10
12
  };
11
13
  export type TimeUnit = keyof typeof TIME_UNITS;
@@ -6,6 +6,8 @@ export const TIME_UNITS = {
6
6
  day: 24 * 3600000,
7
7
  week: 7 * 24 * 3600000,
8
8
  month: 28 * 24 * 3600000,
9
+ quarter: 62 * 24 * 3600000,
10
+ halfYear: 160 * 24 * 3600000,
9
11
  year: 364 * 24 * 3600000,
10
12
  };
11
13
  export const DATETIME_LABEL_FORMATS = {
@@ -16,6 +18,8 @@ export const DATETIME_LABEL_FORMATS = {
16
18
  day: 'DD.MM.YY',
17
19
  week: 'DD.MM.YY',
18
20
  month: "MMM 'YY",
21
+ quarter: "MMM 'YY",
22
+ halfYear: "MMM 'YY",
19
23
  year: 'YYYY',
20
24
  };
21
25
  function getTimeUnit(range) {
@@ -74,7 +74,7 @@ async function getSvgAxisLabel({ getTextSize, text, axis, top, left, labelMaxWid
74
74
  return svgLabel;
75
75
  }
76
76
  export async function prepareXAxisData({ axis, boundsOffsetLeft, boundsOffsetRight, boundsWidth, chartMarginLeft, chartMarginRight, height, scale, series, split, yAxis, }) {
77
- var _a, _b, _c, _d, _e, _f, _g, _h, _j;
77
+ var _a, _b, _c, _d, _e, _f, _g;
78
78
  const xAxisItems = [];
79
79
  const splitPlots = (_a = split === null || split === void 0 ? void 0 : split.plots) !== null && _a !== void 0 ? _a : [];
80
80
  for (let plotIndex = 0; plotIndex < splitPlots.length; plotIndex++) {
@@ -84,18 +84,16 @@ export async function prepareXAxisData({ axis, boundsOffsetLeft, boundsOffsetRig
84
84
  const axisWidth = boundsWidth;
85
85
  const isBottomPlot = plotIndex === splitPlots.length - 1;
86
86
  const plotYAxes = yAxis.filter((a) => a.plotIndex === plotIndex);
87
- const yDomainLeftPosition = ((_b = plotYAxes.find((a) => a.position === 'left')) === null || _b === void 0 ? void 0 : _b.visible)
88
- ? 0
89
- : null;
90
- const yDomainRightPosition = ((_c = plotYAxes.find((a) => a.position === 'right')) === null || _c === void 0 ? void 0 : _c.visible)
91
- ? axisWidth
92
- : null;
87
+ const leftYAxis = plotYAxes.find((a) => a.position === 'left');
88
+ const yDomainLeftPosition = (leftYAxis === null || leftYAxis === void 0 ? void 0 : leftYAxis.visible) && (leftYAxis === null || leftYAxis === void 0 ? void 0 : leftYAxis.lineVisible) !== false ? 0 : null;
89
+ const rightYAxis = plotYAxes.find((a) => a.position === 'right');
90
+ const yDomainRightPosition = (rightYAxis === null || rightYAxis === void 0 ? void 0 : rightYAxis.visible) && (rightYAxis === null || rightYAxis === void 0 ? void 0 : rightYAxis.lineVisible) !== false ? axisWidth : null;
93
91
  let domain = null;
94
- if (isBottomPlot && axis.visible) {
92
+ if (isBottomPlot && axis.visible && axis.lineVisible !== false) {
95
93
  domain = {
96
94
  start: [0, axisTop + axisHeight],
97
95
  end: [axisWidth, axisTop + axisHeight],
98
- lineColor: (_d = axis.lineColor) !== null && _d !== void 0 ? _d : '',
96
+ lineColor: (_b = axis.lineColor) !== null && _b !== void 0 ? _b : '',
99
97
  };
100
98
  }
101
99
  const ticks = [];
@@ -264,14 +262,14 @@ export async function prepareXAxisData({ axis, boundsOffsetLeft, boundsOffsetRig
264
262
  axisScale,
265
263
  axis: 'x',
266
264
  });
267
- const halfBandwidth = ((_f = (_e = axisScale.bandwidth) === null || _e === void 0 ? void 0 : _e.call(axisScale)) !== null && _f !== void 0 ? _f : 0) / 2;
265
+ const halfBandwidth = ((_d = (_c = axisScale.bandwidth) === null || _c === void 0 ? void 0 : _c.call(axisScale)) !== null && _d !== void 0 ? _d : 0) / 2;
268
266
  const startPos = halfBandwidth + Math.min(from, to);
269
267
  const endPos = Math.min(Math.abs(to - from), axisWidth - Math.min(from, to));
270
268
  const plotBandWidth = Math.min(endPos, axisWidth);
271
269
  if (plotBandWidth < 0) {
272
270
  continue;
273
271
  }
274
- const perpExtent = (_g = calculateNumericProperty({ value: plotBand.size, base: axisHeight })) !== null && _g !== void 0 ? _g : axisHeight;
272
+ const perpExtent = (_e = calculateNumericProperty({ value: plotBand.size, base: axisHeight })) !== null && _e !== void 0 ? _e : axisHeight;
275
273
  // X axis is positioned at the bottom of the plot area, so 'start' = bottom edge.
276
274
  const bandY = plotBand.align === 'end' ? axisTop : axisTop + axisHeight - perpExtent;
277
275
  const getPlotLabelSize = getTextSizeFn({ style: plotBand.label.style });
@@ -290,8 +288,8 @@ export async function prepareXAxisData({ axis, boundsOffsetLeft, boundsOffsetRig
290
288
  ? {
291
289
  text: plotBand.label.text,
292
290
  style: plotBand.label.style,
293
- x: plotBand.label.padding + ((_h = labelSize === null || labelSize === void 0 ? void 0 : labelSize.hangingOffset) !== null && _h !== void 0 ? _h : 0),
294
- y: plotBand.label.padding + ((_j = labelSize === null || labelSize === void 0 ? void 0 : labelSize.width) !== null && _j !== void 0 ? _j : 0),
291
+ x: plotBand.label.padding + ((_f = labelSize === null || labelSize === void 0 ? void 0 : labelSize.hangingOffset) !== null && _f !== void 0 ? _f : 0),
292
+ y: plotBand.label.padding + ((_g = labelSize === null || labelSize === void 0 ? void 0 : labelSize.width) !== null && _g !== void 0 ? _g : 0),
295
293
  rotate: -90,
296
294
  qa: plotBand.label.qa,
297
295
  }
@@ -118,7 +118,7 @@ export async function prepareYAxisData({ axis, split, scale, top: topOffset, wid
118
118
  const axisHeight = ((_b = split === null || split === void 0 ? void 0 : split.plots[axis.plotIndex]) === null || _b === void 0 ? void 0 : _b.height) || height;
119
119
  const domainX = axis.position === 'left' ? 0 : width;
120
120
  let domain = null;
121
- if (axis.visible) {
121
+ if (axis.visible && axis.lineVisible !== false) {
122
122
  domain = {
123
123
  start: [domainX, axisPlotTopPosition],
124
124
  end: [domainX, axisPlotTopPosition + axisHeight],
@@ -9,6 +9,7 @@ export declare function getNormalizedXAxis(props: {
9
9
  type?: import("../../..").ChartAxisType;
10
10
  labels?: import("../../..").ChartAxisLabels;
11
11
  lineColor?: string;
12
+ lineVisible?: boolean;
12
13
  title?: import("../../..").ChartAxisTitle;
13
14
  min?: number;
14
15
  max?: number;
@@ -6,7 +6,9 @@ function mapSeriesTypeToZoomType(seriesType) {
6
6
  case SERIES_TYPE.Area: {
7
7
  return [ZOOM_TYPE.X, ZOOM_TYPE.XY, ZOOM_TYPE.Y];
8
8
  }
9
- case SERIES_TYPE.BarX:
9
+ case SERIES_TYPE.BarX: {
10
+ return [ZOOM_TYPE.X, ZOOM_TYPE.XY];
11
+ }
10
12
  case SERIES_TYPE.XRange: {
11
13
  return [ZOOM_TYPE.X];
12
14
  }
@@ -18,7 +18,7 @@ async function setLabelSettings({ axis, seriesData, width, axisLabels, }) {
18
18
  const tickValues = getXAxisTickValues({ axis, scale, labelLineHeight, series: seriesData });
19
19
  const tickStep = getMinSpaceBetween(tickValues, (d) => Number(d.value));
20
20
  if (axis.type === 'datetime' && !(axisLabels === null || axisLabels === void 0 ? void 0 : axisLabels.dateFormat) && tickStep >= TIME_UNITS.day) {
21
- axis.labels.dateFormat = getDefaultDateFormat(tickStep);
21
+ axis.labels.dateFormat = getDefaultDateFormat(tickStep, axisLabels === null || axisLabels === void 0 ? void 0 : axisLabels.dateTimeLabelFormats);
22
22
  }
23
23
  const labels = tickValues.map((tick) => formatAxisTickLabel({
24
24
  axis,
@@ -104,6 +104,7 @@ export const getPreparedXAxis = async ({ xAxis, seriesData, width, boundsWidth,
104
104
  margin: isLabelsEnabled ? get(xAxis, 'labels.margin', axisLabelsDefaults.margin) : 0,
105
105
  padding: get(xAxis, 'labels.padding', axisLabelsDefaults.padding),
106
106
  dateFormat: get(xAxis, 'labels.dateFormat'),
107
+ dateTimeLabelFormats: get(xAxis, 'labels.dateTimeLabelFormats'),
107
108
  numberFormat: get(xAxis, 'labels.numberFormat'),
108
109
  rotation: get(xAxis, 'labels.rotation', 0),
109
110
  style: labelsStyle,
@@ -114,6 +115,7 @@ export const getPreparedXAxis = async ({ xAxis, seriesData, width, boundsWidth,
114
115
  html: labelsHtml,
115
116
  },
116
117
  lineColor: get(xAxis, 'lineColor'),
118
+ lineVisible: get(xAxis, 'lineVisible', true),
117
119
  categories: xAxis === null || xAxis === void 0 ? void 0 : xAxis.categories,
118
120
  timestamps: get(xAxis, 'timestamps'),
119
121
  title: {
@@ -128,6 +128,7 @@ export const getPreparedYAxis = ({ height, boundsHeight, width, seriesData, yAxi
128
128
  html: labelsHtml,
129
129
  },
130
130
  lineColor: get(axisItem, 'lineColor'),
131
+ lineVisible: get(axisItem, 'lineVisible', true),
131
132
  categories: axisItem.categories,
132
133
  timestamps: get(axisItem, 'timestamps'),
133
134
  title: {
@@ -1,8 +1,8 @@
1
1
  export const seriesOptionsDefaults = {
2
2
  'bar-x': {
3
3
  barMaxWidth: 50,
4
- barPadding: 0.1,
5
- groupPadding: 0.2,
4
+ barPadding: 0.2,
5
+ groupPadding: 0.1,
6
6
  stackGap: 1,
7
7
  states: {
8
8
  hover: {
@@ -17,8 +17,8 @@ export const seriesOptionsDefaults = {
17
17
  },
18
18
  'bar-y': {
19
19
  barMaxWidth: 50,
20
- barPadding: 0.1,
21
- groupPadding: 0.2,
20
+ barPadding: 0.2,
21
+ groupPadding: 0.1,
22
22
  stackGap: 1,
23
23
  states: {
24
24
  hover: {
@@ -105,7 +105,7 @@ export const seriesOptionsDefaults = {
105
105
  },
106
106
  waterfall: {
107
107
  barMaxWidth: 50,
108
- barPadding: 0.1,
108
+ barPadding: 0.2,
109
109
  states: {
110
110
  hover: {
111
111
  enabled: true,
@@ -121,8 +121,9 @@ export const prepareBarXData = async (args) => {
121
121
  });
122
122
  const groupGap = Math.max(bandSize * groupPadding, MIN_BAR_GROUP_GAP);
123
123
  const groupSize = bandSize - groupGap;
124
- const rectGap = Math.max(bandSize * barPadding, MIN_BAR_GAP);
125
- const rectWidth = Math.max(MIN_BAR_WIDTH, Math.min(groupSize / maxGroupSize - rectGap, barMaxWidth));
124
+ const barSlotSize = groupSize / maxGroupSize;
125
+ const rectGap = Math.max(barSlotSize * barPadding, MIN_BAR_GAP);
126
+ const rectWidth = Math.max(MIN_BAR_WIDTH, Math.min(barSlotSize - rectGap, barMaxWidth));
126
127
  const plotIndexes = Array.from(dataByPlots.keys());
127
128
  for (let plotDataIndex = 0; plotDataIndex < plotIndexes.length; plotDataIndex++) {
128
129
  const data = (_b = dataByPlots.get(plotIndexes[plotDataIndex])) !== null && _b !== void 0 ? _b : {};
@@ -1,5 +1,6 @@
1
1
  import type { DurationInput } from '@gravity-ui/date-utils';
2
2
  import type { AXIS_TYPE, DashStyle } from '../../constants';
3
+ import type { DateTimeLabelFormats } from '../../utils/time';
3
4
  import type { FormatNumberOptions } from '../formatter';
4
5
  import type { MeaningfulAny } from '../misc';
5
6
  import type { BaseTextStyle } from './base';
@@ -21,6 +22,38 @@ export interface ChartAxisLabels {
21
22
  */
22
23
  padding?: number;
23
24
  dateFormat?: string;
25
+ /**
26
+ * Per-granularity display formats for the x-axis datetime labels.
27
+ * Ignored when `dateFormat` is set.
28
+ *
29
+ * Each value is a format string in the same form as the `format` argument to
30
+ * [`DateTime#format`](https://gravity-ui.github.io/date-utils/pages/api/DateTime/overview.html) in `@gravity-ui/date-utils`
31
+ * (see the **`FormatInput`** type there): Day.js–style tokens, e.g. `YYYY`, `MM`, `DD`, `HH`, `mm`, `ss`, `SSS`, `MMM`.
32
+ *
33
+ * Additional custom token: `B` expands to the half-year digit (`1` or `2`) — `B` stands for *bi*-annual since `H` and `h` are reserved by Day.js for clock hours.
34
+ * Use Day.js escape syntax for the label prefix, e.g. `'YYYY [H]B'` → `"2024 H1"`.
35
+ * Mirrors the quarter pattern: `'YYYY [Q]Q'` → `"2024 Q2"`.
36
+ *
37
+ * Partial objects are merged with the built-in `DATETIME_LABEL_FORMATS`; omitted keys keep defaults.
38
+ * @example ISO-like date and time
39
+ * ```ts
40
+ * dateTimeLabelFormats: { day: 'YYYY-MM-DD', hour: 'YYYY-MM-DD HH:mm', minute: 'YYYY-MM-DD HH:mm' }
41
+ * ```
42
+ * @example US-style calendar date
43
+ * ```ts
44
+ * dateTimeLabelFormats: { day: 'MM/DD/YYYY', week: 'MM/DD/YYYY' }
45
+ * ```
46
+ * @example Quarter and half-year labels
47
+ * ```ts
48
+ * dateTimeLabelFormats: { quarter: 'YYYY [Q]Q', halfYear: 'YYYY [H]B' }
49
+ * ```
50
+ * @example Coarse ranges: short month and full year
51
+ * ```ts
52
+ * dateTimeLabelFormats: { month: 'YYYY-MM', year: 'YYYY' }
53
+ * ```
54
+ * @see https://gravity-ui.github.io/date-utils/pages/api/DateTime/overview.html
55
+ */
56
+ dateTimeLabelFormats?: DateTimeLabelFormats;
24
57
  numberFormat?: FormatNumberOptions;
25
58
  style?: Partial<BaseTextStyle>;
26
59
  /**
@@ -125,6 +158,11 @@ export interface ChartAxis {
125
158
  labels?: ChartAxisLabels;
126
159
  /** The color of the line marking the axis itself. */
127
160
  lineColor?: string;
161
+ /**
162
+ * Whether to display the line marking the axis itself.
163
+ * @default true
164
+ */
165
+ lineVisible?: boolean;
128
166
  title?: ChartAxisTitle;
129
167
  /**
130
168
  * The minimum value of the axis. If undefined the min value is automatically calculated.
@@ -65,13 +65,13 @@ export interface ChartSeriesOptions {
65
65
  */
66
66
  barMaxWidth?: number;
67
67
  /**
68
- * Padding between each column or bar, in x axis units.
69
- * @default 0.1
68
+ * Padding between each bar as a fraction of the space allocated per bar.
69
+ * @default 0.2
70
70
  */
71
71
  barPadding?: number;
72
72
  /**
73
73
  * Padding between each value groups, in x axis units
74
- * @default 0.2
74
+ * @default 0.1
75
75
  */
76
76
  groupPadding?: number;
77
77
  /**
@@ -113,13 +113,13 @@ export interface ChartSeriesOptions {
113
113
  */
114
114
  barMaxWidth?: number;
115
115
  /**
116
- * Padding between each column or bar, in x axis units.
117
- * @default 0.1
116
+ * Padding between each bar as a fraction of the space allocated per bar.
117
+ * @default 0.2
118
118
  */
119
119
  barPadding?: number;
120
120
  /**
121
121
  * Padding between each value groups, in x axis units
122
- * @default 0.2
122
+ * @default 0.1
123
123
  */
124
124
  groupPadding?: number;
125
125
  /**
@@ -254,8 +254,8 @@ export interface ChartSeriesOptions {
254
254
  */
255
255
  barMaxWidth?: number;
256
256
  /**
257
- * Padding between each column or bar, in x axis units.
258
- * @default 0.1
257
+ * Padding between each bar as a fraction of the space allocated per bar.
258
+ * @default 0.2
259
259
  */
260
260
  barPadding?: number;
261
261
  /** Options for the series states that provide additional styling information to the series. */
@@ -18,7 +18,8 @@ export interface ChartZoom {
18
18
  *
19
19
  * Supported zoom types by series type:
20
20
  * - `Area`, `Line`, `Scatter`: `x`, `y`, `xy`
21
- * - `BarX`, `XRange`: `x`
21
+ * - `BarX`: `x`, `xy`
22
+ * - `XRange`: `x`
22
23
  * - `BarY`: `y`
23
24
  *
24
25
  * Default zoom type by series type:
@@ -38,7 +38,12 @@ export function getXAxisTickValues({ axis, labelLineHeight, scale, series, }) {
38
38
  return [];
39
39
  }
40
40
  const scaleTicksCount = getTicksCount({ axis, axisWidth, series });
41
- const scaleTicks = getScaleTicks({ scale, ticksCount: scaleTicksCount });
41
+ const dateTimeLabelFormats = axis.type === 'datetime' ? axis.labels.dateTimeLabelFormats : undefined;
42
+ const scaleTicks = getScaleTicks({
43
+ scale,
44
+ ticksCount: scaleTicksCount,
45
+ dateTimeLabelFormats,
46
+ });
42
47
  const originalTickValues = scaleTicks.map((t) => ({
43
48
  x: scale(t),
44
49
  value: t,
@@ -52,7 +57,7 @@ export function getXAxisTickValues({ axis, labelLineHeight, scale, series, }) {
52
57
  let ticksCount = result.length - 1;
53
58
  while (availableSpaceForLabel < labelLineHeight && result.length > 1) {
54
59
  ticksCount = ticksCount ? ticksCount - 1 : result.length - 1;
55
- const newScaleTicks = getScaleTicks({ scale, ticksCount });
60
+ const newScaleTicks = getScaleTicks({ scale, ticksCount, dateTimeLabelFormats });
56
61
  result = newScaleTicks.map((t) => ({
57
62
  x: scale(t),
58
63
  value: t,
@@ -5,6 +5,7 @@ interface AxisBottomArgs {
5
5
  domain: {
6
6
  size: number;
7
7
  color?: string;
8
+ visible?: boolean;
8
9
  };
9
10
  htmlLayout: HTMLElement;
10
11
  scale: AxisScale<AxisDomain>;
@@ -194,14 +194,16 @@ export async function axisBottom(args) {
194
194
  .append('path')
195
195
  .attr('d', tickPath.toString())
196
196
  .attr('stroke', tickColor !== null && tickColor !== void 0 ? tickColor : 'currentColor');
197
- // Remove tick that has the same x coordinate like domain
198
- selection
199
- .selectAll('.tick')
200
- .filter((d) => {
201
- return position(d) === 0;
202
- })
203
- .select('path')
204
- .remove();
197
+ // Remove tick that has the same x coordinate like domain (only when domain line is visible)
198
+ if (domain.visible !== false) {
199
+ selection
200
+ .selectAll('.tick')
201
+ .filter((d) => {
202
+ return position(d) === 0;
203
+ })
204
+ .select('path')
205
+ .remove();
206
+ }
205
207
  if (labelsHtml) {
206
208
  appendHtmlLabels({
207
209
  htmlSelection,
@@ -236,7 +238,9 @@ export async function axisBottom(args) {
236
238
  x,
237
239
  });
238
240
  }
239
- const { size: domainSize, color: domainColor } = domain;
240
- selection.call(addDomain, { size: domainSize, color: domainColor });
241
+ const { size: domainSize, color: domainColor, visible: domainVisible } = domain;
242
+ if (domainVisible !== false) {
243
+ selection.call(addDomain, { size: domainSize, color: domainColor });
244
+ }
241
245
  };
242
246
  }
@@ -42,7 +42,8 @@ export function getBarYLayout(args) {
42
42
  const groupGap = Math.max(bandSize * groupPadding, MIN_BAR_GROUP_GAP);
43
43
  const maxGroupSize = max(Object.values(groupedData), (d) => Object.values(d).length) || 1;
44
44
  const groupSize = bandSize - groupGap;
45
- const barGap = Math.max(bandSize * barPadding, MIN_BAR_GAP);
46
- const barSize = Math.max(MIN_BAR_WIDTH, Math.min(groupSize / maxGroupSize - barGap, barMaxWidth));
45
+ const barSlotSize = groupSize / maxGroupSize;
46
+ const barGap = Math.max(barSlotSize * barPadding, MIN_BAR_GAP);
47
+ const barSize = Math.max(MIN_BAR_WIDTH, Math.min(barSlotSize - barGap, barMaxWidth));
47
48
  return { bandSize, barGap, barSize };
48
49
  }
@@ -4,11 +4,48 @@ import { formatNumber, getDefaultUnit } from '../../libs';
4
4
  import { DEFAULT_DATE_FORMAT } from '../constants';
5
5
  import { DATETIME_LABEL_FORMATS, TIME_UNITS, getDefaultDateFormat, getDefaultTimeOnlyFormat, } from './time';
6
6
  const LETTER_MOUNTH_AT_START_FORMAT_REGEXP = /^M{3,}/;
7
+ /**
8
+ * Expands the custom `B` token before passing the format string to `date.format()`.
9
+ *
10
+ * - `B` — half-year (*B*i-annual) digit: expands to `1` or `2`.
11
+ * `H` and `h` are already reserved by Day.js (24 h and 12 h clock hours respectively),
12
+ * so `B` (from Latin *bi*, "twice a year") is used as the token for half-year.
13
+ * No major date library defines a native half-year token, so this is a custom convention.
14
+ * Use Day.js escape syntax for the label prefix, e.g. `'YYYY [H]B'` → `"2024 H1"`.
15
+ * Mirrors the quarter pattern: `'YYYY [Q]Q'` → `"2024 Q2"`.
16
+ *
17
+ * Tokens inside `[…]` Day.js escape blocks are passed through unchanged.
18
+ */
19
+ function processCustomDateTokens(format, date) {
20
+ let result = '';
21
+ let i = 0;
22
+ while (i < format.length) {
23
+ if (format[i] === '[') {
24
+ const end = format.indexOf(']', i + 1);
25
+ if (end === -1) {
26
+ result += format[i++];
27
+ }
28
+ else {
29
+ result += format.slice(i, end + 1);
30
+ i = end + 1;
31
+ }
32
+ }
33
+ else if (format[i] === 'B') {
34
+ result += date.month() < 6 ? '1' : '2';
35
+ i++;
36
+ }
37
+ else {
38
+ result += format[i++];
39
+ }
40
+ }
41
+ return result;
42
+ }
7
43
  function getFormattedDate(args) {
8
44
  const { value, format = DEFAULT_DATE_FORMAT } = args;
9
45
  const date = dateTimeUtc({ input: value });
10
46
  if (date === null || date === void 0 ? void 0 : date.isValid()) {
11
- const formattedDate = date.format(format);
47
+ const processedFormat = processCustomDateTokens(format, date);
48
+ const formattedDate = date.format(processedFormat);
12
49
  if (LETTER_MOUNTH_AT_START_FORMAT_REGEXP.test(format)) {
13
50
  return capitalize(formattedDate);
14
51
  }
@@ -57,7 +94,7 @@ export function formatAxisTickLabel(args) {
57
94
  format = isMidnight ? DATETIME_LABEL_FORMATS.day : getDefaultTimeOnlyFormat(step);
58
95
  }
59
96
  else {
60
- format = getDefaultDateFormat(step);
97
+ format = getDefaultDateFormat(step, axis.labels.dateTimeLabelFormats);
61
98
  }
62
99
  return getFormattedDate({ value: date, format });
63
100
  }
@@ -1,3 +1,4 @@
1
+ import type { DateTimeLabelFormats } from '../time';
1
2
  /**
2
3
  * Generates time ticks for the given interval.
3
4
  *
@@ -5,4 +6,4 @@
5
6
  * weeks are considered to start on Monday (ISO 8601 standard)
6
7
  * instead of Sunday (d3 default).
7
8
  */
8
- export declare function getDateTimeTicks(start: Date, stop: Date, count?: number): Date[];
9
+ export declare function getDateTimeTicks(start: Date, stop: Date, count?: number, formats?: DateTimeLabelFormats): Date[];
@@ -1,7 +1,10 @@
1
1
  import { bisector, tickStep } from 'd3-array';
2
2
  import { utcDay, utcHour, utcMillisecond, utcMinute, utcMonth, utcSecond, utcMonday as utcWeek, utcYear, } from 'd3-time';
3
3
  import { DAY, HOUR, MINUTE, MONTH, SECOND, WEEK, YEAR } from '../../constants';
4
- const tickIntervals = [
4
+ // Base tick intervals matching the original D3 utcTicks algorithm
5
+ // (with Monday-start weeks). Extra granularities live in optionalTickIntervals
6
+ // and are injected only when the matching dateTimeLabelFormats key is provided.
7
+ const baseTickIntervals = [
5
8
  [utcSecond, 1, SECOND],
6
9
  [utcSecond, 5, 5 * SECOND],
7
10
  [utcSecond, 15, 15 * SECOND],
@@ -21,20 +24,37 @@ const tickIntervals = [
21
24
  [utcMonth, 3, 3 * MONTH],
22
25
  [utcYear, 1, YEAR],
23
26
  ];
27
+ const optionalTickIntervals = {
28
+ halfYear: [utcMonth, 6, 6 * MONTH],
29
+ };
24
30
  // utcDay.every(2) resets its day counter at the start of each month (field = getUTCDate() - 1),
25
31
  // so in a 31-day month the last tick lands on day 31 and the next tick is day 1 of the following
26
32
  // month — only 1 day apart. Filtering by absolute Unix day number avoids the monthly reset.
27
33
  const utcEvery2Days = utcDay.filter((d) => Math.floor(d.getTime() / DAY) % 2 === 0);
28
- function getDateTimeTickInterval(start, stop, count) {
34
+ function buildTickIntervals(formats) {
35
+ if (!formats) {
36
+ return baseTickIntervals;
37
+ }
38
+ const extras = Object.keys(formats).flatMap((key) => {
39
+ const entry = optionalTickIntervals[key];
40
+ return entry ? [entry] : [];
41
+ });
42
+ if (extras.length === 0) {
43
+ return baseTickIntervals;
44
+ }
45
+ return [...baseTickIntervals, ...extras].sort((a, b) => a[2] - b[2]);
46
+ }
47
+ function getDateTimeTickInterval(start, stop, count, formats) {
48
+ const intervals = buildTickIntervals(formats);
29
49
  const target = Math.abs(stop - start) / count;
30
- const i = bisector(([, , step]) => step).right(tickIntervals, target);
31
- if (i === tickIntervals.length) {
50
+ const i = bisector(([, , step]) => step).right(intervals, target);
51
+ if (i === intervals.length) {
32
52
  return utcYear.every(tickStep(start / YEAR, stop / YEAR, count));
33
53
  }
34
54
  if (i === 0) {
35
55
  return utcMillisecond.every(Math.max(tickStep(start, stop, count), 1));
36
56
  }
37
- const [t, step] = tickIntervals[target / tickIntervals[i - 1][2] < tickIntervals[i][2] / target ? i - 1 : i];
57
+ const [t, step] = intervals[target / intervals[i - 1][2] < intervals[i][2] / target ? i - 1 : i];
38
58
  if (t === utcDay && step === 2) {
39
59
  return utcEvery2Days;
40
60
  }
@@ -47,12 +67,12 @@ function getDateTimeTickInterval(start, stop, count) {
47
67
  * weeks are considered to start on Monday (ISO 8601 standard)
48
68
  * instead of Sunday (d3 default).
49
69
  */
50
- export function getDateTimeTicks(start, stop, count = 10) {
70
+ export function getDateTimeTicks(start, stop, count = 10, formats) {
51
71
  const reverse = stop < start;
52
72
  if (reverse) {
53
73
  [start, stop] = [stop, start];
54
74
  }
55
- const interval = getDateTimeTickInterval(start.getTime(), stop.getTime(), count);
75
+ const interval = getDateTimeTickInterval(start.getTime(), stop.getTime(), count, formats);
56
76
  const ticks = interval ? interval.range(start, new Date(Number(stop) + 1)) : [];
57
77
  return reverse ? ticks.reverse() : ticks;
58
78
  }
@@ -1,6 +1,8 @@
1
1
  import type { AxisDomain, AxisScale } from 'd3-axis';
2
2
  import type { ChartScale } from '../../../hooks';
3
- export declare function getScaleTicks({ scale, ticksCount, }: {
3
+ import { getDateTimeTicks } from './datetime';
4
+ export declare function getScaleTicks({ scale, ticksCount, dateTimeLabelFormats, }: {
4
5
  scale: ChartScale | AxisScale<AxisDomain>;
5
6
  ticksCount?: number;
7
+ dateTimeLabelFormats?: Parameters<typeof getDateTimeTicks>[3];
6
8
  }): string[] | number[] | Date[];
@@ -1,5 +1,5 @@
1
1
  import { getDateTimeTicks } from './datetime';
2
- export function getScaleTicks({ scale, ticksCount, }) {
2
+ export function getScaleTicks({ scale, ticksCount, dateTimeLabelFormats, }) {
3
3
  const scaleDomain = scale.domain();
4
4
  switch (typeof scaleDomain[0]) {
5
5
  case 'number': {
@@ -7,7 +7,7 @@ export function getScaleTicks({ scale, ticksCount, }) {
7
7
  }
8
8
  // datetime scale
9
9
  case 'object': {
10
- return getDateTimeTicks(scaleDomain[0], scaleDomain[scaleDomain.length - 1], ticksCount);
10
+ return getDateTimeTicks(scaleDomain[0], scaleDomain[scaleDomain.length - 1], ticksCount, dateTimeLabelFormats);
11
11
  }
12
12
  case 'string': {
13
13
  return scaleDomain;
@@ -6,6 +6,8 @@ export declare const TIME_UNITS: {
6
6
  readonly day: number;
7
7
  readonly week: number;
8
8
  readonly month: number;
9
+ readonly quarter: number;
10
+ readonly halfYear: number;
9
11
  readonly year: number;
10
12
  };
11
13
  export type TimeUnit = keyof typeof TIME_UNITS;
@@ -6,6 +6,8 @@ export const TIME_UNITS = {
6
6
  day: 24 * 3600000,
7
7
  week: 7 * 24 * 3600000,
8
8
  month: 28 * 24 * 3600000,
9
+ quarter: 62 * 24 * 3600000,
10
+ halfYear: 160 * 24 * 3600000,
9
11
  year: 364 * 24 * 3600000,
10
12
  };
11
13
  export const DATETIME_LABEL_FORMATS = {
@@ -16,6 +18,8 @@ export const DATETIME_LABEL_FORMATS = {
16
18
  day: 'DD.MM.YY',
17
19
  week: 'DD.MM.YY',
18
20
  month: "MMM 'YY",
21
+ quarter: "MMM 'YY",
22
+ halfYear: "MMM 'YY",
19
23
  year: 'YYYY',
20
24
  };
21
25
  function getTimeUnit(range) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gravity-ui/charts",
3
- "version": "1.48.3",
3
+ "version": "1.49.0",
4
4
  "description": "A flexible JavaScript library for data visualization and chart rendering using React",
5
5
  "license": "MIT",
6
6
  "main": "dist/cjs/index.js",