@devexperts/dxcharts-lite 2.7.30 → 2.7.31
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/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.
|
|
@@ -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
|
|
@@ -38,6 +38,7 @@ export class HoverProducerComponent extends ChartBaseElement {
|
|
|
38
38
|
* When true, mobile long-touch (e.g. 200ms) does not activate crosshair / disable pan.
|
|
39
39
|
*/
|
|
40
40
|
this.longTouchCrosshairSuppressed = false;
|
|
41
|
+
this.hoverOverLastCandle = false;
|
|
41
42
|
this.xFormatter = () => '';
|
|
42
43
|
const candleHoverProducerPart = new CandleHoverProducerPart(this.chartModel);
|
|
43
44
|
const compareSeriesHoverProducerPart = new CompareSeriesHoverProducerPart(this.chartModel);
|
|
@@ -68,16 +69,14 @@ export class HoverProducerComponent extends ChartBaseElement {
|
|
|
68
69
|
lastCandle && this.createAndFireHoverFromCandle(lastCandle);
|
|
69
70
|
}));
|
|
70
71
|
this.addRxSubscription(this.chartModel.candlesUpdatedSubject.subscribe(() => {
|
|
71
|
-
// update hover if its timestamp is equal or greater than last candle's one
|
|
72
72
|
const lastCandle = this.chartModel.getLastVisualCandle();
|
|
73
|
-
if (this.hover !== null && lastCandle !== undefined) {
|
|
74
|
-
|
|
75
|
-
this.updateHover(lastCandle);
|
|
76
|
-
}
|
|
73
|
+
if (this.hover !== null && lastCandle !== undefined && this.hoverOverLastCandle) {
|
|
74
|
+
this.updateHover(lastCandle);
|
|
77
75
|
}
|
|
78
76
|
}));
|
|
79
77
|
this.addRxSubscription(this.crossEventProducer.crossSubject.subscribe((cross) => {
|
|
80
78
|
if (cross === null) {
|
|
79
|
+
this.hoverOverLastCandle = false;
|
|
81
80
|
this.hoverSubject.next(null);
|
|
82
81
|
}
|
|
83
82
|
else {
|
|
@@ -243,23 +242,53 @@ export class HoverProducerComponent extends ChartBaseElement {
|
|
|
243
242
|
this.fireHover(hover);
|
|
244
243
|
}
|
|
245
244
|
/**
|
|
246
|
-
*
|
|
247
|
-
*
|
|
245
|
+
* Refreshes hover data (candles, studies, etc.) for the current pointer position.
|
|
246
|
+
* Used when the last candle updates while the legend must stay aligned to crosshair X — not to the last candle center.
|
|
248
247
|
*/
|
|
249
248
|
updateHover(candle) {
|
|
250
249
|
var _a, _b;
|
|
250
|
+
if (!this.hover) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const shouldKeepSetTouchCrosshair = isMobile() &&
|
|
254
|
+
this.longTouchActivatedSubject.getValue() &&
|
|
255
|
+
this.crossEventProducer.crossSubject.getValue() !== null;
|
|
256
|
+
const crossToolHover = this.crossEventProducer.crossToolHover;
|
|
257
|
+
const hoverX = shouldKeepSetTouchCrosshair ? ((_a = crossToolHover === null || crossToolHover === void 0 ? void 0 : crossToolHover.x) !== null && _a !== void 0 ? _a : this.hover.x) : this.hover.x;
|
|
258
|
+
const hoverY = shouldKeepSetTouchCrosshair ? ((_b = crossToolHover === null || crossToolHover === void 0 ? void 0 : crossToolHover.y) !== null && _b !== void 0 ? _b : this.hover.y) : this.hover.y;
|
|
259
|
+
const hoveredCandle = this.chartModel.candleFromX(hoverX, true);
|
|
260
|
+
const lastIdx = candle.candle.idx;
|
|
261
|
+
const hoveredIdx = hoveredCandle.idx;
|
|
262
|
+
if (lastIdx !== undefined &&
|
|
263
|
+
hoveredIdx !== undefined &&
|
|
264
|
+
hoveredIdx < lastIdx &&
|
|
265
|
+
hoverX < candle.xStart(this.scale)) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
251
268
|
const updatedHover = this.createHoverFromCandle(candle);
|
|
252
|
-
if (
|
|
253
|
-
const shouldKeepSetTouchCrosshair = isMobile() &&
|
|
254
|
-
this.longTouchActivatedSubject.getValue() &&
|
|
255
|
-
this.crossEventProducer.crossSubject.getValue() !== null;
|
|
256
|
-
const crossToolHover = this.crossEventProducer.crossToolHover;
|
|
257
|
-
const hoverX = shouldKeepSetTouchCrosshair ? ((_a = crossToolHover === null || crossToolHover === void 0 ? void 0 : crossToolHover.x) !== null && _a !== void 0 ? _a : this.hover.x) : this.hover.x;
|
|
258
|
-
const hoverY = shouldKeepSetTouchCrosshair ? ((_b = crossToolHover === null || crossToolHover === void 0 ? void 0 : crossToolHover.y) !== null && _b !== void 0 ? _b : this.hover.y) : this.hover.y;
|
|
269
|
+
if (updatedHover) {
|
|
259
270
|
const hover = Object.assign(Object.assign({}, updatedHover), { x: hoverX, y: hoverY });
|
|
260
271
|
this.fireHover(hover);
|
|
261
272
|
}
|
|
262
273
|
}
|
|
274
|
+
updateHoverLastCandle(hover) {
|
|
275
|
+
const lastCandle = this.chartModel.getLastVisualCandle();
|
|
276
|
+
if (!lastCandle) {
|
|
277
|
+
this.hoverOverLastCandle = false;
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const hoveredCandle = this.chartModel.candleFromX(hover.x, true);
|
|
281
|
+
const lastIdx = lastCandle.candle.idx;
|
|
282
|
+
const hoveredIdx = hoveredCandle.idx;
|
|
283
|
+
const isHistoricalCandleHover = lastIdx !== undefined &&
|
|
284
|
+
hoveredIdx !== undefined &&
|
|
285
|
+
hoveredIdx < lastIdx &&
|
|
286
|
+
hover.x < lastCandle.xStart(this.scale);
|
|
287
|
+
this.hoverOverLastCandle =
|
|
288
|
+
!isHistoricalCandleHover &&
|
|
289
|
+
(lastCandle.candle.timestamp <= hover.timestamp ||
|
|
290
|
+
(lastIdx !== undefined && hoveredIdx !== undefined && hoveredIdx === lastIdx));
|
|
291
|
+
}
|
|
263
292
|
/**
|
|
264
293
|
* Creates a hover element at the specified coordinates and fires it with the option to show the cross tool
|
|
265
294
|
* @param {CrossEvent} [x,y] - The coordinates where the hover element will be created
|
|
@@ -292,9 +321,11 @@ export class HoverProducerComponent extends ChartBaseElement {
|
|
|
292
321
|
: (_b = hover.candleHover) === null || _b === void 0 ? void 0 : _b.visualCandle.candle;
|
|
293
322
|
candle && this.chartModel.mainCandleSeries.setActiveCandle(candle);
|
|
294
323
|
}
|
|
324
|
+
this.updateHoverLastCandle(hover);
|
|
295
325
|
this.hoverSubject.next(hover);
|
|
296
326
|
}
|
|
297
327
|
else {
|
|
328
|
+
this.hoverOverLastCandle = false;
|
|
298
329
|
this.crossEventProducer.fireCrossClose();
|
|
299
330
|
}
|
|
300
331
|
}
|
|
@@ -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 { CandleTimestampAnchor } from '../chart.config';
|
|
6
7
|
import { Candle } from '../model/candle.model';
|
|
7
8
|
import { DataSeriesPoint } from '../model/data-series.model';
|
|
8
9
|
import { BinarySearchResult } from './array.utils';
|
|
@@ -10,7 +11,10 @@ export declare const getDaysOnlyTimestampFn: (isDaysPeriod: boolean) => (timesta
|
|
|
10
11
|
export declare const searchCandleIndex: (rawTimestamp: number, options: {
|
|
11
12
|
extrapolate?: boolean;
|
|
12
13
|
isDaysPeriod?: boolean;
|
|
14
|
+
candleTimestampAnchor?: CandleTimestampAnchor;
|
|
13
15
|
} | undefined, candles: DataSeriesPoint[], periodMs?: number) => BinarySearchResult;
|
|
16
|
+
export declare const getCandleStart: (candles: DataSeriesPoint[], index: number, periodMs: number, anchor: CandleTimestampAnchor) => number;
|
|
17
|
+
export declare const getCandleEnd: (candle: DataSeriesPoint, periodMs: number, anchor: CandleTimestampAnchor) => number;
|
|
14
18
|
/**
|
|
15
19
|
* checks if the first or the last candle or both have implied volatility data provided
|
|
16
20
|
* @param candles
|
|
@@ -11,7 +11,7 @@ export const getDaysOnlyTimestampFn = (isDaysPeriod) => (timestamp) => {
|
|
|
11
11
|
}
|
|
12
12
|
return timestamp;
|
|
13
13
|
};
|
|
14
|
-
|
|
14
|
+
const searchOpenTimeCandleIndex = (rawTimestamp, options = {}, candles, periodMs = 1000) => {
|
|
15
15
|
var _a, _b, _c, _d;
|
|
16
16
|
const { extrapolate, isDaysPeriod } = options;
|
|
17
17
|
const shouldExtrapolate = Boolean(extrapolate);
|
|
@@ -55,6 +55,85 @@ export const searchCandleIndex = (rawTimestamp, options = {}, candles, periodMs
|
|
|
55
55
|
return binarySearch(candles, timestamp, candle => getDaysOnlyTimestamp(candle.timestamp));
|
|
56
56
|
}
|
|
57
57
|
};
|
|
58
|
+
const searchCloseTimeCandleIndex = (rawTimestamp, options = {}, candles, periodMs = 1000) => {
|
|
59
|
+
var _a, _b, _c, _d, _e, _f;
|
|
60
|
+
const { extrapolate, isDaysPeriod } = options;
|
|
61
|
+
const shouldExtrapolate = Boolean(extrapolate);
|
|
62
|
+
const getDaysOnlyTimestamp = getDaysOnlyTimestampFn(Boolean(isDaysPeriod));
|
|
63
|
+
const timestamp = getDaysOnlyTimestamp(rawTimestamp);
|
|
64
|
+
if (!candles.length) {
|
|
65
|
+
return {
|
|
66
|
+
index: -1,
|
|
67
|
+
exact: true,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const firstTimestamp = getDaysOnlyTimestamp((_b = (_a = firstOf(candles)) === null || _a === void 0 ? void 0 : _a.timestamp) !== null && _b !== void 0 ? _b : 0);
|
|
71
|
+
const lastTimestamp = getDaysOnlyTimestamp((_d = (_c = lastOf(candles)) === null || _c === void 0 ? void 0 : _c.timestamp) !== null && _d !== void 0 ? _d : 0);
|
|
72
|
+
if (timestamp > lastTimestamp) {
|
|
73
|
+
if (shouldExtrapolate) {
|
|
74
|
+
return {
|
|
75
|
+
index: candles.length - 1 + Math.ceil((timestamp - lastTimestamp) / periodMs),
|
|
76
|
+
exact: true,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
index: candles.length - 1,
|
|
81
|
+
exact: true,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (timestamp <= firstTimestamp) {
|
|
85
|
+
const previousClose = getDaysOnlyTimestamp((_f = (_e = candles[0]) === null || _e === void 0 ? void 0 : _e.timestamp) !== null && _f !== void 0 ? _f : firstTimestamp);
|
|
86
|
+
const firstCandleStart = previousClose - periodMs;
|
|
87
|
+
if (timestamp <= firstCandleStart) {
|
|
88
|
+
if (shouldExtrapolate) {
|
|
89
|
+
return {
|
|
90
|
+
index: floor((timestamp - firstTimestamp) / periodMs),
|
|
91
|
+
exact: true,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
index: -1,
|
|
96
|
+
exact: true,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
index: 0,
|
|
101
|
+
exact: timestamp === firstTimestamp,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
let lo = 0;
|
|
105
|
+
let hi = candles.length;
|
|
106
|
+
while (lo < hi) {
|
|
107
|
+
const mid = floor((lo + hi) / 2);
|
|
108
|
+
if (getDaysOnlyTimestamp(candles[mid].timestamp) < timestamp) {
|
|
109
|
+
lo = mid + 1;
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
hi = mid;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const index = lo;
|
|
116
|
+
return {
|
|
117
|
+
index: index >= candles.length ? candles.length - 1 : index,
|
|
118
|
+
exact: index < candles.length && getDaysOnlyTimestamp(candles[index].timestamp) === timestamp,
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
export const searchCandleIndex = (rawTimestamp, options = {}, candles, periodMs = 1000) => {
|
|
122
|
+
var _a;
|
|
123
|
+
const anchor = (_a = options.candleTimestampAnchor) !== null && _a !== void 0 ? _a : 'open';
|
|
124
|
+
if (anchor === 'close') {
|
|
125
|
+
return searchCloseTimeCandleIndex(rawTimestamp, options, candles, periodMs);
|
|
126
|
+
}
|
|
127
|
+
return searchOpenTimeCandleIndex(rawTimestamp, options, candles, periodMs);
|
|
128
|
+
};
|
|
129
|
+
export const getCandleStart = (candles, index, periodMs, anchor) => {
|
|
130
|
+
var _a, _b;
|
|
131
|
+
if (anchor === 'close') {
|
|
132
|
+
return (_b = (_a = candles[index - 1]) === null || _a === void 0 ? void 0 : _a.timestamp) !== null && _b !== void 0 ? _b : candles[index].timestamp - periodMs;
|
|
133
|
+
}
|
|
134
|
+
return candles[index].timestamp;
|
|
135
|
+
};
|
|
136
|
+
export const getCandleEnd = (candle, periodMs, anchor) => anchor === 'close' ? candle.timestamp : candle.timestamp + periodMs;
|
|
58
137
|
/**
|
|
59
138
|
* checks if the first or the last candle or both have implied volatility data provided
|
|
60
139
|
* @param candles
|