@devexperts/dxcharts-lite 2.7.30 → 2.7.32
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/canvas/canvas-chart-html.js +1 -1
- package/dist/chart/chart.config.d.ts +7 -0
- package/dist/chart/chart.config.js +2 -0
- package/dist/chart/components/chart/chart-base.model.d.ts +2 -0
- package/dist/chart/components/chart/chart-base.model.js +2 -1
- package/dist/chart/components/chart/chart.model.d.ts +2 -0
- package/dist/chart/components/chart/chart.model.js +20 -8
- package/dist/chart/components/cross_tool/cross-tool.model.js +7 -0
- package/dist/chart/components/dynamic-objects/dynamic-objects.model.d.ts +7 -0
- package/dist/chart/components/dynamic-objects/dynamic-objects.model.js +60 -7
- package/dist/chart/components/highlights/highlights.drawer.d.ts +4 -0
- package/dist/chart/components/highlights/highlights.drawer.js +27 -5
- package/dist/chart/components/pane/pane.component.d.ts +1 -0
- package/dist/chart/components/pane/pane.component.js +10 -2
- 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/inputhandlers/hover-producer.component.d.ts +4 -2
- package/dist/chart/inputhandlers/hover-producer.component.js +45 -14
- package/dist/chart/utils/candles.utils.d.ts +4 -0
- package/dist/chart/utils/candles.utils.js +80 -1
- package/dist/dxchart.min.js +5 -5
- package/package.json +1 -1
|
@@ -9,8 +9,8 @@ export default (devexpertsPromoLink) => '<div data-element="chartResizer" style=
|
|
|
9
9
|
'\t\t\t<canvas data-element="snapshotCanvas"></canvas>\n' +
|
|
10
10
|
'\t\t\t<canvas data-element="backgroundCanvas"></canvas>\n' +
|
|
11
11
|
'\t\t\t<canvas data-element="mainCanvas"></canvas>\n' +
|
|
12
|
-
'\t\t\t<canvas data-element="yAxisLabelsCanvas"></canvas>\n' +
|
|
13
12
|
'\t\t\t<canvas data-element="dynamicObjectsCanvas"></canvas>\n' +
|
|
13
|
+
'\t\t\t<canvas data-element="yAxisLabelsCanvas"></canvas>\n' +
|
|
14
14
|
'\t\t\t<canvas data-element="yAxisDescriptionsCanvas"></canvas>\n' +
|
|
15
15
|
'\t\t\t<canvas data-element="crossToolCanvas"></canvas>\n' +
|
|
16
16
|
'\t\t\t<canvas data-element="hitTestCanvas"></canvas>\n' +
|
|
@@ -289,6 +289,8 @@ export interface ChartComponents {
|
|
|
289
289
|
*/
|
|
290
290
|
paneResizer: ChartConfigComponentsPaneResizer;
|
|
291
291
|
}
|
|
292
|
+
export declare const candleTimestampAnchor: readonly ["open", "close"];
|
|
293
|
+
export type CandleTimestampAnchor = (typeof candleTimestampAnchor)[number];
|
|
292
294
|
export interface ChartConfigComponentsChart {
|
|
293
295
|
/**
|
|
294
296
|
* The type of chart. Candle, bar, area and others.
|
|
@@ -335,6 +337,11 @@ export interface ChartConfigComponentsChart {
|
|
|
335
337
|
y: boolean;
|
|
336
338
|
};
|
|
337
339
|
sortCandles?: (candles: Candle[]) => Candle[];
|
|
340
|
+
/**
|
|
341
|
+
* Defines whether `Candle.timestamp` represents candle open (start) or close (end) time.
|
|
342
|
+
* @default 'open'
|
|
343
|
+
*/
|
|
344
|
+
candleTimestampAnchor?: CandleTimestampAnchor;
|
|
338
345
|
}
|
|
339
346
|
export interface ChartConfigComponentsEvents {
|
|
340
347
|
/**
|
|
@@ -81,6 +81,7 @@ export const getDefaultConfig = () => ({
|
|
|
81
81
|
y: true,
|
|
82
82
|
},
|
|
83
83
|
sortCandles: defaultSortCandles,
|
|
84
|
+
candleTimestampAnchor: 'open',
|
|
84
85
|
},
|
|
85
86
|
yAxis: {
|
|
86
87
|
type: 'regular',
|
|
@@ -682,4 +683,5 @@ export function immutableMerge(base, override, options) {
|
|
|
682
683
|
});
|
|
683
684
|
return result;
|
|
684
685
|
}
|
|
686
|
+
export const candleTimestampAnchor = ['open', 'close'];
|
|
685
687
|
export const getFontFromConfig = (config) => `${config.fontSize}px ${config.fontFamily}`;
|
|
@@ -8,6 +8,7 @@ import { Candle } from '../../model/candle.model';
|
|
|
8
8
|
import { DataSeriesPoint, VisualSeriesPoint } from '../../model/data-series.model';
|
|
9
9
|
import { Index, Timestamp } from '../../model/scaling/viewport.model';
|
|
10
10
|
import VisualCandle from '../../model/visual-candle';
|
|
11
|
+
import { CandleTimestampAnchor } from '../../chart.config';
|
|
11
12
|
export type BaseType = 'candle' | 'point';
|
|
12
13
|
type DataPoint<T extends BaseType> = T extends 'candle' ? Candle : DataSeriesPoint;
|
|
13
14
|
type VisualPoint<T extends BaseType> = T extends 'candle' ? VisualCandle : VisualSeriesPoint;
|
|
@@ -35,6 +36,7 @@ export declare class ChartBaseModel<T extends BaseType = 'point'> {
|
|
|
35
36
|
* Candles aggregation period in ms. Required for future candles calculation.
|
|
36
37
|
*/
|
|
37
38
|
period: number;
|
|
39
|
+
candleTimestampAnchor: CandleTimestampAnchor;
|
|
38
40
|
constructor(type: T);
|
|
39
41
|
/**
|
|
40
42
|
* For given timestamp finds the closest candle in dataset.
|
|
@@ -28,6 +28,7 @@ export class ChartBaseModel {
|
|
|
28
28
|
* Candles aggregation period in ms. Required for future candles calculation.
|
|
29
29
|
*/
|
|
30
30
|
this.period = 1;
|
|
31
|
+
this.candleTimestampAnchor = 'open';
|
|
31
32
|
}
|
|
32
33
|
/**
|
|
33
34
|
* For given timestamp finds the closest candle in dataset.
|
|
@@ -35,7 +36,7 @@ export class ChartBaseModel {
|
|
|
35
36
|
*/
|
|
36
37
|
// TODO think how to make this function like candleFromX
|
|
37
38
|
dataFromTimestamp(timestamp, options = { extrapolate: true }, selectedDataPoints = this.mainDataPoints) {
|
|
38
|
-
const result = searchCandleIndex(timestamp, options, selectedDataPoints, this.period);
|
|
39
|
+
const result = searchCandleIndex(timestamp, Object.assign(Object.assign({}, options), { candleTimestampAnchor: this.candleTimestampAnchor }), selectedDataPoints, this.period);
|
|
39
40
|
return this.dataFromIdx(result.index);
|
|
40
41
|
}
|
|
41
42
|
/**
|
|
@@ -370,6 +370,8 @@ export declare class ChartModel extends ChartBaseElement {
|
|
|
370
370
|
*/
|
|
371
371
|
private reindexCandlesBasedOnSeries;
|
|
372
372
|
getPeriod(): number;
|
|
373
|
+
getCandleTimestampAnchor(): "close" | "open";
|
|
374
|
+
private candleSearchOptions;
|
|
373
375
|
/**
|
|
374
376
|
* Checks if a given candle is within the viewport.
|
|
375
377
|
* @param {number} idx - The index of the candle to check.
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { Subject, merge } from 'rxjs';
|
|
7
7
|
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
|
|
8
8
|
import { CHART_UUID, CanvasElement, areBoundsChanged, } from '../../canvas/canvas-bounds-container';
|
|
9
|
-
import { getDefaultConfig } from '../../chart.config';
|
|
9
|
+
import { getDefaultConfig, } from '../../chart.config';
|
|
10
10
|
import { CandleSeriesModel } from '../../model/candle-series.model';
|
|
11
11
|
import { copyCandle } from '../../model/candle.model';
|
|
12
12
|
import { MIN_SUPPORTED_CANVAS_SIZE } from '../../model/canvas.model';
|
|
@@ -66,6 +66,7 @@ export class ChartModel extends ChartBaseElement {
|
|
|
66
66
|
return sortCandles ? sortCandles(prepared) : prepared;
|
|
67
67
|
};
|
|
68
68
|
this.chartTypeChanged.next(this.config.components.chart.type);
|
|
69
|
+
this.chartBaseModel.candleTimestampAnchor = this.getCandleTimestampAnchor();
|
|
69
70
|
this.secondaryChartColors = new SecondaryChartColorsPool(this.config);
|
|
70
71
|
const candleSeries = new MainCandleSeriesModel(this.chartBaseModel, this.paneManager.panes[CHART_UUID].mainExtent, uuid(), this.paneManager.hitTestController.getNewDataSeriesHitTestId(), this.bus, this.scale, new ChartInstrument(), this.candlesTransformersByChartType, this.candleWidthByChartType, Object.assign({}, this.config.colors));
|
|
71
72
|
candleSeries.config.type = this.config.components.chart.type;
|
|
@@ -305,7 +306,7 @@ export class ChartModel extends ChartBaseElement {
|
|
|
305
306
|
return;
|
|
306
307
|
}
|
|
307
308
|
const preparedCandles = this.prepareCandles(mainSeries.candles);
|
|
308
|
-
const updateResult = updateCandles(this.mainCandleSeries.dataPoints, preparedCandles);
|
|
309
|
+
const updateResult = updateCandles(this.mainCandleSeries.dataPoints, preparedCandles, this.getCandleTimestampAnchor());
|
|
309
310
|
const updatedCandles = updateResult.candles;
|
|
310
311
|
reindexCandles(updatedCandles);
|
|
311
312
|
this.mainCandleSeries.dataPoints = updatedCandles;
|
|
@@ -313,7 +314,7 @@ export class ChartModel extends ChartBaseElement {
|
|
|
313
314
|
secondarySeries.map(series => {
|
|
314
315
|
var _a, _b, _c, _d;
|
|
315
316
|
const preparedCandles = this.prepareCandles(series.candles);
|
|
316
|
-
const updatedCandles = updateCandles((_d = (_c = this.findSecondarySeriesBySymbol((_b = (_a = series.instrument) === null || _a === void 0 ? void 0 : _a.symbol) !== null && _b !== void 0 ? _b : '')) === null || _c === void 0 ? void 0 : _c.dataPoints) !== null && _d !== void 0 ? _d : [], preparedCandles).candles;
|
|
317
|
+
const updatedCandles = updateCandles((_d = (_c = this.findSecondarySeriesBySymbol((_b = (_a = series.instrument) === null || _a === void 0 ? void 0 : _a.symbol) !== null && _b !== void 0 ? _b : '')) === null || _c === void 0 ? void 0 : _c.dataPoints) !== null && _d !== void 0 ? _d : [], preparedCandles, this.getCandleTimestampAnchor()).candles;
|
|
317
318
|
return this.setSecondaryCandleSeries(updatedCandles, series.instrument, false);
|
|
318
319
|
});
|
|
319
320
|
// do visual recalculations
|
|
@@ -738,7 +739,7 @@ export class ChartModel extends ChartBaseElement {
|
|
|
738
739
|
return series.reduce((candles, candle) => {
|
|
739
740
|
const timestamp = candle.timestamp;
|
|
740
741
|
// find index of candle in baseSeries
|
|
741
|
-
const result = searchCandleIndex(timestamp,
|
|
742
|
+
const result = searchCandleIndex(timestamp, this.candleSearchOptions(false), baseSeries, this.chartBaseModel.period);
|
|
742
743
|
if (result.index >= 0 && result.index < baseSeries.length) {
|
|
743
744
|
candle.idx = result.index;
|
|
744
745
|
candles[result.index] = candle;
|
|
@@ -749,6 +750,17 @@ export class ChartModel extends ChartBaseElement {
|
|
|
749
750
|
getPeriod() {
|
|
750
751
|
return this.chartBaseModel.period;
|
|
751
752
|
}
|
|
753
|
+
getCandleTimestampAnchor() {
|
|
754
|
+
var _a;
|
|
755
|
+
return (_a = this.config.components.chart.candleTimestampAnchor) !== null && _a !== void 0 ? _a : 'open';
|
|
756
|
+
}
|
|
757
|
+
candleSearchOptions(extrapolate, isDaysPeriod) {
|
|
758
|
+
return {
|
|
759
|
+
extrapolate,
|
|
760
|
+
isDaysPeriod,
|
|
761
|
+
candleTimestampAnchor: this.getCandleTimestampAnchor(),
|
|
762
|
+
};
|
|
763
|
+
}
|
|
752
764
|
/**
|
|
753
765
|
* Checks if a given candle is within the viewport.
|
|
754
766
|
* @param {number} idx - The index of the candle to check.
|
|
@@ -793,7 +805,7 @@ export class ChartModel extends ChartBaseElement {
|
|
|
793
805
|
return;
|
|
794
806
|
}
|
|
795
807
|
// detect index of updating candle
|
|
796
|
-
const result = searchCandleIndex(candle.timestamp,
|
|
808
|
+
const result = searchCandleIndex(candle.timestamp, this.candleSearchOptions(true), curCandles, this.getPeriod());
|
|
797
809
|
const idx = Math.min(result.index, curCandles.length);
|
|
798
810
|
isNewCandle = isNewCandle || idx === curCandles.length;
|
|
799
811
|
// update the candle and index
|
|
@@ -898,7 +910,7 @@ export class ChartModel extends ChartBaseElement {
|
|
|
898
910
|
const targetCopy = target.slice();
|
|
899
911
|
const prepend = [];
|
|
900
912
|
prependUpdate.forEach(c => {
|
|
901
|
-
const result = searchCandleIndex(c.timestamp,
|
|
913
|
+
const result = searchCandleIndex(c.timestamp, this.candleSearchOptions(false), target);
|
|
902
914
|
const idx = result.index;
|
|
903
915
|
if (idx < 0) {
|
|
904
916
|
prepend.push(c);
|
|
@@ -1027,12 +1039,12 @@ const findFirstNotEmptyCandle = (candles, startIdx, iterateStep) => {
|
|
|
1027
1039
|
* @param target {Candle[]} - sorted candles
|
|
1028
1040
|
* @param update {Candle[]} - sorted candles
|
|
1029
1041
|
*/
|
|
1030
|
-
const updateCandles = (target, update) => {
|
|
1042
|
+
const updateCandles = (target, update, candleTimestampAnchor = 'open') => {
|
|
1031
1043
|
const targetCopy = target.slice();
|
|
1032
1044
|
const prepend = [];
|
|
1033
1045
|
const append = [];
|
|
1034
1046
|
update.forEach(c => {
|
|
1035
|
-
const result = searchCandleIndex(c.timestamp, { extrapolate: true }, target);
|
|
1047
|
+
const result = searchCandleIndex(c.timestamp, { extrapolate: true, candleTimestampAnchor }, target);
|
|
1036
1048
|
const idx = result.index;
|
|
1037
1049
|
if (idx < 0) {
|
|
1038
1050
|
prepend.push(c);
|
|
@@ -156,6 +156,13 @@ export class CrossToolModel extends ChartBaseElement {
|
|
|
156
156
|
this.updateCrossTool(hover);
|
|
157
157
|
return;
|
|
158
158
|
}
|
|
159
|
+
const anchoredHover = this.crossEventProducer.crossToolHover;
|
|
160
|
+
if (anchoredHover && hover.x === anchoredHover.x && hover.y === anchoredHover.y) {
|
|
161
|
+
const refreshedHover = Object.assign(Object.assign({}, hover), { x: anchoredHover.x, y: anchoredHover.y });
|
|
162
|
+
this.crossEventProducer.crossToolHover = refreshedHover;
|
|
163
|
+
this.updateCrossTool(refreshedHover);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
159
166
|
// additional crosstool move logic
|
|
160
167
|
const paneBounds = this.canvasBoundsContainer.getBounds(CanvasElement.PANE_UUID(hover.paneId));
|
|
161
168
|
const offset = 5;
|
|
@@ -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 { Subject } from 'rxjs';
|
|
6
7
|
import { ChartBaseElement } from '../../model/chart-base-element';
|
|
7
8
|
import { LinkedList } from '../../utils/linkedList.utils';
|
|
8
9
|
import { DynamicModelDrawer } from './dynamic-objects.drawer';
|
|
@@ -21,6 +22,7 @@ export declare class DynamicObjectsModel extends ChartBaseElement {
|
|
|
21
22
|
private _objects;
|
|
22
23
|
private modelIdToObjectMap;
|
|
23
24
|
private _uniqueObjects;
|
|
25
|
+
orderChangedSubject: Subject<void>;
|
|
24
26
|
constructor(canvasModel: CanvasModel);
|
|
25
27
|
/**
|
|
26
28
|
* @returns the `DynamicObject` itself and pane `LinkedList` where the object is stored.
|
|
@@ -52,6 +54,11 @@ export declare class DynamicObjectsModel extends ChartBaseElement {
|
|
|
52
54
|
* Moves the object inside the associated LinkedList to the specified position
|
|
53
55
|
*/
|
|
54
56
|
moveToPosition(id: DynamicObjectId, position: number): void;
|
|
57
|
+
getPaneObjectIds(paneId: PaneId): DynamicObjectId[];
|
|
58
|
+
setPaneOrder(paneId: PaneId, orderedIds: DynamicObjectId[], forceUpdate?: boolean): void;
|
|
59
|
+
reorderObjectIds(paneId: PaneId, orderedIds: DynamicObjectId[], forceUpdate?: boolean): void;
|
|
60
|
+
private notifyOrderChanged;
|
|
61
|
+
private moveObjectToFront;
|
|
55
62
|
/**
|
|
56
63
|
* Moves the object inside the drawing order so it's being drawn before the other elements
|
|
57
64
|
* @param paneId
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
|
|
9
9
|
* If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
10
10
|
*/
|
|
11
|
-
import { BehaviorSubject } from 'rxjs';
|
|
11
|
+
import { BehaviorSubject, Subject } from 'rxjs';
|
|
12
12
|
import { ChartBaseElement } from '../../model/chart-base-element';
|
|
13
13
|
import { LinkedList, ListNode } from '../../utils/linkedList.utils';
|
|
14
14
|
export class DynamicObjectsModel extends ChartBaseElement {
|
|
@@ -17,6 +17,7 @@ export class DynamicObjectsModel extends ChartBaseElement {
|
|
|
17
17
|
this.canvasModel = canvasModel;
|
|
18
18
|
this.modelIdToObjectMap = new Map();
|
|
19
19
|
this._uniqueObjects = {};
|
|
20
|
+
this.orderChangedSubject = new Subject();
|
|
20
21
|
this._objects = new BehaviorSubject({});
|
|
21
22
|
}
|
|
22
23
|
/**
|
|
@@ -130,12 +131,52 @@ export class DynamicObjectsModel extends ChartBaseElement {
|
|
|
130
131
|
}
|
|
131
132
|
this.setDynamicObjects(this.objects);
|
|
132
133
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
134
|
+
getPaneObjectIds(paneId) {
|
|
135
|
+
const paneList = this.objects[paneId];
|
|
136
|
+
if (!paneList) {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
return [...paneList].map(obj => obj.id);
|
|
140
|
+
}
|
|
141
|
+
setPaneOrder(paneId, orderedIds, forceUpdate = true) {
|
|
142
|
+
const paneList = this.objects[paneId];
|
|
143
|
+
if (!paneList || orderedIds.length === 0) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const currentIds = this.getPaneObjectIds(paneId);
|
|
147
|
+
const filteredOrderedIds = orderedIds.filter(id => currentIds.includes(id));
|
|
148
|
+
const missingIds = currentIds.filter(id => !filteredOrderedIds.includes(id));
|
|
149
|
+
const targetOrder = [...filteredOrderedIds, ...missingIds];
|
|
150
|
+
for (let i = 0; i < targetOrder.length; i++) {
|
|
151
|
+
const id = targetOrder[i];
|
|
152
|
+
const currentPos = this.getObjectPosition(id);
|
|
153
|
+
if (currentPos >= 0 && currentPos !== i) {
|
|
154
|
+
this.moveToPosition(id, i);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (forceUpdate) {
|
|
158
|
+
this.notifyOrderChanged();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
reorderObjectIds(paneId, orderedIds, forceUpdate = true) {
|
|
162
|
+
const orderedIdSet = new Set(orderedIds);
|
|
163
|
+
const paneObjectIds = this.getPaneObjectIds(paneId).filter(id => orderedIdSet.has(id));
|
|
164
|
+
if (paneObjectIds.length === 0) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
for (const id of orderedIds) {
|
|
168
|
+
if (paneObjectIds.includes(id)) {
|
|
169
|
+
this.moveObjectToFront(id);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (forceUpdate) {
|
|
173
|
+
this.notifyOrderChanged();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
notifyOrderChanged() {
|
|
177
|
+
this.orderChangedSubject.next();
|
|
178
|
+
}
|
|
179
|
+
moveObjectToFront(id) {
|
|
139
180
|
const objInfo = this.getObjectInfoById(id);
|
|
140
181
|
if (!objInfo) {
|
|
141
182
|
return;
|
|
@@ -149,6 +190,15 @@ export class DynamicObjectsModel extends ChartBaseElement {
|
|
|
149
190
|
this.setDynamicObjects(this.objects);
|
|
150
191
|
}
|
|
151
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* Moves the object inside the drawing order so it's being drawn before the other elements
|
|
195
|
+
* @param paneId
|
|
196
|
+
* @param listNode
|
|
197
|
+
*/
|
|
198
|
+
bringToFront(id) {
|
|
199
|
+
this.moveObjectToFront(id);
|
|
200
|
+
this.notifyOrderChanged();
|
|
201
|
+
}
|
|
152
202
|
/**
|
|
153
203
|
* Moves the object inside the drawing order so it's being drawn after the other elements
|
|
154
204
|
* @param paneId
|
|
@@ -166,6 +216,7 @@ export class DynamicObjectsModel extends ChartBaseElement {
|
|
|
166
216
|
paneList.removeAt(targetPos);
|
|
167
217
|
paneList.insertAt(0, obj);
|
|
168
218
|
this.setDynamicObjects(this.objects);
|
|
219
|
+
this.notifyOrderChanged();
|
|
169
220
|
}
|
|
170
221
|
}
|
|
171
222
|
/**
|
|
@@ -185,6 +236,7 @@ export class DynamicObjectsModel extends ChartBaseElement {
|
|
|
185
236
|
paneList.removeAt(targetPos);
|
|
186
237
|
paneList.insertAt(targetPos + 1, obj);
|
|
187
238
|
this.setDynamicObjects(this.objects);
|
|
239
|
+
this.notifyOrderChanged();
|
|
188
240
|
}
|
|
189
241
|
}
|
|
190
242
|
/**
|
|
@@ -204,6 +256,7 @@ export class DynamicObjectsModel extends ChartBaseElement {
|
|
|
204
256
|
paneList.removeAt(targetPos);
|
|
205
257
|
paneList.insertAt(targetPos - 1, obj);
|
|
206
258
|
this.setDynamicObjects(this.objects);
|
|
259
|
+
this.notifyOrderChanged();
|
|
207
260
|
}
|
|
208
261
|
}
|
|
209
262
|
/**
|
|
@@ -28,6 +28,10 @@ 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
35
|
/**
|
|
32
36
|
* Calculates the position of the highlight label based on the given parameters.
|
|
33
37
|
* @param {HighlightTextPlacement} placement - The placement of the highlight text.
|
|
@@ -8,6 +8,7 @@ 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';
|
|
11
12
|
const LABEL_PADDINGS = [20, 10];
|
|
12
13
|
export class HighlightsDrawer {
|
|
13
14
|
constructor(highlightsModel, chartModel, canvasModel, canvasBoundsContainer, config) {
|
|
@@ -63,13 +64,13 @@ export class HighlightsDrawer {
|
|
|
63
64
|
ctx.strokeStyle = strokeStyle;
|
|
64
65
|
items.forEach(item => {
|
|
65
66
|
var _a, _b, _c;
|
|
66
|
-
const
|
|
67
|
+
const period = this.chartModel.chartBaseModel.period;
|
|
68
|
+
const anchor = this.chartModel.getCandleTimestampAnchor();
|
|
69
|
+
const candles = this.chartModel.getCandles();
|
|
70
|
+
const fromXCandle = this.resolveHighlightFromCandle(item.from, candles, period, anchor);
|
|
67
71
|
const fromXCandleWidth = unitToPixels(fromXCandle.width, this.chartModel.scale.zoomX);
|
|
68
72
|
const fromX = fromXCandle.xStart(this.chartModel.scale);
|
|
69
|
-
|
|
70
|
-
// so we have to take previous timestamp based on current period to exclude PRE_MARKET highlighting
|
|
71
|
-
const xCandleTimestamp = item.to - this.chartModel.chartBaseModel.period;
|
|
72
|
-
const toXCandle = this.chartModel.candleFromTimestamp(xCandleTimestamp);
|
|
73
|
+
const toXCandle = this.resolveHighlightToCandle(item.to, candles, period, anchor);
|
|
73
74
|
const toXCandleWidth = unitToPixels(toXCandle.width, this.chartModel.scale.zoomX);
|
|
74
75
|
const toX = toXCandle.xStart(this.chartModel.scale) + toXCandleWidth;
|
|
75
76
|
// draw highlight' borders
|
|
@@ -98,6 +99,27 @@ export class HighlightsDrawer {
|
|
|
98
99
|
}
|
|
99
100
|
}
|
|
100
101
|
}
|
|
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;
|
|
106
|
+
}
|
|
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);
|
|
113
|
+
}
|
|
114
|
+
return this.chartModel.candleFromTimestamp(sessionFrom);
|
|
115
|
+
}
|
|
116
|
+
resolveHighlightToCandle(sessionTo, candles, period, anchor) {
|
|
117
|
+
const lookupTimestamp = this.getHighlightEndLookupTimestamp(sessionTo, period, anchor);
|
|
118
|
+
return this.chartModel.candleFromTimestamp(lookupTimestamp);
|
|
119
|
+
}
|
|
120
|
+
getHighlightEndLookupTimestamp(sessionTo, periodMs, anchor) {
|
|
121
|
+
return anchor === 'close' ? sessionTo - 1 : sessionTo - periodMs;
|
|
122
|
+
}
|
|
101
123
|
/**
|
|
102
124
|
* Calculates the position of the highlight label based on the given parameters.
|
|
103
125
|
* @param {HighlightTextPlacement} placement - The placement of the highlight text.
|
|
@@ -86,6 +86,7 @@ export declare class PaneComponent extends ChartBaseElement {
|
|
|
86
86
|
private createYPanHandler;
|
|
87
87
|
private addCursors;
|
|
88
88
|
createExtentComponent(options?: AtLeastOne<YExtentCreationOptions>): YExtentComponent;
|
|
89
|
+
private shouldKeepVolumeHostExtent;
|
|
89
90
|
removeExtentComponents(extentComponents: YExtentComponent[]): void;
|
|
90
91
|
/**
|
|
91
92
|
* Create new pane extent and attach data series to it
|
|
@@ -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;
|
|
@@ -160,9 +161,16 @@ export class PaneComponent extends ChartBaseElement {
|
|
|
160
161
|
this.yExtentComponentsChangedSubject.next();
|
|
161
162
|
return yExtentComponent;
|
|
162
163
|
}
|
|
164
|
+
shouldKeepVolumeHostExtent(extent) {
|
|
165
|
+
return (this.uuid === VOLUMES_UUID && extent === this.mainExtent && this.config.components.volumes.showSeparately);
|
|
166
|
+
}
|
|
163
167
|
removeExtentComponents(extentComponents) {
|
|
164
|
-
extentComponents.
|
|
165
|
-
|
|
168
|
+
const removableExtents = extentComponents.filter(extent => !this.shouldKeepVolumeHostExtent(extent));
|
|
169
|
+
if (removableExtents.length === 0) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
removableExtents.forEach(extentComponent => extentComponent.disable());
|
|
173
|
+
this.yExtentComponents = this.yExtentComponents.filter(current => !removableExtents.map(excluded => excluded.idx).includes(current.idx));
|
|
166
174
|
// re-index extents
|
|
167
175
|
this.yExtentComponents.forEach((c, idx) => {
|
|
168
176
|
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;
|
|
@@ -62,6 +62,7 @@ export declare class HoverProducerComponent extends ChartBaseElement {
|
|
|
62
62
|
* When true, mobile long-touch (e.g. 200ms) does not activate crosshair / disable pan.
|
|
63
63
|
*/
|
|
64
64
|
private longTouchCrosshairSuppressed;
|
|
65
|
+
private hoverOverLastCandle;
|
|
65
66
|
private hoverProducerParts;
|
|
66
67
|
xFormatter: DateTimeFormatter;
|
|
67
68
|
constructor(crossEventProducer: CrossEventProducerComponent, scale: ScaleModel, config: FullChartConfig, chartModel: ChartModel, canvasInputListener: CanvasInputListenerComponent, canvasBoundsContainer: CanvasBoundsContainer, paneManager: PaneManager, timeZoneModel: TimeZoneModel, mainCanvasTouchHandler: MainCanvasTouchHandler, formatterFactory: (format: string) => (timestamp: number) => string);
|
|
@@ -103,10 +104,11 @@ export declare class HoverProducerComponent extends ChartBaseElement {
|
|
|
103
104
|
*/
|
|
104
105
|
createAndFireHoverFromCandle(candle: VisualCandle): void;
|
|
105
106
|
/**
|
|
106
|
-
*
|
|
107
|
-
*
|
|
107
|
+
* Refreshes hover data (candles, studies, etc.) for the current pointer position.
|
|
108
|
+
* Used when the last candle updates while the legend must stay aligned to crosshair X — not to the last candle center.
|
|
108
109
|
*/
|
|
109
110
|
updateHover(candle: VisualCandle): void;
|
|
111
|
+
private updateHoverLastCandle;
|
|
110
112
|
/**
|
|
111
113
|
* Creates a hover element at the specified coordinates and fires it with the option to show the cross tool
|
|
112
114
|
* @param {CrossEvent} [x,y] - The coordinates where the hover element will be created
|