@devexperts/dxcharts-lite 2.7.31 → 2.7.33

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 (26) hide show
  1. package/dist/chart/components/chart/price-formatters/__tests__/price.formatter.test.d.ts +6 -0
  2. package/dist/chart/components/chart/price-formatters/__tests__/price.formatter.test.js +150 -0
  3. package/dist/chart/components/chart/price-formatters/price.formatter.d.ts +1 -1
  4. package/dist/chart/components/chart/price-formatters/price.formatter.js +10 -3
  5. package/dist/chart/components/chart/price-formatters/treasury-price.formatter.d.ts +1 -1
  6. package/dist/chart/components/chart/price-formatters/treasury-price.formatter.js +7 -2
  7. package/dist/chart/components/cross_tool/types/cross-and-labels.drawer.js +1 -1
  8. package/dist/chart/components/highlights/highlights.drawer.d.ts +3 -4
  9. package/dist/chart/components/highlights/highlights.drawer.js +38 -29
  10. package/dist/chart/components/pane/extent/y-extent-component.d.ts +7 -2
  11. package/dist/chart/components/pane/extent/y-extent-component.js +9 -5
  12. package/dist/chart/components/pane/pane.component.d.ts +7 -5
  13. package/dist/chart/components/pane/pane.component.js +15 -4
  14. package/dist/chart/components/volumes/separate-volumes.component.d.ts +3 -1
  15. package/dist/chart/components/volumes/separate-volumes.component.js +19 -3
  16. package/dist/chart/components/volumes/volumes.component.d.ts +2 -0
  17. package/dist/chart/components/volumes/volumes.component.js +9 -0
  18. package/dist/chart/components/y_axis/price_labels/data-series-y-axis-labels.provider.js +1 -1
  19. package/dist/chart/components/y_axis/price_labels/last-candle-labels.provider.js +4 -1
  20. package/dist/chart/model/hit-test-canvas.model.d.ts +1 -1
  21. package/dist/chart/model/hit-test-canvas.model.js +1 -0
  22. package/dist/chart/model/scale.model.js +4 -0
  23. package/dist/chart/utils/math.utils.d.ts +2 -0
  24. package/dist/chart/utils/math.utils.js +8 -1
  25. package/dist/dxchart.min.js +4 -4
  26. package/package.json +7 -7
