Загрузка данных
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 945fc82c5ad51d762dfc1982f0a583419f6ec892..dc4ba10ed6cb94be88752e319cccbd4f8da4830d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@
- Добавлены тултипы для элементов управления в шапке графика
- Добавлен параметр `showSnapshotButton?: boolean` для управления отображением кнопки «Снимок графика»
- Добавлен VWAP индикатор
+- Добавлена механика ctrl+c/ctrl+v для дровингов
# 0.2.1
diff --git a/src/components/Loader/index.module.scss b/src/components/Loader/index.module.scss
new file mode 100644
index 0000000000000000000000000000000000000000..6cd13ece627595713578598758e1dac98a380636
--- /dev/null
+++ b/src/components/Loader/index.module.scss
@@ -0,0 +1,28 @@
+.loaderWrapper {
+ position: 'relative';
+ width: '100%';
+ height: '100%';
+ background-color: '#fff';
+ z-index: '100';
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.loader {
+ border: 10px solid #1b1717;
+ border-top: 10px solid #242627;
+ border-radius: 50%;
+ width: 120px;
+ height: 120px;
+ animation: spin 2s linear infinite;
+}
+
+@keyframes spin {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
diff --git a/src/components/Loader/index.tsx b/src/components/Loader/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..89c48f1cce9882665e5845f11cd05ec6617d4594
--- /dev/null
+++ b/src/components/Loader/index.tsx
@@ -0,0 +1,12 @@
+import styles from './index.module.scss';
+
+export const Loader = () => {
+ return (
+ <div
+ className={styles.loaderWrapper}
+ style={{ position: 'relative', width: '100%', height: '100%', backgroundColor: 'transparent', zIndex: '100' }}
+ >
+ <div className={styles.loader} />
+ </div>
+ );
+};
diff --git a/src/core/ContainerManager.ts b/src/core/ContainerManager.ts
index 218997a9159538d440767ba04d405db92c47c8a7..3a4de390c06fd3bf39c58dc13e8a42d294f352bf 100644
--- a/src/core/ContainerManager.ts
+++ b/src/core/ContainerManager.ts
@@ -96,6 +96,15 @@ export class ContainerManager {
chartAreaContainer.tabIndex = -1;
chartAreaContainer.style.cursor = 'crosshair';
+ const loaderContainer = document.createElement('div');
+ loaderContainer.style.position = 'absolute';
+ loaderContainer.style.height = '100%';
+ loaderContainer.style.width = '100%';
+ loaderContainer.style.minHeight = '0';
+ loaderContainer.style.minWidth = '0';
+ loaderContainer.style.outline = 'none';
+ loaderContainer.tabIndex = -1;
+
const toolBarContainer = document.createElement('div');
toolBarContainer.style.height = '100%';
toolBarContainer.style.minHeight = '0';
@@ -123,7 +132,7 @@ export class ContainerManager {
modalContainer.style.zIndex = ZIndex.Modal;
modalContainer.hidden = true;
- chartAreaContainer.append(controlBarContainer, drawingToolbarContainer);
+ chartAreaContainer.append(controlBarContainer, drawingToolbarContainer, loaderContainer);
chartContainer.append(chartAreaContainer, modalContainer);
parentContainer.append(headerContainer, chartContainer);
@@ -161,6 +170,7 @@ export class ContainerManager {
modalContainer,
controlBarContainer,
drawingToolbarContainer,
+ loaderContainer,
toggleToolbar,
};
}
diff --git a/src/core/DataSource.ts b/src/core/DataSource.ts
index 1017342b5907e89a3aac7ce33923d63bfc47ad1c..adcdca0e017deda7f635ab99f608e1266f30340f 100644
--- a/src/core/DataSource.ts
+++ b/src/core/DataSource.ts
@@ -1,5 +1,5 @@
-import { BehaviorSubject, combineLatest, Observable, Subject, Subscription } from 'rxjs';
-import { map } from 'rxjs/operators';
+import { BehaviorSubject, combineLatest, Observable, of, Subject, Subscription } from 'rxjs';
+import { map, switchMap } from 'rxjs/operators';
import { EventManager } from '@core/EventManager';
import { SymbolSource } from '@src/core/SymbolSource';
@@ -26,6 +26,7 @@ export class DataSource {
private readonly eventManager: EventManager;
private readonly states = new Map<string, SymbolSource>();
+ private readonly activeStates$ = new BehaviorSubject<Set<SymbolSource>>(new Set());
private readonly activeSymbols$ = new BehaviorSubject<Set<string>>(new Set());
private realtimeSub: Subscription | null = null;
@@ -37,6 +38,23 @@ export class DataSource {
this.initSubscriptions();
}
+ public isLoading = (): Observable<boolean> => {
+ return this.activeStates$.pipe(
+ switchMap((activeStates: Set<SymbolSource>) => {
+ if (activeStates.size === 0) {
+ return of(false);
+ }
+
+ const isSymbolLoading$ = Array.from(activeStates).map((st) => st.isLoading());
+ return combineLatest(isSymbolLoading$).pipe(
+ map((isSymbolLoading) => {
+ return isSymbolLoading.some((val) => val === true);
+ }),
+ );
+ }),
+ );
+ };
+
public setSymbols(symbols: string[]): void {
const nextSet = new Set<string>();
@@ -175,6 +193,17 @@ export class DataSource {
}
private initSubscriptions(): void {
+ this.subscriptions.add(
+ this.activeSymbols$.subscribe((symbolsSet) => {
+ const symbolSources: Set<SymbolSource> = new Set();
+ [...symbolsSet].forEach((symbol) => {
+ symbolSources.add(this.ensureState(symbol));
+ });
+
+ this.activeStates$.next(symbolSources);
+ }),
+ );
+
this.subscriptions.add(
combineLatest([this.eventManager.getSelectedSeries()]).subscribe(() => {
this.states.forEach((st) => st.saveRealtimeCache());
diff --git a/src/core/Drawings.ts b/src/core/Drawings.ts
index dc72e4e6bddef066e1e1907753944e365166c2b6..d80060268388852b2acd22de541901861ea4354a 100644
--- a/src/core/Drawings.ts
+++ b/src/core/Drawings.ts
@@ -15,7 +15,6 @@ interface DrawingParams extends DOMObjectParams {
lwcChart: IChartApi;
mainSeries: SeriesStrategies;
onDelete: (id: string) => void;
- onCopy: () => void;
construct: (chart: IChartApi, series: ISeriesApi<SeriesType>, interaction: DrawingInteraction) => ISeriesDrawing;
selected$: Observable<boolean>;
isSelected: () => boolean;
@@ -35,7 +34,6 @@ export class Drawing extends DOMObject {
private subscriptions = new Subscription();
private unregisterDeleteHotkey = () => {};
- private unregisterCopyHotkey = () => {};
constructor({
lwcChart,
@@ -44,7 +42,6 @@ export class Drawing extends DOMObject {
drawingName,
id,
onDelete,
- onCopy,
zIndex,
moveUp,
moveDown,
@@ -103,15 +100,6 @@ export class Drawing extends DOMObject {
this.delete();
},
});
-
- this.unregisterCopyHotkey = this.hotkeys.register({
- keys: [Keys.mod, Keys.c],
- callback: () => {
- if (!this.isCreationPending()) {
- onCopy();
- }
- },
- });
}),
);
}
@@ -219,9 +207,7 @@ export class Drawing extends DOMObject {
private unregisterSelectedDrawingHotkeys(): void {
this.unregisterDeleteHotkey();
- this.unregisterCopyHotkey();
this.unregisterDeleteHotkey = () => {};
- this.unregisterCopyHotkey = () => {};
}
}
diff --git a/src/core/Drawings/__tests__/shortcuts.test.ts b/src/core/Drawings/__tests__/shortcuts.test.ts
index ec941c4692765df0357027330e3fba7ce5c4bce5..e65100ab26fbeafe524869a3ceec2df91f9c95bf 100644
--- a/src/core/Drawings/__tests__/shortcuts.test.ts
+++ b/src/core/Drawings/__tests__/shortcuts.test.ts
@@ -9,7 +9,7 @@ import { Keys } from '@src/core/Hotkeys';
import type { MouseEventParams } from 'lightweight-charts';
-function createEvent({
+export function createEvent({
shiftKey = false,
altKey = false,
ctrlKey = false,
diff --git a/src/core/Drawings/axisLine/axisLine.ts b/src/core/Drawings/axisLine/axisLine.ts
index 9da496e9abda866ef0c076e722b784b3433974e3..8bce3938d26a20a7ddfa9640033e2fc3a72cf043 100644
--- a/src/core/Drawings/axisLine/axisLine.ts
+++ b/src/core/Drawings/axisLine/axisLine.ts
@@ -36,6 +36,19 @@ interface AxisLineParams extends BaseDrawingParams {
direction: AxisLineDirection;
}
+export function isAxisLineState(state: unknown): state is AxisLineState {
+ if (typeof state !== 'object' || state === null) {
+ return false;
+ }
+
+ return (
+ 'time' in state &&
+ 'price' in state &&
+ ((typeof state.time === 'number' && state.price === null) ||
+ (state.time === null && typeof state.price === 'number'))
+ );
+}
+
interface AxisLineState {
hidden: boolean;
mode: AxisLineMode;
diff --git a/src/core/Drawings/fibonacciRetracement/fibonacciRetracement.ts b/src/core/Drawings/fibonacciRetracement/fibonacciRetracement.ts
index a288a14cad591c7118567b6e2718e3ac59e7ea6b..ccc8e3b691698e1783ad044b1fc162651b56d9e1 100644
--- a/src/core/Drawings/fibonacciRetracement/fibonacciRetracement.ts
+++ b/src/core/Drawings/fibonacciRetracement/fibonacciRetracement.ts
@@ -29,6 +29,23 @@ type PriceLabelKind = 'top' | 'bottom';
type FibonacciRetracementParams = BaseDrawingParams;
+export function isFibonacciRetracementState(state: unknown): state is FibonacciRetracementState {
+ if (typeof state !== 'object' || state === null) {
+ return false;
+ }
+
+ return (
+ 'startTime' in state &&
+ typeof state.startTime === 'number' &&
+ 'endTime' in state &&
+ typeof state.endTime === 'number' &&
+ 'startPrice' in state &&
+ typeof state.startPrice === 'number' &&
+ 'endPrice' in state &&
+ typeof state.endPrice === 'number'
+ );
+}
+
interface FibonacciRetracementState {
hidden: boolean;
mode: FibonacciRetracementMode;
diff --git a/src/core/Drawings/line/lineDrawing.ts b/src/core/Drawings/line/lineDrawing.ts
index 128255b28d9bd901e225f6d8c796e10de919980b..b21f8814cb7baf4267f114b63f04da81cd8b5e4e 100644
--- a/src/core/Drawings/line/lineDrawing.ts
+++ b/src/core/Drawings/line/lineDrawing.ts
@@ -5,6 +5,8 @@ import { BaseDrawingParams, ISeriesDrawing, LinearDrawingBase, TwoPointGeometry
import { type ChartOptionsModel, Defaults, LineMarker, type SettingsTab } from '@src/types';
import { formatDate, formatPrice } from '@src/utils';
+import { isAnchor } from '../utils';
+
import { LineDrawingPaneView } from './paneView';
import { createDefaultSettings, getLineDrawingSettingsTabs } from './settings';
@@ -24,6 +26,14 @@ interface LineDrawingParams extends BaseDrawingParams {
defaultMarkers?: Partial<LineDrawingMarkers>;
}
+export function isLineDrawingState(state: unknown): state is LineDrawingState {
+ if (typeof state !== 'object' || state === null) {
+ return false;
+ }
+
+ return 'startAnchor' in state && isAnchor(state.startAnchor) && 'endAnchor' in state && isAnchor(state.endAnchor);
+}
+
interface LineDrawingState {
hidden: boolean;
mode: LineDrawingMode;
diff --git a/src/core/Drawings/ray/ray.ts b/src/core/Drawings/ray/ray.ts
index 46a152f0867d181894d1d1210a1698eb5ccf7261..a90c04a7ddd3d251334b15d6dfae716d30bc429f 100644
--- a/src/core/Drawings/ray/ray.ts
+++ b/src/core/Drawings/ray/ray.ts
@@ -1,6 +1,6 @@
import { PrimitiveHoveredItem, UTCTimestamp } from 'lightweight-charts';
-import { getDistanceToSegment } from '@core/Drawings/utils';
+import { getDistanceToSegment, isAnchor } from '@core/Drawings/utils';
import { BaseDrawingParams, ISeriesDrawing, LinearDrawingBase } from '@src/core/Drawings/base';
import { type ChartOptionsModel, Defaults, type SettingsTab } from '@src/types';
@@ -23,6 +23,19 @@ type PriceLabelKind = 'start' | 'direction';
type RayParams = BaseDrawingParams;
+export function isRayState(state: unknown): state is RayState {
+ if (typeof state !== 'object' || state === null) {
+ return false;
+ }
+
+ return (
+ 'startAnchor' in state &&
+ isAnchor(state.startAnchor) &&
+ 'directionAnchor' in state &&
+ isAnchor(state.directionAnchor)
+ );
+}
+
interface RayState {
hidden: boolean;
mode: RayMode;
diff --git a/src/core/Drawings/regressionTrend/regressionTrend.ts b/src/core/Drawings/regressionTrend/regressionTrend.ts
index 6ed24bfd367a9c32664674a6d64490e755e6eb9a..b9ad932850c9c26eeb097eb13ffb7b83a6b45774 100644
--- a/src/core/Drawings/regressionTrend/regressionTrend.ts
+++ b/src/core/Drawings/regressionTrend/regressionTrend.ts
@@ -25,6 +25,19 @@ type PriceLabelKind = 'start' | 'end';
type RegressionTrendParams = BaseDrawingParams;
+export function isRegressionTrendState(state: unknown): state is RegressionTrendState {
+ if (typeof state !== 'object' || state === null) {
+ return false;
+ }
+
+ return (
+ 'startTime' in state &&
+ typeof state.startTime === 'number' &&
+ 'endTime' in state &&
+ typeof state.endTime === 'number'
+ );
+}
+
interface RegressionTrendState {
hidden: boolean;
mode: RegressionTrendMode;
diff --git a/src/core/Drawings/sliderPosition/sliderPosition.ts b/src/core/Drawings/sliderPosition/sliderPosition.ts
index c9abaa9ac3918de4726334d0c6b7d8ef2983537b..1fa5d8d7bbb73335e7c4a9e9dccbd55a5bf7b3c4 100644
--- a/src/core/Drawings/sliderPosition/sliderPosition.ts
+++ b/src/core/Drawings/sliderPosition/sliderPosition.ts
@@ -51,6 +51,25 @@ interface SliderPositionParams extends BaseDrawingParams {
resetTriggers?: Observable<unknown>[];
}
+export function isSliderPositionState(state: unknown): state is SliderPositionState {
+ if (typeof state !== 'object' || state === null) {
+ return false;
+ }
+
+ return (
+ 'startTime' in state &&
+ typeof state.startTime === 'number' &&
+ 'endTime' in state &&
+ typeof state.endTime === 'number' &&
+ 'entryPrice' in state &&
+ typeof state.entryPrice === 'number' &&
+ 'stopPrice' in state &&
+ typeof state.stopPrice === 'number' &&
+ 'targetPrice' in state &&
+ typeof state.targetPrice === 'number'
+ );
+}
+
interface SliderPositionState {
hidden: boolean;
mode: SliderMode;
diff --git a/src/core/Drawings/text/text.ts b/src/core/Drawings/text/text.ts
index 03c59c46ef8dfb2eb95b2ab6be10cfd3ddff0b65..9489acb4130a608630839b4dc7c9560896bd54ee 100644
--- a/src/core/Drawings/text/text.ts
+++ b/src/core/Drawings/text/text.ts
@@ -9,7 +9,7 @@ import {
getYCoordinateFromPrice,
isPointInBounds,
} from '@core/Drawings/helpers';
-import { updateViews } from '@core/Drawings/utils';
+import { isAnchor, updateViews } from '@core/Drawings/utils';
import { BaseDrawingParams, DrawingBase, ISeriesDrawing } from '@src/core/Drawings/base';
import { Defaults, SettingsTab } from '@src/types';
@@ -23,6 +23,14 @@ import type { ChartOptionsModel } from '@src/types';
type TextMode = 'idle' | 'dragging' | 'ready';
+export function isTextState(state: unknown): state is TextState {
+ if (typeof state !== 'object' || state === null) {
+ return false;
+ }
+
+ return 'point' in state && isAnchor(state.point);
+}
+
export interface TextState {
hidden: boolean;
mode: TextMode;
diff --git a/src/core/Drawings/traectory/traectory.ts b/src/core/Drawings/traectory/traectory.ts
index 8d2fb230572b8d8ba90ae0309c7ca6a990f4c88d..aad51bf06c25bc85543f7e8f7e287e24f00a8512 100644
--- a/src/core/Drawings/traectory/traectory.ts
+++ b/src/core/Drawings/traectory/traectory.ts
@@ -2,7 +2,7 @@ import { PrimitiveHoveredItem } from 'lightweight-charts';
import { clamp } from 'lodash-es';
import { clampPointToContainer, getContainerSize, isNearPoint } from '@core/Drawings/helpers';
-import { getDistanceToSegment } from '@core/Drawings/utils';
+import { getDistanceToSegment, isAnchor } from '@core/Drawings/utils';
import { BaseDrawingParams, DrawingBase, ISeriesDrawing } from '@src/core/Drawings/base';
@@ -18,6 +18,14 @@ type TraectoryMode = 'idle' | 'drawing' | 'ready' | 'dragging-point' | 'dragging
type TraectoryHandleId = `${number}`;
type TraectoryParams = BaseDrawingParams;
+export function isTraectoryState(state: unknown): state is TraectoryState {
+ if (typeof state !== 'object' || state === null) {
+ return false;
+ }
+
+ return 'points' in state && Array.isArray(state.points) && state.points.every((point) => isAnchor(point));
+}
+
export interface TraectoryState {
hidden: boolean;
mode: TraectoryMode;
diff --git a/src/core/Drawings/utils.ts b/src/core/Drawings/utils.ts
index a00fdf76fd8222319e2578fd891a5a0bf8baf12b..92cbcc6a3b8f89503446106dcd7173e2351a9b68 100644
--- a/src/core/Drawings/utils.ts
+++ b/src/core/Drawings/utils.ts
@@ -1,4 +1,4 @@
-import type { Point, UpdatableView } from './types';
+import type { Anchor, Point, UpdatableView } from './types';
export function updateViews(views: readonly UpdatableView[]): void {
for (const view of views) {
@@ -66,3 +66,14 @@ export function getDistanceToSegment(point: Point, startPoint: Point, endPoint:
return Math.hypot(point.x - projectionX, point.y - projectionY);
}
+
+export function isAnchor(value: unknown): value is Anchor {
+ return (
+ value !== null &&
+ typeof value === 'object' &&
+ 'time' in value &&
+ typeof value.time === 'number' &&
+ 'price' in value &&
+ typeof value.price === 'number'
+ );
+}
diff --git a/src/core/DrawingsManager.tsx b/src/core/DrawingsManager.tsx
index 5b1f486c6e87eaff0dd87fbceb90637769eb5ccb..87ed194714fbeb9886ef4e46b93e2bf6c934cff8 100644
--- a/src/core/DrawingsManager.tsx
+++ b/src/core/DrawingsManager.tsx
@@ -13,12 +13,13 @@ import { ModalRenderer } from '@src/core/ModalRenderer';
import { SeriesStrategies } from '@src/modules/series-strategies/SeriesFactory';
import { ActiveDrawingTool, DOMObjectSnapshot } from '@src/types';
+import { COPPIED_DRWAING_ID } from './DrawingsManagerCollection';
import { Pane } from './Pane';
import type { DrawingInteraction } from '@src/core/Drawings/base';
import type { SettingsValues } from '@src/types/settings';
-interface DrawingsManagerParams {
+export interface DrawingsManagerParams {
eventManager: EventManager;
mainSeries$: Observable<SeriesStrategies | null>;
lwcChart: IChartApi;
@@ -69,7 +70,6 @@ export class DrawingsManager {
private selectedDrawingSnapshot: DrawingSnapshotItem | null = null;
private pane: Pane;
- private copyPasteBuffer: DrawingSnapshotItem | null = null;
private setActiveTool: (name: ActiveDrawingTool) => void;
private getActiveTool: () => ActiveDrawingTool;
private getIsEndlessMode: () => boolean;
@@ -120,36 +120,14 @@ export class DrawingsManager {
}),
);
+ // todo: перенести в panе
window.addEventListener('pointerup', this.handlePointerUp);
window.addEventListener('pointercancel', this.handlePointerUp);
this.container.addEventListener('click', this.handleClick);
this.container.addEventListener('pointerdown', this.handlePointerDown);
this.container.addEventListener('dblclick', this.handleDoubleClick);
this.container.addEventListener('contextmenu', this.handleContextMenu);
- // todo: implement ctrl+v
- // hotkeys.register({
- // keys: [Keys.control, Keys.v],
- // callback: () => {
- // const bufferWithAppliedPosition = {
- // ...this.copyPasteBuffer,
- // state: {
- // ...this.copyPasteBuffer?.state,
- // startAnchor: {
- // price: 73.36210252637723,
- // time: 1783679170
- // }
- // }
- // }
- //
- // this.setSnapshot([
- // ...this.getSnapshot(),
- // bufferWithAppliedPosition
- // ])
- // // hotkeys.unregister({ // todo: unregister all else ctrl+c's
- // // keys: [Keys.control, Keys.c]
- // // })
- // }
- // })
+ this.container.addEventListener('contextmenu', this.handleContextMenu);
}
// TODO: handlePointerDown конфликтует с DrawingsManagerCollection.handlePaneClick
@@ -240,6 +218,7 @@ export class DrawingsManager {
};
private handleClick = (event: MouseEvent): void => {
+ // todo: move method to pane
if (!this.isEventInPane(event)) {
return;
}
@@ -248,6 +227,8 @@ export class DrawingsManager {
pendingDrawing?.getSeriesDrawing().click(event);
+ this.pane.setSelected();
+
this.DOM.refreshEntities();
};
@@ -561,11 +542,6 @@ export class DrawingsManager {
drawingName: name,
name: drawingLabelById()[name],
onDelete: this.removeDrawing,
- onCopy: () => {
- if (createdDrawing) {
- this.copyPasteBuffer = this.createDrawingSnapshot(createdDrawing);
- }
- },
zIndex: entityZIndex,
moveDown,
moveUp,
@@ -634,13 +610,20 @@ export class DrawingsManager {
drawings.push(
this.createDrawing({
name: item.drawingName,
- options: {
- id: item.id,
- state: cloneDeep(item.state),
- isLocked: item.isLocked,
- zIndex: item.zIndex,
- shouldUpdateDrawingsList: false,
- },
+ options:
+ item.id === COPPIED_DRWAING_ID
+ ? {
+ state: cloneDeep(item.state),
+ isLocked: item.isLocked,
+ shouldUpdateDrawingsList: false,
+ }
+ : {
+ id: item.id,
+ state: cloneDeep(item.state),
+ isLocked: item.isLocked,
+ zIndex: item.zIndex,
+ shouldUpdateDrawingsList: false,
+ },
}),
);
@@ -698,6 +681,16 @@ export class DrawingsManager {
this.removeDrawing(drawing.id);
}
+ public getSelectedDrawingSnapshot(): DrawingSnapshotItem | null {
+ const drawing = this.selectedDrawing$.value;
+
+ if (!drawing) {
+ return null;
+ }
+
+ return this.createDrawingSnapshot(drawing);
+ }
+
public toggleSelectedDrawingLock(): void {
const drawing = this.selectedDrawing$.value;
@@ -768,7 +761,6 @@ export class DrawingsManager {
this.drawings$.value.forEach((drawing) => drawing.destroy());
this.selectedDrawingSnapshot = null;
- this.copyPasteBuffer = null;
this.subscriptions.unsubscribe();
this.drawings$.complete();
diff --git a/src/core/DrawingsManagerCollection.ts b/src/core/DrawingsManagerCollection.ts
index 06359f4739f3b061fc75e939bb071ed0629aa35c..7586556d4255844363b00cc0fab7311d083fe5c1 100644
--- a/src/core/DrawingsManagerCollection.ts
+++ b/src/core/DrawingsManagerCollection.ts
@@ -1,21 +1,45 @@
-import { BehaviorSubject, Observable } from 'rxjs';
+import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { DrawingsNames } from '@src/constants';
import { DRAWING_KEYBOARD_SHORTCUTS, findDrawingPointerShortcut } from '@src/core/Drawings/shortcuts';
import { ActiveDrawingTool, SettingsValues } from '@src/types';
+import { ChartMouseEvents } from './ChartMouseEvents';
import { Drawing } from './Drawings';
-import { DrawingsManager } from './DrawingsManager';
+import { isAxisLineState } from './Drawings/axisLine/axisLine';
+import { isFibonacciRetracementState } from './Drawings/fibonacciRetracement/fibonacciRetracement';
+import { getPriceFromYCoordinate, getTimeFromXCoordinate } from './Drawings/helpers';
+import { isLineDrawingState } from './Drawings/line/lineDrawing';
+import { isRayState } from './Drawings/ray/ray';
+import { isRegressionTrendState } from './Drawings/regressionTrend/regressionTrend';
+import { isSliderPositionState } from './Drawings/sliderPosition/sliderPosition';
+import { isTextState } from './Drawings/text/text';
+import { isTraectoryState } from './Drawings/traectory/traectory';
+import { Anchor, SeriesApi } from './Drawings/types';
+import { DrawingsManager, DrawingSnapshotItem } from './DrawingsManager';
import { Hotkeys, Keys } from './Hotkeys';
import type { Pane } from './Pane';
+import type { IChartApi, MouseEventParams, Point, Time } from 'lightweight-charts';
-import type { MouseEventParams } from 'lightweight-charts';
+export interface DrawingsManagerCollectionParams {
+ hotkeys: Hotkeys;
+ selectedPane: Observable<Pane | null>;
+ subscribeChartEvent: ChartMouseEvents['subscribe'];
+ lwcChart: IChartApi;
+}
+
+export const COPPIED_DRWAING_ID = 'coppied';
-// todo: нужно дописывать класс)
export class DrawingsManagerCollection {
private managersMap: Map<number, DrawingsManager> = new Map();
private hotkeys: Hotkeys;
+ private lwcChart: IChartApi;
+ private selectedPane: Pane | null = null;
+ private copyPasteBuffer: DrawingSnapshotItem | null = null;
+ private cursorPos = new BehaviorSubject<null | Point>(null);
+ private hoveredPane = new BehaviorSubject<null | number>(null);
+ private copyPasteAnchor: Anchor | null = null;
private activeTool$ = new BehaviorSubject<ActiveDrawingTool>('crosshair');
private endlessMode$ = new BehaviorSubject(false);
@@ -23,26 +47,23 @@ export class DrawingsManagerCollection {
private isAwaitingDrawingStart = false;
private unregisterHotkeys: (() => void)[] = [];
- constructor({ hotkeys }: { hotkeys: Hotkeys }) {
- this.hotkeys = hotkeys;
+ private subscriptions = new Subscription();
- for (const { keys, drawingName } of DRAWING_KEYBOARD_SHORTCUTS) {
- this.unregisterHotkeys.push(
- this.hotkeys.register({
- keys,
- callback: () => {
- this.activateDrawingTool(drawingName);
- },
- }),
- );
- }
-
- this.unregisterHotkeys.push(
- this.hotkeys.register({
- keys: [Keys.escape],
- callback: this.activateCrosshair,
+ constructor({ hotkeys, selectedPane, subscribeChartEvent, lwcChart }: DrawingsManagerCollectionParams) {
+ this.hotkeys = hotkeys;
+ this.lwcChart = lwcChart;
+ selectedPane.subscribe((next) => {
+ this.selectedPane = next;
+ });
+
+ this.subscriptions.add(
+ subscribeChartEvent('crosshairMove', (param) => {
+ this.cursorPos.next(param.point ?? null);
+ this.hoveredPane.next(param.paneIndex ?? null);
}),
);
+
+ this.hotkeysSetup();
}
public getIsEndlessMode(): boolean {
@@ -150,9 +171,237 @@ export class DrawingsManagerCollection {
this.endlessMode$.complete();
}
+ public copy() { // public only for tests and this is sad
+ const pane = this.selectedPane;
+ if (!pane || this.hoveredPane.value !== pane.paneIndex()) {
+ return;
+ }
+
+ const drawingManager = pane.getDrawingManager();
+
+ const yCoord = this.cursorPos.value?.y;
+ const xCoord = this.cursorPos.value?.x;
+
+ if (yCoord === undefined || xCoord === undefined) {
+ this.copyPasteBuffer = null;
+ return;
+ }
+
+ this.copyPasteBuffer = drawingManager.getSelectedDrawingSnapshot();
+ this.copyPasteAnchor = {
+ time: <Time>getTimeFromXCoordinate(this.lwcChart, xCoord),
+ price: <number>getPriceFromYCoordinate(pane.getLocalMainSeries() as SeriesApi, yCoord),
+ };
+ }
+
+ public paste() { // public only for tests and this is sad
+ const pane = this.selectedPane;
+ if (!pane || this.hoveredPane.value !== pane.paneIndex()) {
+ return;
+ }
+
+ const drawingManager = pane.getDrawingManager();
+
+ const drawingToPaste = this.copyPasteBuffer;
+
+ if (!drawingToPaste) {
+ return;
+ }
+
+ const { state } = drawingToPaste;
+
+ const yCoord = this.cursorPos.value?.y;
+ const xCoord = this.cursorPos.value?.x;
+
+ if (yCoord === undefined || xCoord === undefined || !this.copyPasteAnchor) {
+ return;
+ }
+
+ const { time: prevTime, price: prevPrice } = this.copyPasteAnchor;
+
+ const shift = {
+ priceDiff: <number>getPriceFromYCoordinate(pane.getLocalMainSeries() as SeriesApi, yCoord) - prevPrice,
+ timeDiff: <number>getTimeFromXCoordinate(this.lwcChart, xCoord) - <number>prevTime,
+ };
+
+ const shiftedState = shiftDrawingStateCoordsTo(state, shift);
+
+ const bufferWithAppliedPosition: DrawingSnapshotItem = {
+ ...drawingToPaste,
+ id: COPPIED_DRWAING_ID,
+ zIndex: drawingToPaste.zIndex,
+ state: shiftedState,
+ };
+
+ drawingManager.setSnapshot([...drawingManager.getSnapshot(), bufferWithAppliedPosition]);
+ }
+
private cancelPendingDrawings(): void {
for (const manager of this.managersMap.values()) {
manager.cancelPendingDrawing();
}
}
+
+ private hotkeysSetup(): void {
+ for (const { keys, drawingName } of DRAWING_KEYBOARD_SHORTCUTS) {
+ this.unregisterHotkeys.push(
+ this.hotkeys.register({
+ keys,
+ callback: () => {
+ this.activateDrawingTool(drawingName);
+ },
+ }),
+ );
+ }
+
+ this.unregisterHotkeys.push(
+ this.hotkeys.register({
+ keys: [Keys.mod, Keys.c],
+ callback: () => this.copy(),
+ }),
+ );
+
+ this.unregisterHotkeys.push(
+ this.hotkeys.register({
+ keys: [Keys.mod, Keys.v],
+ callback: () => this.paste(),
+ }),
+ );
+
+ this.unregisterHotkeys.push(
+ this.hotkeys.register({
+ keys: [Keys.escape],
+ callback: this.activateCrosshair,
+ }),
+ );
+ }
+}
+
+export function shiftDrawingStateCoordsTo(state: unknown, shift: { timeDiff: number; priceDiff: number }) {
+ if (isLineDrawingState(state)) {
+ // LineDrawing, ParallelChannelState, VolumeProfileState
+ const startAnchor = {
+ time: ((state.startAnchor?.time as number) + shift.timeDiff) as Time,
+ price: (state.startAnchor?.price as number) + shift.priceDiff,
+ };
+ const endAnchor = {
+ time: ((state.endAnchor?.time as number) + shift.timeDiff) as Time,
+ price: (state.endAnchor?.price as number) + shift.priceDiff,
+ };
+ return {
+ ...state,
+ startAnchor,
+ endAnchor,
+ };
+ }
+ if (isRayState(state)) {
+ // Ray
+ const startAnchor = {
+ time: ((state.startAnchor?.time as number) + shift.timeDiff) as Time,
+ price: (state.startAnchor?.price as number) + shift.priceDiff,
+ };
+
+ const directionAnchor = {
+ time: ((state.directionAnchor?.time as number) + shift.timeDiff) as Time,
+ price: (state.directionAnchor?.price as number) + shift.priceDiff,
+ };
+
+ return {
+ ...state,
+ startAnchor,
+ directionAnchor,
+ };
+ }
+
+ if (isSliderPositionState(state)) {
+ // SliderPositionState
+ const startTime = ((state.startTime as number) + shift.timeDiff) as Time;
+ const endTime = ((state.endTime as number) + shift.timeDiff) as Time;
+ const entryPrice = (state.entryPrice as number) + shift.priceDiff;
+ const stopPrice = (state.stopPrice as number) + shift.priceDiff;
+ const targetPrice = (state.targetPrice as number) + shift.priceDiff;
+
+ return {
+ ...state,
+ startTime,
+ endTime,
+ entryPrice,
+ stopPrice,
+ targetPrice,
+ };
+ }
+ if (isFibonacciRetracementState(state)) {
+ // FibonacciRetracement, DiapsonState, RectangleState
+ const startTime = ((state.startTime as number) + shift.timeDiff) as Time;
+ const endTime = ((state.endTime as number) + shift.timeDiff) as Time;
+ const startPrice = (state.startPrice as number) + shift.priceDiff;
+ const endPrice = (state.endPrice as number) + shift.priceDiff;
+
+ return {
+ ...state,
+ startTime,
+ endTime,
+ startPrice,
+ endPrice,
+ };
+ }
+ if (isRegressionTrendState(state)) {
+ // RegressionTrend
+ const startTime = ((state.startTime as number) + shift.timeDiff) as Time;
+ const endTime = ((state.endTime as number) + shift.timeDiff) as Time;
+
+ return {
+ ...state,
+ startTime,
+ endTime,
+ };
+ }
+
+ if (isAxisLineState(state)) {
+ // AxisLine
+ if (state.time === null) {
+ return {
+ ...state,
+ price: (state.price as number) + shift.priceDiff,
+ };
+ }
+
+ return {
+ ...state,
+ time: ((state.time as number) + shift.timeDiff) as Time,
+ };
+ }
+
+ if (isTextState(state)) {
+ // TextState
+ const point = {
+ time: ((state.point?.time as number) + shift.timeDiff) as Time,
+ price: (state.point?.price as number) + shift.priceDiff,
+ };
+
+ return {
+ ...state,
+ point,
+ };
+ }
+
+ if (isTraectoryState(state)) {
+ // TraectoryState
+
+ const points = state.points.map((point) => {
+ const anchor = {
+ time: ((point.time as number) + shift.timeDiff) as Time,
+ price: (point.price as number) + shift.priceDiff,
+ };
+
+ return anchor;
+ });
+
+ return {
+ ...state,
+ points,
+ };
+ }
+
+ return state;
}
diff --git a/src/core/Indicators/index.tsx b/src/core/Indicators/index.tsx
index aadeff35ba5b32315086118eebfe5152ffbbd80a..60c57bdf05b9f76eec104f60620fcf3727cb9c5f 100644
--- a/src/core/Indicators/index.tsx
+++ b/src/core/Indicators/index.tsx
@@ -1,4 +1,4 @@
-import { PriceScaleMode, SeriesType } from 'lightweight-charts';
+import { AutoscaleInfo, PriceScaleMode, SeriesType } from 'lightweight-charts';
import { Indicator } from '@core/Indicator';
import { emaIndicator } from '@core/Indicators/ema';
@@ -50,6 +50,18 @@ export const indicatorsMap = (): Partial<Record<IndicatorsIds, IndicatorConfig>>
priceFormat: {
type: 'volume',
},
+ autoscaleInfoProvider: (baseImplementation: () => AutoscaleInfo | null) => {
+ const res = baseImplementation();
+ if (!res || !res.priceRange || (res.priceRange.maxValue === 0 && res.priceRange.minValue === 0)) {
+ return {
+ priceRange: {
+ minValue: 0,
+ maxValue: 1,
+ },
+ };
+ }
+ return res;
+ },
},
dataFormatter: (params) => volume(params as IndicatorDataFormatter<'Histogram'>),
},
diff --git a/src/core/MoexChart.tsx b/src/core/MoexChart.tsx
index 7f6258f5392ba17db04f9f442a08313b5d4fb774..a5496358ab9bb037bdf03adf6bdb1463c6c987b2 100644
--- a/src/core/MoexChart.tsx
+++ b/src/core/MoexChart.tsx
@@ -8,6 +8,7 @@ import { DataSource, DataSourceParams } from '@core/DataSource';
import { Hotkeys, Keys } from '@core/Hotkeys';
import { ModalRenderer } from '@core/ModalRenderer';
import { FloatingDrawingToolbar } from '@src/components/FloatingToolbar';
+import { Loader } from '@src/components/Loader';
import { SettingsModal } from '@src/components/SettingsModal';
import Toolbar from '@src/components/Toolbar';
@@ -110,6 +111,7 @@ export class MoexChart implements ISerializable<MoexChartSnapshot> {
private headerRenderer!: UIRenderer;
private modalRenderer!: ModalRenderer;
private toolbarRenderer: UIRenderer | undefined;
+ private loaderRenderer: UIRenderer | undefined;
private controlBarRenderer?: UIRenderer;
private footerRenderer?: UIRenderer;
private drawingToolbarRenderer!: UIRenderer;
@@ -173,6 +175,7 @@ export class MoexChart implements ISerializable<MoexChartSnapshot> {
controlBarContainer,
drawingToolbarContainer,
footerContainer,
+ loaderContainer,
toggleToolbar, // todo: move this function to toolbarModel
} = ContainerManager.createContainers({
parentContainer: this.rootContainer,
@@ -243,8 +246,17 @@ export class MoexChart implements ISerializable<MoexChartSnapshot> {
this.headerRenderer = new ReactRenderer(headerContainer);
this.toolbarRenderer = new ReactRenderer(toolBarContainer);
+ this.loaderRenderer = new ReactRenderer(loaderContainer);
this.drawingToolbarRenderer = new ReactRenderer(drawingToolbarContainer);
+ this.dataSource.isLoading().subscribe((isLoading) => {
+ if (isLoading) {
+ this.loaderRenderer?.renderComponent(<Loader />);
+ } else {
+ this.loaderRenderer?.clear();
+ }
+ });
+
if (config.chartCollectionPreset.showControlBar) {
this.controlBarRenderer = new ReactRenderer(controlBarContainer);
}
diff --git a/src/core/Pane.tsx b/src/core/Pane.tsx
index 0484870b14f536ffdf4d930fc32eccb47c1a5545..df0333525801649e77acf2a3b51262d15fe39d37 100644
--- a/src/core/Pane.tsx
+++ b/src/core/Pane.tsx
@@ -58,6 +58,7 @@ export interface PaneParams {
getActiveTool: () => ActiveDrawingTool;
getIsEndlessMode: () => boolean;
continueDrawing: (name: DrawingsNames) => void;
+ deselectAllPanes: () => void;
}
// todo: Pane, ему должна принадлежать mainSerie, а также IndicatorManager и drawingsManager, mouseEvents. Также перекинуть соответствующие/необходимые свойства из чарта, и из чарта удалить
@@ -94,6 +95,8 @@ export class Pane implements ISerializable<PaneSnapshot> {
private readonly onPriceScaleStateChange: () => void;
private readonly subscriptions = new Subscription();
private paneContainerSyncFrameId: number | null = null;
+ private isSelected = new BehaviorSubject<boolean>(false);
+ private deselectAllPanes: () => void;
constructor({
lwcChart,
@@ -120,6 +123,7 @@ export class Pane implements ISerializable<PaneSnapshot> {
getActiveTool,
getIsEndlessMode,
continueDrawing,
+ deselectAllPanes,
}: PaneParams) {
this.onDelete = onDelete;
this.onPriceScaleStateChange = onPriceScaleStateChange;
@@ -129,6 +133,7 @@ export class Pane implements ISerializable<PaneSnapshot> {
this.subscribeChartEvent = subscribeChartEvent;
this.isMain = isMainPane;
this.id = id;
+ this.deselectAllPanes = deselectAllPanes;
if (isMainPane) {
this.lwcPane = this.lwcChart.panes()[MAIN_PANE_INDEX];
@@ -216,8 +221,25 @@ export class Pane implements ISerializable<PaneSnapshot> {
this.legendContainer.style.display = hasRuler ? 'none' : '';
}),
);
+
+ this.subscriptions.add(
+ this.drawingsManager.entities().subscribe((drawings) => {
+ const hasRuler = drawings.some((drawing) => drawing.getDrawingName() === DrawingsNames.ruler);
+
+ this.legendContainer.style.display = hasRuler ? 'none' : '';
+ }),
+ );
}
+ public setSelected = () => {
+ this.deselectAllPanes();
+ this.isSelected.next(true);
+ };
+
+ public deselected = () => {
+ this.isSelected.next(false);
+ };
+
public getLocalMainSeries = () => {
return this.localMainSeries.value;
};
diff --git a/src/core/PaneManager.ts b/src/core/PaneManager.ts
index a2e59cbc7ffd88a421c57287c24a23be18694fa3..d27f85e29debf0ec752ed4ec7bfea9244fb317d9 100644
--- a/src/core/PaneManager.ts
+++ b/src/core/PaneManager.ts
@@ -1,4 +1,4 @@
-import { type Observable, Subscription } from 'rxjs';
+import { BehaviorSubject, type Observable, Subscription } from 'rxjs';
import { DataSource } from '@core/DataSource';
import { DrawingsManagerSnapshot } from '@core/DrawingsManager';
@@ -29,7 +29,8 @@ type CollectionDependencies =
| 'setActiveTool'
| 'getActiveTool'
| 'getIsEndlessMode'
- | 'continueDrawing';
+ | 'continueDrawing'
+ | 'deselectAllPanes';
interface PaneManagerStartParams {
compareEntities$: Observable<Indicator[]>;
@@ -48,6 +49,7 @@ export class PaneManager implements ISerializable<PaneSnapshot[]> {
private readonly lwcChart: IChartApi;
private readonly panesMap = new Map<number, Pane>();
+ private selectedPane = new BehaviorSubject<Pane | null>(null);
private mainPane: Pane;
private nextPaneId: number;
private priceAxisLabels: PriceAxisLabels | null = null;
@@ -57,6 +59,13 @@ export class PaneManager implements ISerializable<PaneSnapshot[]> {
private readonly subscriptions = new Subscription();
constructor({ panesSnapshot, ...sharedPaneParams }: PaneManagerParams) {
+ this.drawingsManagerCollection = new DrawingsManagerCollection({
+ hotkeys: sharedPaneParams.hotkeys,
+ selectedPane: this.selectedPane.asObservable(),
+ subscribeChartEvent: sharedPaneParams.subscribeChartEvent,
+ lwcChart: sharedPaneParams.lwcChart,
+ });
+
this.sharedPaneParams = {
...sharedPaneParams,
};
@@ -65,10 +74,6 @@ export class PaneManager implements ISerializable<PaneSnapshot[]> {
const mainPaneSnapshot = panesSnapshot.find((paneSnapshot) => paneSnapshot.isMain);
const mainPaneId = mainPaneSnapshot?.id ?? 0;
- this.drawingsManagerCollection = new DrawingsManagerCollection({
- hotkeys: sharedPaneParams.hotkeys,
- });
-
this.mainPane = new Pane({
...this.sharedPaneParams,
...this.getCollectionDependencies(),
@@ -229,19 +234,25 @@ export class PaneManager implements ISerializable<PaneSnapshot[]> {
this.panesMap.clear();
}
+ private deselectAllPanes(): void {
+ this.panesMap.forEach((pane) => {
+ pane.deselected();
+ });
+ }
+
private initClickListener(): void {
const handler = (param: MouseEventParams) => {
if (param.paneIndex === undefined) {
return;
}
- const clickedPane = this.getPaneByIndex(param.paneIndex);
+ this.selectedPane.next(this.getPaneByIndex(param.paneIndex) ?? null);
- if (!clickedPane) {
+ if (!this.selectedPane.value) {
return;
}
- this.drawingsManagerCollection.handlePaneClick(clickedPane, param);
+ this.drawingsManagerCollection.handlePaneClick(this.selectedPane.value, param);
};
this.lwcChart.subscribeClick(handler);
@@ -261,6 +272,10 @@ export class PaneManager implements ISerializable<PaneSnapshot[]> {
return;
}
+ if (this.selectedPane.value?.getId() === id) {
+ this.selectedPane.next(null);
+ }
+
const paneIndex = pane.paneIndex();
this.panesMap.delete(id);
@@ -291,6 +306,7 @@ export class PaneManager implements ISerializable<PaneSnapshot[]> {
getActiveTool: () => this.drawingsManagerCollection.getActiveToolValue(),
getIsEndlessMode: () => this.drawingsManagerCollection.getIsEndlessMode(),
continueDrawing: (name: DrawingsNames) => this.drawingsManagerCollection.activateDrawingTool(name),
+ deselectAllPanes: () => this.deselectAllPanes(),
};
}
diff --git a/src/core/SymbolSource.ts b/src/core/SymbolSource.ts
index 7d478ebb1b6038a9745c3138ce4c550d79e1e886..86f5901bece0e4baf0811dd0ffb216dd3b2d09b5 100644
--- a/src/core/SymbolSource.ts
+++ b/src/core/SymbolSource.ts
@@ -87,6 +87,10 @@ export class SymbolSource {
return this.isLoadingSubject.value;
}
+ public isLoading = (): Observable<boolean> => {
+ return this.isLoadingSubject.asObservable();
+ };
+
public getOldestTime(): number | null {
return this.oldestCandle?.time ?? null;
}
diff --git a/src/core/__tests__/DrawingsManagerCollection.test.ts b/src/core/__tests__/DrawingsManagerCollection.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f8700c4e00d12686c373eb75deeb776c33a81a99
--- /dev/null
+++ b/src/core/__tests__/DrawingsManagerCollection.test.ts
@@ -0,0 +1,420 @@
+import { IChartApi, MouseEventParams } from 'lightweight-charts';
+import { BehaviorSubject } from 'rxjs';
+
+import { DrawingsManager, DrawingsManagerParams } from '@core/DrawingsManager';
+import {
+ DrawingsManagerCollection,
+ DrawingsManagerCollectionParams,
+ shiftDrawingStateCoordsTo,
+} from '@core/DrawingsManagerCollection';
+import { Pane } from '@core/Pane';
+
+import { DrawingsNames } from '@src/constants';
+
+import { SettingValue } from '@src/types';
+
+import { Hotkeys } from '../Hotkeys';
+import { createEvent } from '../Drawings/__tests__/shortcuts.test';
+
+jest.mock('@core/DrawingsManager');
+
+describe('DrawingsManagerCollection', () => {
+ const DrawingManagerMock = DrawingsManager as jest.MockedClass<typeof DrawingsManager>;
+
+ let collection: DrawingsManagerCollection;
+ let pane: Pane;
+ const cancelPendingDrawing = jest.fn(() => {});
+ const startDrawing = jest.fn(() => {});
+ const openSelectedDrawingSettings = jest.fn(() => {});
+ const deleteSelectedDrawing = jest.fn(() => {});
+ const updateSelectedDrawingSettings = jest.fn(() => {});
+ const toggleSelectedDrawingLock = jest.fn(() => {});
+ const paneIndex = jest.fn(() => { 0 });
+ const selectedPane = new BehaviorSubject({
+ getId: jest.fn(() => 0),
+ paneIndex,
+ } as unknown as Pane | null);
+
+ beforeEach(() => {
+ DrawingManagerMock.mockImplementation(
+ (params) =>
+ ({
+ startDrawing,
+ cancelPendingDrawing,
+ openSelectedDrawingSettings,
+ deleteSelectedDrawing,
+ updateSelectedDrawingSettings,
+ toggleSelectedDrawingLock,
+ paneIndex,
+ }) as unknown as DrawingsManager,
+ );
+ pane = {
+ getDrawingManager: () => new DrawingsManager({} as DrawingsManagerParams),
+ } as Pane;
+
+ const hotkeys = {
+ register: () => {},
+ } as unknown as Hotkeys;
+
+ const subscribeChartEvent = jest.fn((event, handler) => {
+ return () => {};
+ }) as unknown as DrawingsManagerCollectionParams['subscribeChartEvent'];
+
+ const lwcChart = {} as IChartApi;
+
+ collection = new DrawingsManagerCollection({
+ hotkeys,
+ selectedPane,
+ subscribeChartEvent,
+ lwcChart,
+ });
+ });
+
+it('Менеджер должен добавляться в колекцию', () => {
+ expect(collection['managersMap'].size).toBe(0);
+
+ const drawingManager = new DrawingsManager({} as DrawingsManagerParams);
+ collection.addDrawingManager(drawingManager, 1);
+
+ expect(collection['managersMap'].size).toBe(1);
+ });
+
+ it('Менеджер должен удаляться из колекции', () => {
+ const drawingManager = new DrawingsManager({} as DrawingsManagerParams);
+ collection.addDrawingManager(drawingManager, 1);
+ collection.removeDrawingManager(1);
+
+ expect(collection['managersMap'].size).toBe(0);
+ });
+
+ it('Активация drawingTool должена вызывать методы drawingManager', () => {
+ const drawingManager = new DrawingsManager({} as DrawingsManagerParams);
+ collection.addDrawingManager(drawingManager, 1);
+
+ collection.activateDrawingTool(DrawingsNames.trendLine);
+ expect(collection['activeTool$'].value).toBe(DrawingsNames.trendLine);
+ expect(cancelPendingDrawing).toHaveBeenCalledTimes(1);
+
+ collection.activateDrawingTool(DrawingsNames.traectory);
+ expect(collection['activeTool$'].value).toBe(DrawingsNames.traectory);
+ expect(cancelPendingDrawing).toHaveBeenCalledTimes(2);
+ });
+
+ it('Активация activateCrosshair должна переключать activeTool', () => {
+ const drawingManager = new DrawingsManager({} as DrawingsManagerParams);
+ collection.addDrawingManager(drawingManager, 1);
+
+ collection.activateCrosshair();
+ expect(cancelPendingDrawing).toHaveBeenCalledTimes(1);
+ expect(collection.getActiveToolValue()).toBe('crosshair');
+ });
+
+ it('Активация activateCrosshair должна переключать activeTool', () => {
+ const drawingManager = new DrawingsManager({} as DrawingsManagerParams);
+ collection.addDrawingManager(drawingManager, 1);
+
+ collection.activateCrosshair();
+ expect(cancelPendingDrawing).toHaveBeenCalledTimes(1);
+ expect(collection.getActiveToolValue()).toBe('crosshair');
+ });
+
+ it('Установка endlessDrawingMode', () => {
+ collection.setEndlessDrawingMode(true);
+ expect(collection.getIsEndlessMode()).toBe(true);
+ collection.setEndlessDrawingMode(true);
+ expect(collection.getIsEndlessMode()).toBe(true);
+ });
+
+ it('handlePaneClick должен начинать рисование в соответстующем DrawingManager', () => {
+ collection.activateDrawingTool(DrawingsNames.verticalLine);
+ const e = {} as MouseEventParams;
+ collection.handlePaneClick(pane, {} as MouseEventParams);
+ expect(startDrawing).toBeCalledWith(DrawingsNames.verticalLine, e);
+ expect(startDrawing).toBeCalledTimes(1);
+
+ collection.handlePaneClick(pane, {} as MouseEventParams);
+ expect(startDrawing).toBeCalledTimes(1);
+
+ collection.activateCrosshair()
+ collection.handlePaneClick(pane, createEvent({}));
+ expect(startDrawing).toBeCalledTimes(1);
+ });
+
+ it('Should open selected drawing settings', () => {
+ const drawingManager = new DrawingsManager({} as DrawingsManagerParams);
+ collection.addDrawingManager(drawingManager, 1);
+
+ collection.openSelectedDrawingSettings();
+ expect(openSelectedDrawingSettings).toHaveBeenCalledTimes(1);
+ });
+
+ it('Should delete selected drawings', () => {
+ const drawingManager = new DrawingsManager({} as DrawingsManagerParams);
+ collection.addDrawingManager(drawingManager, 1);
+
+ collection.deleteSelectedDrawing();
+ expect(deleteSelectedDrawing).toHaveBeenCalledTimes(1);
+ });
+
+ it('Should delete selected drawings', () => {
+ const drawingManager = new DrawingsManager({} as DrawingsManagerParams);
+ collection.addDrawingManager(drawingManager, 1);
+ collection.toggleSelectedDrawingLock();
+
+ expect(toggleSelectedDrawingLock).toHaveBeenCalledTimes(1);
+ });
+
+ it('Should update selected drawing settings', () => {
+ const drawingManager = new DrawingsManager({} as DrawingsManagerParams);
+ collection.addDrawingManager(drawingManager, 1);
+ const settings = {} as SettingValue;
+
+ collection.updateSelectedDrawingSettings(settings);
+ expect(updateSelectedDrawingSettings).toHaveBeenCalledWith(settings);
+ });
+
+ it('copy should work', () => {
+ const drawingManager = new DrawingsManager({} as DrawingsManagerParams);
+ collection.addDrawingManager(drawingManager, 1);
+
+ collection.copy();
+ expect(paneIndex).toHaveBeenCalledTimes(1);
+ expect(collection['copyPasteBuffer']).toBe(null);
+ })
+
+ it('paste should work', () => {
+ const drawingManager = new DrawingsManager({} as DrawingsManagerParams);
+ collection.addDrawingManager(drawingManager, 1);
+
+ collection.paste();
+ expect(collection['copyPasteBuffer']).toBe(null);
+ });
+
+ it('ray coords shift', () => {
+ const shift = {
+ timeDiff: 9432,
+ priceDiff: 156,
+ };
+ const rayState = {
+ hidden: false,
+ mode: 'idle',
+ startAnchor: {
+ time: 503200,
+ price: 12,
+ },
+ directionAnchor: {
+ time: 50000,
+ price: 23,
+ },
+ settings: {},
+ };
+
+ expect(shiftDrawingStateCoordsTo(rayState, shift)).toEqual({
+ ...rayState,
+ startAnchor: {
+ time: rayState.startAnchor.time + shift.timeDiff,
+ price: rayState.startAnchor.price + shift.priceDiff,
+ },
+ directionAnchor: {
+ time: rayState.directionAnchor.time + shift.timeDiff,
+ price: rayState.directionAnchor.price + shift.priceDiff,
+ },
+ });
+ });
+
+ it('LineDrawing coords shift', () => {
+ const shift = {
+ timeDiff: 9432,
+ priceDiff: 156,
+ };
+ const rayState = {
+ hidden: false,
+ mode: 'idle',
+ startAnchor: {
+ time: 5032400,
+ price: 142,
+ },
+ endAnchor: {
+ time: 500540,
+ price: 243,
+ },
+ settings: {},
+ };
+
+ expect(shiftDrawingStateCoordsTo(rayState, shift)).toEqual({
+ ...rayState,
+ startAnchor: {
+ time: rayState.startAnchor.time + shift.timeDiff,
+ price: rayState.startAnchor.price + shift.priceDiff,
+ },
+ endAnchor: {
+ time: rayState.endAnchor.time + shift.timeDiff,
+ price: rayState.endAnchor.price + shift.priceDiff,
+ },
+ });
+ });
+
+ it('SliderPosition coords shift', () => {
+ const shift = {
+ timeDiff: 9432,
+ priceDiff: 156,
+ };
+ const sliderPositionState = {
+ hidden: false,
+ mode: 'idle',
+ startTime: 3263,
+ endTime: 32532,
+ entryPrice: 50,
+ stopPrice: 60,
+ targetPrice: 70,
+ settings: {},
+ };
+
+ expect(shiftDrawingStateCoordsTo(sliderPositionState, shift)).toEqual({
+ ...sliderPositionState,
+ startTime: sliderPositionState.startTime + shift.timeDiff,
+ endTime: sliderPositionState.endTime + shift.timeDiff,
+ entryPrice: sliderPositionState.entryPrice + shift.priceDiff,
+ stopPrice: sliderPositionState.stopPrice + shift.priceDiff,
+ targetPrice: sliderPositionState.targetPrice + shift.priceDiff,
+ });
+ });
+
+ it('FibonacciRetracement coords shift', () => {
+ const shift = {
+ timeDiff: 9432,
+ priceDiff: 156,
+ };
+ const fibonacciRetracementState = {
+ hidden: false,
+ mode: 'idle',
+ startTime: 503200000,
+ endTime: 50000000,
+ startPrice: 12,
+ endPrice: 53,
+ settings: {},
+ };
+
+ expect(shiftDrawingStateCoordsTo(fibonacciRetracementState, shift)).toEqual({
+ ...fibonacciRetracementState,
+ startTime: fibonacciRetracementState.startTime + shift.timeDiff,
+ endTime: fibonacciRetracementState.endTime + shift.timeDiff,
+ startPrice: fibonacciRetracementState.startPrice + shift.priceDiff,
+ endPrice: fibonacciRetracementState.endPrice + shift.priceDiff,
+ });
+ });
+
+ it('RegressionTrend coords shift', () => {
+ const shift = {
+ timeDiff: 9432,
+ priceDiff: 156,
+ };
+ const regressionTrendState = {
+ hidden: false,
+ mode: 'idle',
+ startTime: 234,
+ endTime: 987,
+ settings: {},
+ };
+
+ expect(shiftDrawingStateCoordsTo(regressionTrendState, shift)).toEqual({
+ ...regressionTrendState,
+ startTime: regressionTrendState.startTime + shift.timeDiff,
+ endTime: regressionTrendState.endTime + shift.timeDiff,
+ });
+ });
+
+ it('AxisLine coords shift', () => {
+ const shift = {
+ timeDiff: 9432,
+ priceDiff: 156,
+ };
+
+ const vertAxisLineState = {
+ hidden: false,
+ mode: 'idle',
+ time: 234,
+ price: null,
+ settings: {},
+ };
+
+ const horzAxisLineState = {
+ hidden: false,
+ mode: 'idle',
+ time: null,
+ price: 457,
+ settings: {},
+ };
+
+ expect(shiftDrawingStateCoordsTo(vertAxisLineState, shift)).toEqual({
+ ...vertAxisLineState,
+ time: vertAxisLineState.time + shift.timeDiff,
+ });
+ expect(shiftDrawingStateCoordsTo(horzAxisLineState, shift)).toEqual({
+ ...horzAxisLineState,
+ price: horzAxisLineState.price + shift.priceDiff,
+ });
+ });
+
+ it('TextState coords shift', () => {
+ const shift = {
+ timeDiff: 9432,
+ priceDiff: 156,
+ };
+
+ const textState = {
+ hidden: false,
+ mode: 'idle',
+ point: {
+ time: 100500,
+ price: 366666,
+ },
+ settings: {},
+ };
+
+ expect(shiftDrawingStateCoordsTo(textState, shift)).toEqual({
+ ...textState,
+ point: {
+ time: textState.point.time + shift.timeDiff,
+ price: textState.point.price + shift.priceDiff,
+ },
+ });
+ });
+
+ it('TraectoryState coords shift', () => {
+ const shift = {
+ timeDiff: 9432,
+ priceDiff: 156,
+ };
+
+ const traectoryState = {
+ hidden: false,
+ mode: 'idle',
+ points: [
+ {
+ time: 100500,
+ price: 366666,
+ },
+ {
+ time: 100502,
+ price: 34279,
+ },
+ {
+ time: 100,
+ price: 3666,
+ },
+ ],
+ settings: {},
+ };
+
+ expect(shiftDrawingStateCoordsTo(traectoryState, shift)).toEqual({
+ ...traectoryState,
+ points: traectoryState.points.map((point) => {
+ return {
+ time: point.time + shift.timeDiff,
+ price: point.price + shift.priceDiff,
+ };
+ }),
+ });
+ });
+});
diff --git a/src/core/__tests__/Pane.test.ts b/src/core/__tests__/Pane.test.ts
index d425fb524776ca1157d81e679d9f23f978d1ffd2..61755d7ae33dd47bab015a96d81e5c09b15b7980 100644
--- a/src/core/__tests__/Pane.test.ts
+++ b/src/core/__tests__/Pane.test.ts
@@ -160,6 +160,7 @@ describe('Pane', () => {
getActiveTool: jest.fn(),
getIsEndlessMode: jest.fn(),
continueDrawing: jest.fn(),
+ deselectAllPanes: jest.fn(),
});
}
diff --git a/src/core/__tests__/PaneManager.test.ts b/src/core/__tests__/PaneManager.test.ts
index 79adee94c2eee2d7958599a338f0ce1f4a08b7e9..afe414bd1e4b777106b96f4860274f66216882d6 100644
--- a/src/core/__tests__/PaneManager.test.ts
+++ b/src/core/__tests__/PaneManager.test.ts
@@ -8,6 +8,7 @@ import { PriceAxisLabels } from '@core/PriceAxisLabels';
import { IndicatorsIds } from '@src/constants';
import { PaneSnapshot } from '@src/types/snapshot';
+import { Direction } from '@src/types';
jest.mock('@core/Pane');
jest.mock('@core/DrawingsManagerCollection');
@@ -125,4 +126,69 @@ describe('PaneManager', () => {
manager.destroy();
});
+
+ it('Выставление шкал', () => {
+ const manager = createManager([
+ {
+ isMain: true,
+ id: 0,
+ indicators: [],
+ drawings: [],
+ },
+ {
+ isMain: false,
+ id: 1,
+ indicators: [],
+ drawings: [],
+ },
+ {
+ isMain: false,
+ id: 2,
+ indicators: [
+ {
+ indicatorType: IndicatorsIds.RSI,
+ },
+ ],
+ drawings: [],
+ },
+ ]);
+ manager.setPriceScaleSideVisible(Direction.Left, true)
+
+ expect(manager['leftPriceScaleVisible']).toBe(true)
+ expect(manager['rightPriceScaleVisible']).toBe(true)
+ expect(manager.getMainPane().getPriceScale(Direction.Left)).toBeDefined()
+
+ manager.destroy();
+ });
+
+ it('геттер пейнов', () => {
+ const manager = createManager([
+ {
+ isMain: true,
+ id: 0,
+ indicators: [],
+ drawings: [],
+ },
+ {
+ isMain: false,
+ id: 1,
+ indicators: [],
+ drawings: [],
+ },
+ {
+ isMain: false,
+ id: 2,
+ indicators: [
+ {
+ indicatorType: IndicatorsIds.RSI,
+ },
+ ],
+ drawings: [],
+ },
+ ]);
+
+ expect(manager.getPaneByIndex(0)).toMatchObject(manager.getMainPane())
+
+ manager.destroy();
+ });
});