@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.
@@ -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, { extrapolate: false }, baseSeries, this.chartBaseModel.period);
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, { extrapolate: true }, curCandles, this.getPeriod());
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, { extrapolate: false }, target);
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
- * Moves the object inside the drawing order so it's being drawn before the other elements
135
- * @param paneId
136
- * @param listNode
137
- */
138
- bringToFront(id) {
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 fromXCandle = this.chartModel.candleFromTimestamp(item.from);
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
- // currently endTime timestamp for PRE_MARKET type includes same timestamp for REGULAR type startTime
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.forEach(extentComponent => extentComponent.disable());
165
- this.yExtentComponents = this.yExtentComponents.filter(current => !extentComponents.map(excluded => excluded.idx).includes(current.idx));
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, deleting the scale model, and removing the underlay volumes area drawer.
31
+ * Deactivates the separate volumes feature by removing the separate volumes pane when it has no study data series,
32
+ * or by keeping the pane when studies remain on it.
31
33
  */
32
34
  deactiveSeparateVolumes(): void;
33
35
  /**
@@ -22,6 +22,9 @@ export class SeparateVolumesComponent extends ChartBaseElement {
22
22
  this.addRxSubscription(this.chartComponent.chartModel.candlesUpdatedSubject.subscribe(() => { var _a, _b; return !((_a = this.pane) === null || _a === void 0 ? void 0 : _a.scale.isViewportValid()) && ((_b = this.pane) === null || _b === void 0 ? void 0 : _b.scale.doAutoScale(true)); }));
23
23
  this.addRxSubscription(this.volumesModel.volumeMax.subscribe(() => { var _a; return (_a = this.pane) === null || _a === void 0 ? void 0 : _a.scale.doAutoScale(); }));
24
24
  }
25
+ paneHasStudyDataSeries(pane) {
26
+ return pane.dataSeries.length > 0;
27
+ }
25
28
  /**
26
29
  * Activates the separate volumes feature.
27
30
  * If the pane component for separate volumes does not exist, it creates a new pane with the specified UUID and options.
@@ -29,7 +32,8 @@ export class SeparateVolumesComponent extends ChartBaseElement {
29
32
  * A new VolumesDrawer is created and added to the drawing manager with the specified parameters.
30
33
  */
31
34
  activateSeparateVolumes() {
32
- if (this.paneManager.panes[SeparateVolumesComponent.UUID] === undefined) {
35
+ const existingPane = this.paneManager.panes[SeparateVolumesComponent.UUID];
36
+ if (existingPane === undefined) {
33
37
  const precision = 1;
34
38
  const volumePane = this.paneManager.createPane(SeparateVolumesComponent.UUID, {
35
39
  paneFormatters: {
@@ -38,18 +42,30 @@ export class SeparateVolumesComponent extends ChartBaseElement {
38
42
  useDefaultHighLow: false,
39
43
  increment: 1,
40
44
  });
41
- this.pane = volumePane;
42
45
  volumePane.mainExtent.yAxis.setAxisType('regular');
43
46
  const { scale: scaleModel } = volumePane;
44
47
  const volumesHighLowProvider = createCandlesOffsetProvider(() => ({ top: 10, bottom: 0, left: 0, right: 0, visible: true }), this.volumesModel.highLowProvider);
45
48
  scaleModel.autoScaleModel.setHighLowProvider('volumes', volumesHighLowProvider);
46
49
  scaleModel.doAutoScale(true);
50
+ this.pane = volumePane;
51
+ return;
47
52
  }
53
+ this.pane = existingPane;
48
54
  }
49
55
  /**
50
- * Deactivates the separate volumes feature by removing the separate volumes pane, deleting the scale model, and removing the underlay volumes area drawer.
56
+ * Deactivates the separate volumes feature by removing the separate volumes pane when it has no study data series,
57
+ * or by keeping the pane when studies remain on it.
51
58
  */
52
59
  deactiveSeparateVolumes() {
60
+ const pane = this.paneManager.panes[SeparateVolumesComponent.UUID];
61
+ if (pane === undefined) {
62
+ delete this.pane;
63
+ return;
64
+ }
65
+ if (this.paneHasStudyDataSeries(pane)) {
66
+ this.pane = undefined;
67
+ return;
68
+ }
53
69
  this.paneManager.removePane(SeparateVolumesComponent.UUID);
54
70
  delete this.pane;
55
71
  }
@@ -40,6 +40,8 @@ export declare class VolumesComponent extends ChartBaseElement {
40
40
  * @param {boolean} separate - A boolean value indicating whether the volumes should be shown separately or not.
41
41
  * @returns {void}
42
42
  */
43
+ /** Restores separate volumes pane and dynamic object after study pane cleanup. */
44
+ ensureSeparateVolumesHost(): void;
43
45
  setShowVolumesSeparatly(separate: boolean): void;
44
46
  /**
45
47
  * This method deactivates the current component by calling the superclass doDeactivate method and setting the visibility of the component to false.
@@ -53,6 +53,15 @@ export class VolumesComponent extends ChartBaseElement {
53
53
  * @param {boolean} separate - A boolean value indicating whether the volumes should be shown separately or not.
54
54
  * @returns {void}
55
55
  */
56
+ /** Restores separate volumes pane and dynamic object after study pane cleanup. */
57
+ ensureSeparateVolumesHost() {
58
+ if (!this.config.components.volumes.showSeparately) {
59
+ return;
60
+ }
61
+ this.separateVolumes.activateSeparateVolumes();
62
+ this.syncVolumesDynamicObject();
63
+ this.canvasModel.fireDraw();
64
+ }
56
65
  setShowVolumesSeparatly(separate) {
57
66
  if (this.config.components.volumes.showSeparately !== separate) {
58
67
  this.config.components.volumes.showSeparately = separate;
@@ -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
- * Update current hover using a VisualCandle and fires it.
107
- * @param {VisualCandle} candle - The VisualCandle object to create the hover from.
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