@@ -0,0 +1,6 @@
1
+ /*
2
+ * Copyright (C) 2019 - 2026 Devexperts Solutions IE Limited
3
+ * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
4
+ * If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
5
+ */
6
+ export {};
@@ -0,0 +1,150 @@
1
+ /*
2
+ * Copyright (C) 2019 - 2026 Devexperts Solutions IE Limited
3
+ * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
4
+ * If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
5
+ */
6
+ import { getDefaultConfig } from '../../../../chart.config';
7
+ import { PriceIncrementsUtils } from '../../../../utils/price-increments.utils';
8
+ import { createPercentFormatter, createRegularPriceFormatter } from '../price.formatter';
9
+ const DEFAULT_PRICE_PRECISIONS = PriceIncrementsUtils.computePrecisions([0.01, 1, 0.1, 10, 0.01]);
10
+ const DEFAULT_INTL_FORMATTER = getDefaultConfig().intlFormatter;
11
+ const DEFAULT_Y_AXIS_CONFIG = getDefaultConfig().components.yAxis;
12
+ const createMockDataSeries = (baseline, pricePrecisions = DEFAULT_PRICE_PRECISIONS) => ({
13
+ pricePrecisions,
14
+ getBaseline: () => baseline,
15
+ });
16
+ const createMockExtent = ({ intlFormatter = DEFAULT_INTL_FORMATTER, pricePrecisions = DEFAULT_PRICE_PRECISIONS, dataSeries, } = {}) => ({
17
+ dataSeries: dataSeries !== null && dataSeries !== void 0 ? dataSeries : new Set([{ pricePrecisions }]),
18
+ paneComponent: {
19
+ intlFormatter,
20
+ },
21
+ });
22
+ const createRegularFormatter = (options, yAxisConfig = createRegularYAxisConfig()) => {
23
+ // @ts-expect-error partial YExtentComponent mock for price formatter tests
24
+ return createRegularPriceFormatter(createMockExtent(options), yAxisConfig);
25
+ };
26
+ const createPercentFormatterFromMock = (options) => {
27
+ // @ts-expect-error partial YExtentComponent mock for price formatter tests
28
+ return createPercentFormatter(createMockExtent(options));
29
+ };
30
+ const createTreasuryYAxisConfig = () => (Object.assign(Object.assign({}, DEFAULT_Y_AXIS_CONFIG), { treasuryFormat: { enabled: true } }));
31
+ const createRegularYAxisConfig = () => (Object.assign(Object.assign({}, DEFAULT_Y_AXIS_CONFIG), { treasuryFormat: { enabled: false } }));
32
+ describe('createRegularPriceFormatter', () => {
33
+ describe('input coercion', () => {
34
+ it('should format numeric values', () => {
35
+ const formatter = createRegularFormatter();
36
+ expect(formatter(123.45)).toBe('123.45');
37
+ });
38
+ it('should parse string values without separators by default', () => {
39
+ const formatter = createRegularFormatter({
40
+ intlFormatter: { decimalSeparator: ',', thousandsSeparator: '_' },
41
+ });
42
+ expect(formatter('3456.78')).toBe('3456.78');
43
+ });
44
+ it('should return em dash for falsy non-string values', () => {
45
+ const formatter = createRegularFormatter();
46
+ expect(formatter(undefined)).toBe('—');
47
+ expect(formatter(null)).toBe('—');
48
+ });
49
+ });
50
+ describe('thousands and decimal separators', () => {
51
+ it('should not apply thousandsSeparator by default for values of 1000 or more', () => {
52
+ const formatter = createRegularFormatter();
53
+ expect(formatter(1234.5)).toBe('1234.50');
54
+ expect(formatter(1234567.89)).toBe('1234567.89');
55
+ expect(formatter(1234567890.12)).toBe('1234567890.12');
56
+ });
57
+ it('should use toFixed without thousands grouping for values below 1000', () => {
58
+ const formatter = createRegularFormatter();
59
+ expect(formatter(999.99)).toBe('999.99');
60
+ });
61
+ it('should support custom decimalSeparator and thousandsSeparator when formatWithIntlSeparators is true', () => {
62
+ const atWithSpaceFormatter = createRegularFormatter({
63
+ intlFormatter: { decimalSeparator: '@', thousandsSeparator: ' ' },
64
+ });
65
+ const enUSFormatter = createRegularFormatter({
66
+ intlFormatter: { decimalSeparator: '.', thousandsSeparator: ',' },
67
+ });
68
+ expect(enUSFormatter(1234.5, true)).toBe('1,234.50');
69
+ expect(enUSFormatter(1234567.89, true)).toBe('1,234,567.89');
70
+ expect(enUSFormatter(1234567890.12, true)).toBe('1,234,567,890.12');
71
+ expect(atWithSpaceFormatter(1234.5, true)).toBe('1 234@50');
72
+ });
73
+ });
74
+ describe('treasury formatting', () => {
75
+ it('should format treasury prices when treasury format is enabled', () => {
76
+ const formatter = createRegularFormatter(undefined, createTreasuryYAxisConfig());
77
+ expect(formatter(132.0625)).toBe("132'02");
78
+ });
79
+ it('should not group treasury integer part below 1000 even with thousandsSeparator', () => {
80
+ const formatter = createRegularFormatter({ intlFormatter: { decimalSeparator: '.', thousandsSeparator: ',' } }, createTreasuryYAxisConfig());
81
+ expect(formatter(999.99)).toBe("999'31");
82
+ });
83
+ it('should support custom thousandsSeparator in treasury format when formatWithIntlSeparators is true', () => {
84
+ const commaSeparatorFormatter = createRegularFormatter({ intlFormatter: { thousandsSeparator: ',' } }, createTreasuryYAxisConfig());
85
+ const spaceSeparatorFormatter = createRegularFormatter({ intlFormatter: { thousandsSeparator: ' ' } }, createTreasuryYAxisConfig());
86
+ expect(spaceSeparatorFormatter(1234567.5, true)).toBe("1 234 567'16");
87
+ expect(commaSeparatorFormatter(1234567.5, true)).toBe("1,234,567'16");
88
+ });
89
+ });
90
+ describe('data series availability', () => {
91
+ it('should return stringified value when main data series is missing', () => {
92
+ const formatter = createRegularFormatter({ dataSeries: new Set() });
93
+ expect(formatter(1234.56)).toBe('1234.56');
94
+ });
95
+ });
96
+ describe('optional edge cases — review whether to keep', () => {
97
+ // Boundary: exactly 1000 triggers thousands formatting path when formatWithIntlSeparators is true.
98
+ it('should apply thousandsSeparator at exactly 1000 when formatWithIntlSeparators is true', () => {
99
+ const formatter = createRegularFormatter({
100
+ intlFormatter: { decimalSeparator: '.', thousandsSeparator: ',' },
101
+ });
102
+ expect(formatter(1000, true)).toBe('1,000.00');
103
+ });
104
+ // Current implementation uses `checkedValue >= 1000`, not absolute value.
105
+ it('should apply thousandsSeparator for negative values above 1000 threshold check', () => {
106
+ const formatter = createRegularFormatter({
107
+ intlFormatter: { decimalSeparator: '.', thousandsSeparator: ',' },
108
+ });
109
+ expect(formatter(-1500, true)).toBe('-1,500.00');
110
+ });
111
+ // Treasury formatting takes precedence over intl thousands/decimal separators.
112
+ it('should prefer treasury formatting over thousandsSeparator config', () => {
113
+ const formatter = createRegularFormatter({ intlFormatter: { decimalSeparator: ',', thousandsSeparator: ' ' } }, createTreasuryYAxisConfig());
114
+ expect(formatter(132.0625)).toBe("132'02");
115
+ });
116
+ // Invalid numeric strings become NaN and are stringified by the fallback path.
117
+ it('should stringify NaN when data series is missing and input is not numeric', () => {
118
+ const formatter = createRegularFormatter({ dataSeries: new Set() });
119
+ expect(formatter('not-a-number')).toBe('NaN');
120
+ });
121
+ });
122
+ });
123
+ describe('createPercentFormatter', () => {
124
+ describe('basic formatting', () => {
125
+ it('should return zero percent for zero value', () => {
126
+ const formatter = createPercentFormatterFromMock({ dataSeries: new Set() });
127
+ expect(formatter(0)).toBe('0.00 %');
128
+ });
129
+ it('should format value without conversion when no series is available', () => {
130
+ const formatter = createPercentFormatterFromMock({ dataSeries: new Set() });
131
+ expect(formatter(5)).toBe('5.00 %');
132
+ });
133
+ it('should convert value using main data series baseline', () => {
134
+ const formatter = createPercentFormatterFromMock({
135
+ dataSeries: new Set([createMockDataSeries(100)]),
136
+ });
137
+ expect(formatter(110)).toBe('10.00 %');
138
+ });
139
+ it('should convert value using explicitly passed data series', () => {
140
+ const formatter = createPercentFormatterFromMock({ dataSeries: new Set() });
141
+ const series = createMockDataSeries(200);
142
+ // @ts-expect-error partial DataSeriesModel mock for price formatter tests
143
+ expect(formatter(250, series)).toBe('25.00 %');
144
+ });
145
+ it('should normalize negative zero percent display', () => {
146
+ const formatter = createPercentFormatterFromMock({ dataSeries: new Set() });
147
+ expect(formatter(-0.0001)).toBe('0.00 %');
148
+ });
149
+ });
150
+ });
@@ -8,6 +8,6 @@ import { DataSeriesModel } from '../../../model/data-series.model';
8
8
  import { Unit } from '../../../model/scaling/viewport.model';
