@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.
- package/dist/chart/components/chart/price-formatters/__tests__/price.formatter.test.d.ts +6 -0
- package/dist/chart/components/chart/price-formatters/__tests__/price.formatter.test.js +150 -0
- package/dist/chart/components/chart/price-formatters/price.formatter.d.ts +1 -1
- package/dist/chart/components/chart/price-formatters/price.formatter.js +10 -3
- package/dist/chart/components/chart/price-formatters/treasury-price.formatter.d.ts +1 -1
- package/dist/chart/components/chart/price-formatters/treasury-price.formatter.js +7 -2
- package/dist/chart/components/cross_tool/types/cross-and-labels.drawer.js +1 -1
- package/dist/chart/components/highlights/highlights.drawer.d.ts +3 -4
- package/dist/chart/components/highlights/highlights.drawer.js +38 -29
- package/dist/chart/components/pane/extent/y-extent-component.d.ts +7 -2
- package/dist/chart/components/pane/extent/y-extent-component.js +9 -5
- package/dist/chart/components/pane/pane.component.d.ts +7 -5
- package/dist/chart/components/pane/pane.component.js +15 -4
- package/dist/chart/components/volumes/separate-volumes.component.d.ts +3 -1
- package/dist/chart/components/volumes/separate-volumes.component.js +19 -3
- package/dist/chart/components/volumes/volumes.component.d.ts +2 -0
- package/dist/chart/components/volumes/volumes.component.js +9 -0
- package/dist/chart/components/y_axis/price_labels/data-series-y-axis-labels.provider.js +1 -1
- package/dist/chart/components/y_axis/price_labels/last-candle-labels.provider.js +4 -1
- package/dist/chart/model/hit-test-canvas.model.d.ts +1 -1
- package/dist/chart/model/hit-test-canvas.model.js +1 -0
- package/dist/chart/model/scale.model.js +4 -0
- package/dist/chart/utils/math.utils.d.ts +2 -0
- package/dist/chart/utils/math.utils.js +8 -1
- package/dist/dxchart.min.js +4 -4
- 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 `${
|
|
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
|
|
32
|
-
private
|
|
33
|
-
private
|
|
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
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
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,
|
|
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, [
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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
|
|
114
|
+
return xStart;
|
|
115
115
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
121
|
-
return
|
|
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,
|
|
77
|
-
|
|
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,
|
|
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.
|
|
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,
|
|
163
|
-
get regularFormatter(): (value: number,
|
|
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,
|
|
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,
|
|
58
|
-
return this.mainExtent.valueFormatter(value,
|
|
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.
|
|
165
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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;
|