@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.
@@ -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
- if (lastCandle.candle.timestamp <= this.hover.timestamp) {
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
- * Update current hover using a VisualCandle and fires it.
247
- * @param {VisualCandle} candle - The VisualCandle object to create the hover from.
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 (this.hover && updatedHover) {
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
- export const searchCandleIndex = (rawTimestamp, options = {}, candles, periodMs = 1000) => {
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