9
9
  import { YExtentComponent } from '../../pane/extent/y-extent-component';
10
10
  import { YExtentFormatters } from '../../pane/pane.component';
11
- export declare const createRegularPriceFormatter: (extent: YExtentComponent, config: YAxisConfig) => (value: unknown) => string;
11
+ export declare const createRegularPriceFormatter: (extent: YExtentComponent, config: YAxisConfig) => (value: unknown, shouldAddSeparators?: boolean) => string;
12
12
  export declare const createPercentFormatter: (extent: YExtentComponent) => (value: Unit, dataSeries?: DataSeriesModel) => string;
13
13
  export declare const createYExtentFormatters: (extent: YExtentComponent, config: YAxisConfig) => YExtentFormatters;
@@ -4,11 +4,11 @@
4
4
  * If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
5
5
  */
6
6
  import { unitToPercent } from '../../../model/scaling/viewport.model';
7
- import { replaceMinusSign } from '../../../utils/math.utils';
7
+ import { MathUtils, replaceMinusSign } from '../../../utils/math.utils';
8
8
  import { PriceIncrementsUtils } from '../../../utils/price-increments.utils';
9
9
  import { MINUS_SIGN } from '../../../utils/symbol-constants';
10
10
  import { treasuryPriceFormatter } from './treasury-price.formatter';
11
- export const createRegularPriceFormatter = (extent, config) => (value) => {
11
+ export const createRegularPriceFormatter = (extent, config) => (value, shouldAddSeparators = false) => {
12
12
  let checkedValue;
13
13
  if (typeof value === 'number') {
14
14
  checkedValue = value;
@@ -19,13 +19,20 @@ export const createRegularPriceFormatter = (extent, config) => (value) => {
19
19
  else {
20
20
  return '—';
21
21
  }
22
+ const separators = extent.paneComponent.intlFormatter;
23
+ const formatWithIntlSeparators = (precision) => {
24
+ return MathUtils.makeDecimal(checkedValue, precision, separators.decimalSeparator, separators.thousandsSeparator);
25
+ };
22
26
  const treasuryFormatConfig = config.treasuryFormat;
23
27
  if (treasuryFormatConfig && treasuryFormatConfig.enabled) {
24
- return treasuryPriceFormatter(checkedValue);
28
+ return treasuryPriceFormatter(checkedValue, shouldAddSeparators ? separators.thousandsSeparator : undefined);
25
29
  }
26
30
  const [mainDataSeries] = extent.dataSeries;
27
31
  if (mainDataSeries !== undefined) {
28
32
  const precision = PriceIncrementsUtils.getPricePrecision(checkedValue, mainDataSeries.pricePrecisions);
33
+ if (shouldAddSeparators && Math.abs(checkedValue) >= 1000) {
34
+ return formatWithIntlSeparators(precision);
35
+ }
29
36
  return checkedValue.toFixed(precision);
30
37
  }
31
38
  return `${checkedValue}`;
@@ -13,7 +13,7 @@
13
13
  *
14
14
  */
15
15
  export declare const TREASURY_32ND: number;
16
- export declare const treasuryPriceFormatter: (value: number) => string;
16
+ export declare const treasuryPriceFormatter: (value: number, thousandsSeparator?: string) => string;
17
17
  export declare const isTreasuryPriceFormat: (value: string) => boolean;
18
18
  /**
19
19
  * Parses treasury price format back to decimal number
@@ -3,6 +3,7 @@
3
3
  * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
4
4
  * If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
5
5
  */
6
+ import { MathUtils } from '../../../utils/math.utils';
6
7
  /**
7
8
  * Formats price values for US Treasury contracts in 32nds format
8
9
  *
@@ -13,8 +14,12 @@
13
14
  *
14
15
  */
15
16
  export const TREASURY_32ND = 1 / 32; // 0.03125
16
- export const treasuryPriceFormatter = (value) => {
17
+ export const treasuryPriceFormatter = (value, thousandsSeparator) => {
17
18
  const integerValue = Math.floor(value);
19
+ // Format the integer part with the thousands separator if provided
20
+ const formattedInteger = thousandsSeparator !== undefined
21
+ ? MathUtils.formatIntegerWithSeparator(integerValue, thousandsSeparator)
22
+ : integerValue.toString();
18
23
  // Get the decimal part and convert to 64ths
19
24
  const decimalPart = value - integerValue;
20
25
  const sixtyFourths = Math.round(decimalPart * 64);
@@ -22,7 +27,7 @@ export const treasuryPriceFormatter = (value) => {
22
27
  const thirtySeconds = Math.floor(sixtyFourths / 2);
23
28
  // Format the 32nds part to always show 2 digits (00-31)
24
29
  const thirtySecondsFormatted = thirtySeconds.toString().padStart(2, '0');
25
- return `${integerValue}'${thirtySecondsFormatted}`;
30
+ return `${formattedInteger}'${thirtySecondsFormatted}`;
26
31
  };
27
32
  const getTreasuryPriceMatch = (value) => value.match(/^\-?0*(\d+)'(\d{2})$/);
28
33
  export const isTreasuryPriceFormat = (value) => Boolean(getTreasuryPriceMatch(value));
@@ -132,7 +132,7 @@ export class CrossAndLabelsDrawerType {
132
132
  }
133
133
  for (const extent of pane.yExtentComponents) {
134
134
  const price = extent.regularValueFromY(y);
135
- const label = extent.valueFormatter(price);
135
+ const label = extent.valueFormatter(price, { formatWithSeparators: true });
136
136
  const bounds = this.canvasBoundsContainer.getBounds(CanvasElement.PANE_UUID_Y_AXIS(pane.uuid, extent.idx));
137
137
  const drawYLabel = priceLabelDrawersMap[type];
138
138
  const textColor = type === 'plain' ? crossToolColors.lineColor : crossToolColors.labelTextColor;
@@ -28,10 +28,9 @@ export declare class HighlightsDrawer implements Drawer {
28
28
  * chartComponent.draw();
29
29
  */
30
30
  draw(): void;
31
- private findHighlightStartIndex;
32
- private resolveHighlightFromCandle;
33
- private resolveHighlightToCandle;
34
- private getHighlightEndLookupTimestamp;
31
+ private getHighlightBoundaryX;
32
+ private findCandleIndicesInHighlight;
33
+ private isCandleTimestampInHighlight;
35
34
  /**
36
35
  * Calculates the position of the highlight label based on the given parameters.
37
36
  * @param {HighlightTextPlacement} placement - The placement of the highlight text.
@@ -8,7 +8,6 @@ import { HIGHLIGHTS_TYPES } from './highlights.model';
8
8
  import { CanvasElement } from '../../canvas/canvas-bounds-container';
9
9
  import { unitToPixels } from '../../model/scaling/viewport.model';
10
10
  import { clipToBounds } from '../../utils/canvas/canvas-drawing-functions.utils';
11
- import { getCandleStart, searchCandleIndex } from '../../utils/candles.utils';
12
11
  const LABEL_PADDINGS = [20, 10];
13
12
  export class HighlightsDrawer {
14
13
  constructor(highlightsModel, chartModel, canvasModel, canvasBoundsContainer, config) {
@@ -64,21 +63,24 @@ export class HighlightsDrawer {
64
63
  ctx.strokeStyle = strokeStyle;
65
64
  items.forEach(item => {
66
65
  var _a, _b, _c;
67
- const period = this.chartModel.chartBaseModel.period;
68
66
  const anchor = this.chartModel.getCandleTimestampAnchor();
69
67
  const candles = this.chartModel.getCandles();
70
- const fromXCandle = this.resolveHighlightFromCandle(item.from, candles, period, anchor);
71
- const fromXCandleWidth = unitToPixels(fromXCandle.width, this.chartModel.scale.zoomX);
72
- const fromX = fromXCandle.xStart(this.chartModel.scale);
73
- const toXCandle = this.resolveHighlightToCandle(item.to, candles, period, anchor);
74
- const toXCandleWidth = unitToPixels(toXCandle.width, this.chartModel.scale.zoomX);
75
- const toX = toXCandle.xStart(this.chartModel.scale) + toXCandleWidth;
76
- // draw highlight' borders
68
+ const inRange = this.findCandleIndicesInHighlight(candles, item.from, item.to);
69
+ const highlightFromX = this.getHighlightBoundaryX(item.from, anchor);
70
+ const highlightToX = this.getHighlightBoundaryX(item.to, anchor);
71
+ let fillFromX = highlightFromX;
72
+ let fillToX = highlightToX;
77
73
  if (item.border) {
78
- this.drawBorders(item.border, ctx, fromX + fromXCandleWidth, toX - toXCandleWidth, chartBounds);
74
+ this.drawBorders(item.border, ctx, highlightFromX, highlightToX, chartBounds);
75
+ }
76
+ if (inRange) {
77
+ const fromXCandle = this.chartModel.candleFromIdx(inRange.startIdx);
78
+ const toXCandle = this.chartModel.candleFromIdx(inRange.endIdx);
79
+ fillFromX = fromXCandle.xStart(this.chartModel.scale);
80
+ const toXCandleWidth = unitToPixels(toXCandle.width, this.chartModel.scale.zoomX);
81
+ fillToX = toXCandle.xStart(this.chartModel.scale) + toXCandleWidth;
82
+ ctx.fillRect(fillFromX, chartBounds.y, fillToX - fillFromX, chartBounds.y + chartBounds.height);
79
83
  }
80
- // draw highlight' background
81
- ctx.fillRect(fromX, chartBounds.y, toX - fromX, chartBounds.y + chartBounds.height);
82
84
  // draw highlight' label
83
85
  if (item.label) {
84
86
  const label = (_a = item.label.text) !== null && _a !== void 0 ? _a : '';
@@ -86,7 +88,7 @@ export class HighlightsDrawer {
86
88
  ctx.save();
87
89
  ctx.fillStyle = (_b = itemColors === null || itemColors === void 0 ? void 0 : itemColors.label) !== null && _b !== void 0 ? _b : '#ffffff';
88
90
  const labelWidth = calculateTextWidth(label, ctx, font);
89
- const [labelX, labelY] = this.resolveHighlightLabelPosition((_c = item.label.placement) !== null && _c !== void 0 ? _c : 'left-left', chartBounds, [fromX, toX], labelWidth);
91
+ const [labelX, labelY] = this.resolveHighlightLabelPosition((_c = item.label.placement) !== null && _c !== void 0 ? _c : 'left-left', chartBounds, [fillFromX, fillToX], labelWidth);
90
92
  ctx.fillText(label, labelX, labelY);
91
93
  ctx.restore();
92
94
  }
@@ -99,26 +101,33 @@ export class HighlightsDrawer {
99
101
  }
100
102
  }
101
103
  }
102
- findHighlightStartIndex(candles, sessionFrom, periodMs, anchor) {
103
- if (anchor === 'open') {
104
- const result = searchCandleIndex(sessionFrom, { extrapolate: false, candleTimestampAnchor: 'open' }, candles, periodMs);
105
- return result.index;
104
+ getHighlightBoundaryX(highlightTimestamp, anchor) {
105
+ const candle = this.chartModel.candleFromTimestamp(highlightTimestamp, { extrapolate: true });
106
+ const width = unitToPixels(candle.width, this.chartModel.scale.zoomX);
107
+ const xStart = candle.xStart(this.chartModel.scale);
108
+ if (anchor === 'close') {
109
+ return xStart + width;
106
110
  }
107
- return candles.findIndex((_, index) => getCandleStart(candles, index, periodMs, 'close') === sessionFrom);
108
- }
109
- resolveHighlightFromCandle(sessionFrom, candles, period, anchor) {
110
- const startIdx = this.findHighlightStartIndex(candles, sessionFrom, period, anchor);
111
- if (startIdx >= 0) {
112
- return this.chartModel.candleFromIdx(startIdx);
111
+ if (candle.candle.timestamp < highlightTimestamp) {
112
+ return xStart + width;
113
113
  }
114
- return this.chartModel.candleFromTimestamp(sessionFrom);
114
+ return xStart;
115
115
  }
116
- resolveHighlightToCandle(sessionTo, candles, period, anchor) {
117
- const lookupTimestamp = this.getHighlightEndLookupTimestamp(sessionTo, period, anchor);
118
- return this.chartModel.candleFromTimestamp(lookupTimestamp);
116
+ findCandleIndicesInHighlight(candles, highlightFrom, highlightTo) {
117
+ let startIdx = -1;
118
+ let endIdx = -1;
119
+ for (let i = 0; i < candles.length; i++) {
120
+ if (this.isCandleTimestampInHighlight(candles[i].timestamp, highlightFrom, highlightTo)) {
121
+ if (startIdx === -1) {
122
+ startIdx = i;
123
+ }
124
+ endIdx = i;
125
+ }
126
+ }
127
+ return startIdx === -1 ? null : { startIdx, endIdx };
119
128
  }
120
- getHighlightEndLookupTimestamp(sessionTo, periodMs, anchor) {
121
- return anchor === 'close' ? sessionTo - 1 : sessionTo - periodMs;
129
+ isCandleTimestampInHighlight(candleTimestamp, highlightFrom, highlightTo) {
130
+ return highlightFrom <= candleTimestamp && candleTimestamp < highlightTo;
122
131
  }
123
132
  /**
124
133
  * Calculates the position of the highlight label based on the given parameters.
@@ -17,6 +17,10 @@ import { DragNDropYComponent } from '../../dran-n-drop_helper/drag-n-drop-y.comp
17
17
  import { YAxisComponent } from '../../y_axis/y-axis.component';
18
18
  import { PaneHitTestController } from '../pane-hit-test.controller';
19
19
  import { PaneComponent, YExtentFormatters } from '../pane.component';
20
+ export type ValueFormatterOptions = Partial<{
21
+ dataSeries: DataSeriesModel;
22
+ formatWithSeparators: boolean;
23
+ }>;
20
24
  export interface YExtentCreationOptions {
21
25
  scale: ScaleModel;
22
26
  order: number;
@@ -73,8 +77,9 @@ export declare class YExtentComponent extends ChartBaseElement {
73
77
  * @returns {void}
74
78
  */
75
79
  removeDataSeries(series: DataSeriesModel): void;
76
- valueFormatter: (value: Unit, dataSeries?: DataSeriesModel) => string;
77
- get regularFormatter(): (value: number, precision?: number) => string;
80
+ valueFormatter: (value: Unit, options?: ValueFormatterOptions) => string;
81
+ private yAxisNumericLabelsFormatter;
82
+ get regularFormatter(): (value: number, formatWithIntlSeparators?: boolean) => string;
78
83
  /**
79
84
  * Sets the pane value formatters for the current instance.
80
85
  * @param {YExtentFormatters} formatters - The pane value formatters to be set.
@@ -40,25 +40,29 @@ export class YExtentComponent extends ChartBaseElement {
40
40
  var _a, _b;
41
41
  return (_b = (_a = this.mainDataSeries) === null || _a === void 0 ? void 0 : _a.view.toY(value)) !== null && _b !== void 0 ? _b : 1;
42
42
  };
43
- this.valueFormatter = (value, dataSeries) => {
43
+ this.valueFormatter = (value, options) => {
44
+ const { dataSeries, formatWithSeparators = false } = options !== null && options !== void 0 ? options : {};
44
45
  if (!this.formatters[this.yAxis.getAxisType()]) {
45
- return this.formatters.regular(value);
46
+ return this.formatters.regular(value, formatWithSeparators);
46
47
  }
47
48
  const { regular, percent, logarithmic } = this.formatters;
48
49
  switch (this.yAxis.getAxisType()) {
49
50
  case 'regular':
50
- return this.formatters.regular(value);
51
+ return this.formatters.regular(value, formatWithSeparators);
51
52
  case 'percent':
52
53
  return percent ? percent(value, dataSeries) : regular(value);
53
54
  case 'logarithmic':
54
55
  return logarithmic ? logarithmic(value) : regular(value);
55
56
  default:
56
- return this.regularFormatter(value);
57
+ return this.regularFormatter(value, formatWithSeparators);
57
58
  }
58
59
  };
60
+ this.yAxisNumericLabelsFormatter = (value) => {
61
+ return this.valueFormatter(value, { formatWithSeparators: true });
62
+ };
59
63
  this.addChildEntity(scale);
60
64
  this.setValueFormatters(createYExtentFormatters(this, config));
61
- this.yAxis = createYAxisComponent(this.valueFormatter.bind(this), () => this.mainDataSeries);
65
+ this.yAxis = createYAxisComponent((value) => this.yAxisNumericLabelsFormatter(value), () => this.mainDataSeries);
62
66
  this.addChildEntity(this.yAxis);
63
67
  }
64
68
  doDeactivate() {
@@ -7,7 +7,7 @@ import { Subject } from 'rxjs';
7
7
  import { CanvasAnimation } from '../../animation/canvas-animation';
8
8
  import { CanvasBoundsContainer, HitBoundsTest } from '../../canvas/canvas-bounds-container';
9
9
  import { CursorHandler } from '../../canvas/cursor.handler';
10
- import { FullChartConfig, YAxisAlign } from '../../chart.config';
10
+ import { FullChartConfig, IntlFormatter, YAxisAlign } from '../../chart.config';
11
11
  import { DrawingManager } from '../../drawers/drawing-manager';
12
12
  import EventBus from '../../events/event-bus';
13
13
  import { CanvasInputListenerComponent } from '../../inputlisteners/canvas-input-listener.component';
@@ -22,7 +22,7 @@ import { ChartBaseModel } from '../chart/chart-base.model';
22
22
  import { PriceAxisType } from '../labels_generator/numeric-axis-labels.generator';
23
23
  import { ChartPanComponent } from '../pan/chart-pan.component';
24
24
  import { YAxisComponent } from '../y_axis/y-axis.component';
25
- import { YExtentComponent, YExtentCreationOptions } from './extent/y-extent-component';
25
+ import { ValueFormatterOptions, YExtentComponent, YExtentCreationOptions } from './extent/y-extent-component';
26
26
  import { PaneHitTestController } from './pane-hit-test.controller';
27
27
  import { HitTestCanvasModel } from '../../model/hit-test-canvas.model';
28
28
  import { ChartResizeHandler } from '../../inputhandlers/chart-resize.handler';
@@ -54,6 +54,7 @@ export declare class PaneComponent extends ChartBaseElement {
54
54
  yExtentComponentsChangedSubject: Subject<void>;
55
55
  get scale(): ScaleModel;
56
56
  get yAxis(): YAxisComponent;
57
+ get intlFormatter(): IntlFormatter;
57
58
  get dataSeries(): DataSeriesModel<import("../../model/data-series.model").DataSeriesPoint, import("../../model/data-series.model").VisualSeriesPoint, import("../../model/data-series.config").DataSeriesConfig>[];
58
59
  get visible(): boolean;
59
60
  mainExtent: YExtentComponent;
@@ -86,6 +87,7 @@ export declare class PaneComponent extends ChartBaseElement {
86
87
  private createYPanHandler;
87
88
  private addCursors;
88
89
  createExtentComponent(options?: AtLeastOne<YExtentCreationOptions>): YExtentComponent;
90
+ private shouldKeepVolumeHostExtent;
89
91
  removeExtentComponents(extentComponents: YExtentComponent[]): void;
90
92
  /**
91
93
  * Create new pane extent and attach data series to it
@@ -159,8 +161,8 @@ export declare class PaneComponent extends ChartBaseElement {
159
161
  * @deprecated Use `paneManager.canMovePaneDown()` instead
160
162
  */
161
163
  canMoveDown(): boolean;
162
- valueFormatter: (value: Unit, dataSeries?: DataSeriesModel) => string;
163
- get regularFormatter(): (value: number, precision?: number) => string;
164
+ valueFormatter: (value: Unit, options?: ValueFormatterOptions) => string;
165
+ get regularFormatter(): (value: number, formatWithIntlSeparators?: boolean) => string;
164
166
  /**
165
167
  * Sets the pane value formatters for the current instance.
166
168
  * @param {YExtentFormatters} formatters - The pane value formatters to be set.
@@ -174,7 +176,7 @@ export declare class PaneComponent extends ChartBaseElement {
174
176
  regularValueFromY(y: number): number;
175
177
  }
176
178
  export interface YExtentFormatters {
177
- regular: (value: number, precision?: number) => string;
179
+ regular: (value: number, formatWithIntlSeparators?: boolean) => string;
178
180
  percent?: (value: number, dataSeries?: DataSeriesModel) => string;
179
181
  logarithmic?: (value: number) => string;
180
182
  }
@@ -15,6 +15,7 @@ import { GridComponent } from '../grid/grid.component';
15
15
  import { YAxisComponent } from '../y_axis/y-axis.component';
16
16
  import { createDefaultYExtentHighLowProvider, YExtentComponent, } from './extent/y-extent-component';
17
17
  import { merge } from '../../utils/merge.utils';
18
+ import { VOLUMES_UUID } from '../volumes/volumes.model';
18
19
  export class PaneComponent extends ChartBaseElement {
19
20
  get scale() {
20
21
  return this.mainExtent.scale;
@@ -22,6 +23,9 @@ export class PaneComponent extends ChartBaseElement {
22
23
  get yAxis() {
23
24
  return this.mainExtent.yAxis;
24
25
  }
26
+ get intlFormatter() {
27
+ return this.config.intlFormatter;
28
+ }
25
29
  get dataSeries() {
26
30
  return flatMap(this.yExtentComponents, c => Array.from(c.dataSeries));
27
31
  }
@@ -54,8 +58,8 @@ export class PaneComponent extends ChartBaseElement {
54
58
  this.getYAxisBounds = () => {
55
59
  return this.canvasBoundsContainer.getBounds(CanvasElement.PANE_UUID_Y_AXIS(this.uuid));
56
60
  };
57
- this.valueFormatter = (value, dataSeries) => {
58
- return this.mainExtent.valueFormatter(value, dataSeries);
61
+ this.valueFormatter = (value, options) => {
62
+ return this.mainExtent.valueFormatter(value, options);
59
63
  };
60
64
  const yExtentComponent = this.createExtentComponent(options);
61
65
  this.mainExtent = yExtentComponent;
@@ -160,9 +164,16 @@ export class PaneComponent extends ChartBaseElement {
160
164
  this.yExtentComponentsChangedSubject.next();
161
165
  return yExtentComponent;
162
166
  }
167
+ shouldKeepVolumeHostExtent(extent) {
168
+ return (this.uuid === VOLUMES_UUID && extent === this.mainExtent && this.config.components.volumes.showSeparately);
169
+ }
163
170
  removeExtentComponents(extentComponents) {
164
- extentComponents.forEach(extentComponent => extentComponent.disable());
165
- this.yExtentComponents = this.yExtentComponents.filter(current => !extentComponents.map(excluded => excluded.idx).includes(current.idx));
171
+ const removableExtents = extentComponents.filter(extent => !this.shouldKeepVolumeHostExtent(extent));
172
+ if (removableExtents.length === 0) {
173
+ return;
174
+ }
175
+ removableExtents.forEach(extentComponent => extentComponent.disable());
176
+ this.yExtentComponents = this.yExtentComponents.filter(current => !removableExtents.map(excluded => excluded.idx).includes(current.idx));
166
177
  // re-index extents
167
178
  this.yExtentComponents.forEach((c, idx) => {
168
179
  c.yAxis.setExtentIdx(idx);
@@ -19,6 +19,7 @@ export declare class SeparateVolumesComponent extends ChartBaseElement {
19
19
  pane: PaneComponent | undefined;
20
20
  constructor(chartComponent: ChartComponent, config: FullChartConfig, volumesModel: VolumesModel, paneManager: PaneManager);
21
21
  protected doActivate(): void;
22
+ private paneHasStudyDataSeries;
22
23
  /**
23
24
  * Activates the separate volumes feature.
24
25
  * If the pane component for separate volumes does not exist, it creates a new pane with the specified UUID and options.
@@ -27,7 +28,8 @@ export declare class SeparateVolumesComponent extends ChartBaseElement {
27
28
  */
28
29
  activateSeparateVolumes(): void;
29
30
  /**
30
- * Deactivates the separate volumes feature by removing the separate volumes pane, deleting the scale model, and removing the underlay volumes area drawer.
31
+ * Deactivates the separate volumes feature by removing the separate volumes pane when it has no study data series,
32
+ * or by keeping the pane when studies remain on it.
31
33
  */
32
34
  deactiveSeparateVolumes(): void;
33
35
  /**
@@ -22,6 +22,9 @@ export class SeparateVolumesComponent extends ChartBaseElement {
22
22
  this.addRxSubscription(this.chartComponent.chartModel.candlesUpdatedSubject.subscribe(() => { var _a, _b; return !((_a = this.pane) === null || _a === void 0 ? void 0 : _a.scale.isViewportValid()) && ((_b = this.pane) === null || _b === void 0 ? void 0 : _b.scale.doAutoScale(true)); }));
23
23
  this.addRxSubscription(this.volumesModel.volumeMax.subscribe(() => { var _a; return (_a = this.pane) === null || _a === void 0 ? void 0 : _a.scale.doAutoScale(); }));
24
24
  }
25
+ paneHasStudyDataSeries(pane) {
26
+ return pane.dataSeries.length > 0;
27
+ }
25
28
  /**
26
29
  * Activates the separate volumes feature.
27
30
  * If the pane component for separate volumes does not exist, it creates a new pane with the specified UUID and options.
@@ -29,7 +32,8 @@ export class SeparateVolumesComponent extends ChartBaseElement {
29
32
  * A new VolumesDrawer is created and added to the drawing manager with the specified parameters.
30
33
  */
31
34
  activateSeparateVolumes() {
32
- if (this.paneManager.panes[SeparateVolumesComponent.UUID] === undefined) {
35
+ const existingPane = this.paneManager.panes[SeparateVolumesComponent.UUID];
36
+ if (existingPane === undefined) {
33
37
  const precision = 1;
34
38
  const volumePane = this.paneManager.createPane(SeparateVolumesComponent.UUID, {
35
39
  paneFormatters: {
@@ -38,18 +42,30 @@ export class SeparateVolumesComponent extends ChartBaseElement {
38
42
  useDefaultHighLow: false,
39
43
  increment: 1,
40
44
  });
41
- this.pane = volumePane;
42
45
  volumePane.mainExtent.yAxis.setAxisType('regular');
43
46
  const { scale: scaleModel } = volumePane;
44
47
  const volumesHighLowProvider = createCandlesOffsetProvider(() => ({ top: 10, bottom: 0, left: 0, right: 0, visible: true }), this.volumesModel.highLowProvider);
45
48
  scaleModel.autoScaleModel.setHighLowProvider('volumes', volumesHighLowProvider);
46
49
  scaleModel.doAutoScale(true);
50
+ this.pane = volumePane;
51
+ return;
47
52
  }
53
+ this.pane = existingPane;
48
54
  }
49
55
  /**
50
- * Deactivates the separate volumes feature by removing the separate volumes pane, deleting the scale model, and removing the underlay volumes area drawer.
56
+ * Deactivates the separate volumes feature by removing the separate volumes pane when it has no study data series,
57
+ * or by keeping the pane when studies remain on it.
51
58
  */
52
59
  deactiveSeparateVolumes() {
60
+ const pane = this.paneManager.panes[SeparateVolumesComponent.UUID];
61
+ if (pane === undefined) {
62
+ delete this.pane;
63
+ return;
64
+ }
65
+ if (this.paneHasStudyDataSeries(pane)) {
66
+ this.pane = undefined;
67
+ return;
68
+ }
53
69
  this.paneManager.removePane(SeparateVolumesComponent.UUID);
54
70
  delete this.pane;
55
71
  }
@@ -40,6 +40,8 @@ export declare class VolumesComponent extends ChartBaseElement {
40
40
  * @param {boolean} separate - A boolean value indicating whether the volumes should be shown separately or not.
41
41
  * @returns {void}
42
42
  */
43
+ /** Restores separate volumes pane and dynamic object after study pane cleanup. */
44
+ ensureSeparateVolumesHost(): void;
43
45
  setShowVolumesSeparatly(separate: boolean): void;
44
46
  /**
45
47
  * This method deactivates the current component by calling the superclass doDeactivate method and setting the visibility of the component to false.
@@ -53,6 +53,15 @@ export class VolumesComponent extends ChartBaseElement {
53
53
  * @param {boolean} separate - A boolean value indicating whether the volumes should be shown separately or not.
54
54
  * @returns {void}
55
55
  */
56
+ /** Restores separate volumes pane and dynamic object after study pane cleanup. */
57
+ ensureSeparateVolumesHost() {
58
+ if (!this.config.components.volumes.showSeparately) {
59
+ return;
60
+ }
61
+ this.separateVolumes.activateSeparateVolumes();
62
+ this.syncVolumesDynamicObject();
63
+ this.canvasModel.fireDraw();
64
+ }
56
65
  setShowVolumesSeparatly(separate) {
57
66
  if (this.config.components.volumes.showSeparately !== separate) {
58
67
  this.config.components.volumes.showSeparately = separate;