Загрузка данных


diff --git a/src/core/ContainerManager.ts b/src/core/ContainerManager.ts
index 4b42ac0ef55869ed9c67435b69abc4b56030694c..5302f4e1f747428caaf9ae14d5e1172a3bab13b8 100644
--- a/src/core/ContainerManager.ts
+++ b/src/core/ContainerManager.ts
@@ -103,7 +103,7 @@ export class ContainerManager {
     controlBarContainer.style.left = '50%';
     controlBarContainer.style.transform = 'translateX(-50%)';
     controlBarContainer.style.bottom = 'var(--space-2000)';
-    controlBarContainer.style.zIndex = ZIndex.Base;
+    controlBarContainer.style.zIndex = ZIndex.Floating;
 
     const drawingToolbarContainer = document.createElement('div');
     drawingToolbarContainer.classList.add(CHART_DRAWING_TOOLBAR_CONTAINER);
diff --git a/src/core/Drawings.ts b/src/core/Drawings.ts
index 6df1b9712de194f5d6c43c272c9c6d12cab099fb..1909a72a21eb3baea679846ec430bf486525e42b 100644
--- a/src/core/Drawings.ts
+++ b/src/core/Drawings.ts
@@ -8,7 +8,7 @@ import { DrawingsNames } from '@src/constants';
 import { SeriesStrategies } from '@src/modules/series-strategies/SeriesFactory';
 import { SettingsTab, SettingsValues, ToolbarSettingField } from '@src/types/settings';
 
-import type { DrawingInteraction, ISeriesDrawing } from '@src/core/Drawings/SeriesDrawingBase';
+import type { DrawingInteraction, ISeriesDrawing } from '@src/core/Drawings/base/DrawingBase';
 
 interface DrawingParams extends DOMObjectParams {
   drawingName: DrawingsNames;
diff --git a/src/core/Drawings/__tests__/SeriesDrawingBase.test.ts b/src/core/Drawings/__tests__/SeriesDrawingBase.test.ts
deleted file mode 100644
index 96c08569669d0bdd9a3a90e2d58071f4e42749b9..0000000000000000000000000000000000000000
--- a/src/core/Drawings/__tests__/SeriesDrawingBase.test.ts
+++ /dev/null
@@ -1,573 +0,0 @@
-import { BehaviorSubject } from 'rxjs';
-
-import { DrawingHandlesPrimitive } from '@core/Drawings/handles';
-import { SeriesDrawingBase } from '@core/Drawings/SeriesDrawingBase';
-
-import type { DrawingHandle } from '@core/Drawings/handles';
-import type { DrawingInteraction } from '@core/Drawings/SeriesDrawingBase';
-import type { AxisLabel, AxisSegment, Point, SeriesApi } from '@core/Drawings/types';
-import type { SettingsTab, SettingsValues } from '@src/types';
-import type {
-  IChartApi,
-  IPrimitivePaneView,
-  ISeriesPrimitiveAxisView,
-  PrimitiveHoveredItem,
-  SeriesAttachedParameter,
-  Time,
-} from 'lightweight-charts';
-
-class TestDrawing extends SeriesDrawingBase<SettingsValues, 'main'> {
-  protected mode: 'idle' | 'ready' = 'ready';
-
-  protected settings: SettingsValues = {
-    color: '#ffffff',
-  };
-
-  public handlePoint: Point = {
-    x: 50,
-    y: 50,
-  };
-
-  public updateAllViews = jest.fn();
-
-  public pointerDownCalls = 0;
-  public clickCalls = 0;
-  public doubleClickCalls = 0;
-  public contextMenuCalls = 0;
-
-  public isCreationPending(): boolean {
-    return this.mode === 'idle';
-  }
-
-  public getState() {
-    return {
-      mode: this.mode,
-    };
-  }
-
-  public setState(state: unknown): void {
-    if (!state || typeof state !== 'object') {
-      return;
-    }
-
-    const nextState = state as { mode?: 'idle' | 'ready' };
-
-    this.mode = nextState.mode ?? this.mode;
-  }
-
-  public getSettingsTabs(): SettingsTab[] {
-    return [];
-  }
-
-  public paneViews(): readonly IPrimitivePaneView[] {
-    return [];
-  }
-
-  public priceAxisPaneViews(): readonly IPrimitivePaneView[] {
-    return [];
-  }
-
-  public priceAxisViews(): readonly ISeriesPrimitiveAxisView[] {
-    return [];
-  }
-
-  public timeAxisPaneViews(): readonly IPrimitivePaneView[] {
-    return [];
-  }
-
-  public timeAxisViews(): readonly ISeriesPrimitiveAxisView[] {
-    return [];
-  }
-
-  public getRenderData() {
-    return null;
-  }
-
-  public setMode(mode: 'idle' | 'ready'): void {
-    this.mode = mode;
-  }
-
-  public markReady(): void {
-    this.mode = 'ready';
-    this.resolveReady?.();
-  }
-
-  public findHandle(point: Point): DrawingHandle<'main'> | null {
-    return this.getDrawingHandleAtPoint(point);
-  }
-
-  protected getDrawingHandles(): readonly DrawingHandle<'main'>[] {
-    return [
-      {
-        id: 'main',
-        ...this.handlePoint,
-      },
-    ];
-  }
-
-  protected getHoveredItem(x: number, y: number): PrimitiveHoveredItem | null {
-    if (Math.abs(x - this.handlePoint.x) > 8 || Math.abs(y - this.handlePoint.y) > 8) {
-      return null;
-    }
-
-    return {
-      cursorStyle: 'move',
-      externalId: 'test',
-      zOrder: 'top',
-    };
-  }
-
-  protected getGeometry() {
-    return null;
-  }
-
-  protected getTimeAxisSegments(): AxisSegment[] {
-    return [];
-  }
-
-  protected getPriceAxisSegments(): AxisSegment[] {
-    return [];
-  }
-
-  protected getTimeAxisLabel(_kind: string): AxisLabel | null {
-    return null;
-  }
-
-  protected getPriceAxisLabel(_kind: string): AxisLabel | null {
-    return null;
-  }
-
-  protected handlePointerDown(): void {
-    this.pointerDownCalls += 1;
-  }
-
-  protected handleClick(): void {
-    this.clickCalls += 1;
-  }
-
-  protected handleDoubleClick(): void {
-    this.doubleClickCalls += 1;
-  }
-
-  protected handleContextMenu(): void {
-    this.contextMenuCalls += 1;
-  }
-}
-
-function createEnvironment({
-  selected = false,
-  locked = false,
-  width = 400,
-  height = 300,
-}: {
-  selected?: boolean;
-  locked?: boolean;
-  width?: number;
-  height?: number;
-} = {}) {
-  let selectedValue = selected;
-  let lockedValue = locked;
-
-  const selected$ = new BehaviorSubject(selected);
-  const locked$ = new BehaviorSubject(locked);
-
-  const interaction: DrawingInteraction = {
-    selected$,
-    locked$,
-    isSelected: jest.fn(() => selectedValue),
-    isLocked: jest.fn(() => lockedValue),
-    select: jest.fn(() => {
-      selectedValue = true;
-      selected$.next(true);
-    }),
-    deselect: jest.fn(() => {
-      selectedValue = false;
-      selected$.next(false);
-    }),
-  };
-
-  const container = document.createElement('div');
-
-  jest.spyOn(container, 'getBoundingClientRect').mockReturnValue({
-    x: 0,
-    y: 0,
-    left: 0,
-    top: 0,
-    right: width,
-    bottom: height,
-    width,
-    height,
-    toJSON: jest.fn(),
-  });
-
-  const pane = {
-    paneIndex: jest.fn(() => 0),
-    getHeight: jest.fn(() => height),
-  };
-
-  const chart = {
-    panes: jest.fn(() => [pane]),
-    applyOptions: jest.fn(),
-  } as unknown as IChartApi;
-
-  const series = {
-    attachPrimitive: jest.fn(),
-    detachPrimitive: jest.fn(),
-    getPane: jest.fn(() => pane),
-  } as unknown as SeriesApi;
-
-  return {
-    container,
-    interaction,
-    chart,
-    series,
-    setSelected(value: boolean) {
-      selectedValue = value;
-      selected$.next(value);
-    },
-    setLocked(value: boolean) {
-      lockedValue = value;
-      locked$.next(value);
-    },
-  };
-}
-
-function createPointerEvent(target: Node, x: number, y: number, button = 0): PointerEvent {
-  return {
-    target,
-    clientX: x,
-    clientY: y,
-    button,
-    pointerId: 1,
-    preventDefault: jest.fn(),
-    stopPropagation: jest.fn(),
-  } as unknown as PointerEvent;
-}
-
-function createMouseEvent(target: Node, x: number, y: number): MouseEvent {
-  return {
-    target,
-    clientX: x,
-    clientY: y,
-    preventDefault: jest.fn(),
-    stopPropagation: jest.fn(),
-  } as unknown as MouseEvent;
-}
-
-describe('SeriesDrawingBase', () => {
-  it('должен возвращать копию settings', () => {
-    const env = createEnvironment();
-    const drawing = new TestDrawing(env);
-
-    const first = drawing.getSettings();
-
-    first.color = '#000000';
-
-    expect(drawing.getSettings()).toEqual({
-      color: '#ffffff',
-    });
-  });
-
-  it('должен объединять новые settings с текущими', () => {
-    const env = createEnvironment();
-    const drawing = new TestDrawing(env);
-
-    drawing.updateSettings({
-      lineWidth: 2,
-    });
-
-    expect(drawing.getSettings()).toEqual({
-      color: '#ffffff',
-      lineWidth: 2,
-    });
-  });
-
-  it('должен уведомлять подписчика settings', () => {
-    const env = createEnvironment();
-    const drawing = new TestDrawing(env);
-    const callback = jest.fn();
-
-    const subscription = drawing.subscribeSettings(callback);
-
-    expect(callback).toHaveBeenCalledWith({
-      color: '#ffffff',
-    });
-
-    drawing.updateSettings({
-      color: '#ff0000',
-    });
-
-    expect(callback).toHaveBeenLastCalledWith({
-      color: '#ff0000',
-    });
-
-    subscription.unsubscribe();
-  });
-
-  it('должен скрывать drawing', () => {
-    const env = createEnvironment({
-      selected: true,
-    });
-
-    const drawing = new TestDrawing(env);
-
-    drawing.hide();
-
-    expect(drawing.findHandle({ x: 50, y: 50 })).toBeNull();
-    expect(drawing.updateAllViews).toHaveBeenCalled();
-    expect(env.chart.applyOptions).toHaveBeenCalled();
-  });
-
-  it('должен снова показывать drawing', () => {
-    const env = createEnvironment({
-      selected: true,
-    });
-
-    const drawing = new TestDrawing(env);
-
-    drawing.hide();
-    drawing.show();
-
-    expect(drawing.findHandle({ x: 50, y: 50 })?.id).toBe('main');
-  });
-
-  it('должен показывать handle для selected drawing', () => {
-    const env = createEnvironment({
-      selected: true,
-    });
-
-    const drawing = new TestDrawing(env);
-
-    expect(drawing.findHandle({ x: 50, y: 50 })?.id).toBe('main');
-  });
-
-  it('не должен показывать handle для невыбранного ready drawing', () => {
-    const env = createEnvironment();
-    const drawing = new TestDrawing(env);
-
-    expect(drawing.findHandle({ x: 50, y: 50 })).toBeNull();
-  });
-
-  it('должен показывать handle во время создания', () => {
-    const env = createEnvironment();
-    const drawing = new TestDrawing(env);
-
-    drawing.setMode('idle');
-
-    expect(drawing.findHandle({ x: 50, y: 50 })?.id).toBe('main');
-  });
-
-  it('не должен показывать handle для locked drawing', () => {
-    const env = createEnvironment({
-      selected: true,
-      locked: true,
-    });
-
-    const drawing = new TestDrawing(env);
-
-    expect(drawing.findHandle({ x: 50, y: 50 })).toBeNull();
-  });
-
-  it('должен прикреплять DrawingHandlesPrimitive при attached', () => {
-    const env = createEnvironment();
-    const drawing = new TestDrawing(env);
-
-    drawing.attached({
-      chart: env.chart,
-      series: env.series,
-      requestUpdate: jest.fn(),
-    } as unknown as SeriesAttachedParameter<Time>);
-
-    expect(env.series.attachPrimitive).toHaveBeenCalledTimes(1);
-    expect(env.series.attachPrimitive).toHaveBeenCalledWith(expect.any(DrawingHandlesPrimitive));
-
-    drawing.detached();
-  });
-
-  it('должен отсоединять DrawingHandlesPrimitive при detached', () => {
-    const env = createEnvironment();
-    const drawing = new TestDrawing(env);
-
-    drawing.attached({
-      chart: env.chart,
-      series: env.series,
-      requestUpdate: jest.fn(),
-    } as unknown as SeriesAttachedParameter<Time>);
-
-    const handlesPrimitive = (env.series.attachPrimitive as jest.Mock).mock.calls[0][0];
-
-    drawing.detached();
-
-    expect(env.series.detachPrimitive).toHaveBeenCalledWith(handlesPrimitive);
-  });
-
-  it('должен возвращать false для object tree в idle', () => {
-    const env = createEnvironment();
-    const drawing = new TestDrawing(env);
-
-    drawing.setMode('idle');
-
-    expect(drawing.shouldShowInObjectTree()).toBe(false);
-  });
-
-  it('должен возвращать true для object tree в ready', () => {
-    const env = createEnvironment();
-    const drawing = new TestDrawing(env);
-
-    expect(drawing.shouldShowInObjectTree()).toBe(true);
-  });
-
-  it('waitTillReady должен сразу завершаться для ready drawing', async () => {
-    const env = createEnvironment();
-    const drawing = new TestDrawing(env);
-
-    await expect(drawing.waitTillReady()).resolves.toBeUndefined();
-  });
-
-  it('waitTillReady должен ожидать перехода в ready', async () => {
-    const env = createEnvironment();
-    const drawing = new TestDrawing(env);
-
-    drawing.setMode('idle');
-
-    const promise = drawing.waitTillReady();
-
-    drawing.markReady();
-
-    await expect(promise).resolves.toBeUndefined();
-  });
-
-  it('должен возвращать исходный cursor для unlocked drawing', () => {
-    const env = createEnvironment();
-    const drawing = new TestDrawing(env);
-
-    expect(drawing.hitTest(50, 50)?.cursorStyle).toBe('move');
-  });
-
-  it('должен заменять cursor на pointer для locked drawing', () => {
-    const env = createEnvironment({
-      locked: true,
-    });
-
-    const drawing = new TestDrawing(env);
-
-    expect(drawing.hitTest(50, 50)?.cursorStyle).toBe('pointer');
-  });
-
-  it('должен передавать pointerDown в drawing для unlocked состояния', () => {
-    const env = createEnvironment();
-    const drawing = new TestDrawing(env);
-
-    drawing.pointerDown(createPointerEvent(env.container, 50, 50));
-
-    expect(drawing.pointerDownCalls).toBe(1);
-  });
-
-  it('не должен обрабатывать событие вне container', () => {
-    const env = createEnvironment();
-    const drawing = new TestDrawing(env);
-    const outside = document.createElement('div');
-
-    drawing.pointerDown(createPointerEvent(outside, 50, 50));
-
-    expect(drawing.pointerDownCalls).toBe(0);
-  });
-
-  it('должен выбирать locked drawing при pointerDown по нему', () => {
-    const env = createEnvironment({
-      locked: true,
-    });
-
-    const drawing = new TestDrawing(env);
-
-    drawing.pointerDown(createPointerEvent(env.container, 50, 50));
-
-    expect(env.interaction.select).toHaveBeenCalledTimes(1);
-    expect(drawing.pointerDownCalls).toBe(0);
-  });
-
-  it('должен вызывать click handler', () => {
-    const env = createEnvironment();
-    const drawing = new TestDrawing(env);
-
-    drawing.click(createMouseEvent(env.container, 50, 50));
-
-    expect(drawing.clickCalls).toBe(1);
-  });
-
-  it('должен вызывать doubleClick handler', () => {
-    const env = createEnvironment();
-    const drawing = new TestDrawing(env);
-
-    drawing.doubleClick(createMouseEvent(env.container, 50, 50));
-
-    expect(drawing.doubleClickCalls).toBe(1);
-  });
-
-  it('должен вызывать contextMenu handler', () => {
-    const env = createEnvironment();
-    const drawing = new TestDrawing(env);
-
-    drawing.contextMenu(createMouseEvent(env.container, 50, 50));
-
-    expect(drawing.contextMenuCalls).toBe(1);
-  });
-
-  it('должен принимать событие на выступающей части handle поверх time axis', () => {
-    const env = createEnvironment({
-      selected: true,
-      height: 100,
-    });
-
-    const drawing = new TestDrawing(env);
-
-    drawing.handlePoint = {
-      x: 50,
-      y: 100,
-    };
-
-    drawing.attached({
-      chart: env.chart,
-      series: env.series,
-      requestUpdate: jest.fn(),
-    } as unknown as SeriesAttachedParameter<Time>);
-
-    const handlesPrimitive = (env.series.attachPrimitive as jest.Mock).mock.calls[0][0] as DrawingHandlesPrimitive;
-
-    handlesPrimitive.attached({
-      chart: env.chart,
-      series: env.series,
-      requestUpdate: jest.fn(),
-    } as unknown as SeriesAttachedParameter<Time>);
-
-    const outside = document.createElement('div');
-
-    expect(drawing.isHit(createMouseEvent(outside, 50, 104))).toBe(true);
-
-    drawing.detached();
-  });
-
-  it('должен rebind drawing на новую series', () => {
-    const env = createEnvironment();
-    const drawing = new TestDrawing(env);
-
-    const newSeries = {
-      attachPrimitive: jest.fn(),
-      detachPrimitive: jest.fn(),
-    } as unknown as SeriesApi;
-
-    drawing.rebind(newSeries);
-
-    expect(env.series.detachPrimitive).toHaveBeenCalledWith(drawing);
-    expect(newSeries.attachPrimitive).toHaveBeenCalledWith(drawing);
-  });
-
-  it('должен detach drawing при destroy', () => {
-    const env = createEnvironment();
-    const drawing = new TestDrawing(env);
-
-    drawing.destroy();
-
-    expect(env.series.detachPrimitive).toHaveBeenCalledWith(drawing);
-  });
-});
diff --git a/src/core/Drawings/__tests__/drawingTestUtils.ts b/src/core/Drawings/__tests__/drawingTestUtils.ts
index 25ad42b837b1294618231747f3266623382152e5..3746044a651252cada48ff719d90ebed443a9c82 100644
--- a/src/core/Drawings/__tests__/drawingTestUtils.ts
+++ b/src/core/Drawings/__tests__/drawingTestUtils.ts
@@ -1,6 +1,6 @@
 import { BehaviorSubject } from 'rxjs';
 
-import type { BaseDrawingParams } from '@core/Drawings/SeriesDrawingBase';
+import type { BaseDrawingParams } from '@src/core/Drawings/base';
 import type { CanvasRenderingTarget2D } from 'fancy-canvas';
 import type { Coordinate, Logical, Time } from 'lightweight-charts';
 
diff --git a/src/core/Drawings/__tests__/drawings/axis/CustomAxisView.test.ts b/src/core/Drawings/__tests__/drawings/axis/CustomAxisView.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6f9588e58036cf64592c5a841c6fc4ccf802fec9
--- /dev/null
+++ b/src/core/Drawings/__tests__/drawings/axis/CustomAxisView.test.ts
@@ -0,0 +1,163 @@
+import { CustomAxisView } from '@core/Drawings/axis/CustomAxisView';
+
+import type { AxisLabel } from '@core/Drawings/types';
+
+class TestCustomAxisView extends CustomAxisView {
+  protected getViewportSize(rect: DOMRect): number {
+    return rect.width;
+  }
+}
+
+function createViewport(width = 400, height = 300): HTMLElement {
+  const viewport = document.createElement('div');
+
+  jest.spyOn(viewport, 'getBoundingClientRect').mockReturnValue({
+    width,
+    height,
+    x: 0,
+    y: 0,
+    top: 0,
+    left: 0,
+    right: width,
+    bottom: height,
+    toJSON: () => ({}),
+  });
+
+  return viewport;
+}
+
+function createLabel(overrides: Partial<AxisLabel> = {}): AxisLabel {
+  return {
+    coordinate: 100,
+    text: 'Label',
+    textColor: '#ffffff',
+    backgroundColor: '#000000',
+    ...overrides,
+  };
+}
+
+describe('CustomAxisView', () => {
+  it('должен возвращать данные текущего label', () => {
+    const getAxisLabel = jest.fn(() => createLabel());
+
+    const view = new TestCustomAxisView({
+      getAxisLabel,
+      labelKind: 'start',
+      viewport: createViewport(),
+    });
+
+    expect(view.coordinate()).toBe(100);
+    expect(view.text()).toBe('Label');
+    expect(view.textColor()).toBe('#ffffff');
+    expect(view.backColor()).toBe('#000000');
+    expect(getAxisLabel).toHaveBeenCalledWith('start');
+  });
+
+  it('должен возвращать значения по умолчанию при отсутствии label', () => {
+    const view = new TestCustomAxisView({
+      getAxisLabel: () => null,
+      labelKind: 'start',
+      viewport: createViewport(),
+    });
+
+    expect(view.visible()).toBe(false);
+    expect(view.tickVisible()).toBe(false);
+    expect(view.coordinate()).toBe(0);
+    expect(view.text()).toBe('');
+    expect(view.textColor()).toBe('');
+    expect(view.backColor()).toBe('');
+  });
+
+  it('не должен показывать label с пустым текстом', () => {
+    const view = new TestCustomAxisView({
+      getAxisLabel: () =>
+        createLabel({
+          text: '',
+        }),
+      labelKind: 'start',
+      viewport: createViewport(),
+    });
+
+    expect(view.visible()).toBe(false);
+  });
+
+  it('должен показывать label внутри viewport', () => {
+    const view = new TestCustomAxisView({
+      getAxisLabel: () =>
+        createLabel({
+          coordinate: 200,
+        }),
+      labelKind: 'start',
+      viewport: createViewport(400),
+    });
+
+    expect(view.visible()).toBe(true);
+  });
+
+  it.each([-1, 401])('не должен показывать label вне viewport: %s', (coordinate) => {
+    const view = new TestCustomAxisView({
+      getAxisLabel: () =>
+        createLabel({
+          coordinate,
+        }),
+      labelKind: 'start',
+      viewport: createViewport(400),
+    });
+
+    expect(view.visible()).toBe(false);
+  });
+
+  it.each([0, 400])('должен показывать label на границе viewport: %s', (coordinate) => {
+    const view = new TestCustomAxisView({
+      getAxisLabel: () =>
+        createLabel({
+          coordinate,
+        }),
+      labelKind: 'start',
+      viewport: createViewport(400),
+    });
+
+    expect(view.visible()).toBe(true);
+  });
+
+  it.each([Number.NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY])(
+    'не должен показывать label с невалидной coordinate: %s',
+    (coordinate) => {
+      const view = new TestCustomAxisView({
+        getAxisLabel: () =>
+          createLabel({
+            coordinate,
+          }),
+        labelKind: 'start',
+        viewport: createViewport(),
+      });
+
+      expect(view.visible()).toBe(false);
+    },
+  );
+
+  it('tick не должен быть видимым', () => {
+    const view = new TestCustomAxisView({
+      getAxisLabel: () => createLabel(),
+      labelKind: 'start',
+      viewport: createViewport(),
+    });
+
+    expect(view.tickVisible()).toBe(false);
+  });
+
+  it('update не должен изменять состояние view', () => {
+    const getAxisLabel = jest.fn(() => createLabel());
+
+    const view = new TestCustomAxisView({
+      getAxisLabel,
+      labelKind: 'start',
+      viewport: createViewport(),
+    });
+
+    view.update();
+
+    expect(view.visible()).toBe(true);
+    expect(view.coordinate()).toBe(100);
+  });
+});
diff --git a/src/core/Drawings/__tests__/drawings/CustomPriceAxis.test.ts b/src/core/Drawings/__tests__/drawings/axis/CustomPriceAxis.test.ts
similarity index 55%
rename from src/core/Drawings/__tests__/drawings/CustomPriceAxis.test.ts
rename to src/core/Drawings/__tests__/drawings/axis/CustomPriceAxis.test.ts
index 1a259067c5fc13feac359bffa2d0647dc9e30e08..9ca991de84ce05efc1d6ecaa5d381ae1cb14176d 100644
--- a/src/core/Drawings/__tests__/drawings/CustomPriceAxis.test.ts
+++ b/src/core/Drawings/__tests__/drawings/axis/CustomPriceAxis.test.ts
@@ -23,18 +23,41 @@ function createTarget(verticalPixelRatio = 2) {
         context,
         horizontalPixelRatio: 2,
         verticalPixelRatio,
-        bitmapSize: { width: 300, height: 200 },
+        bitmapSize: {
+          width: 300,
+          height: 200,
+        },
       });
     },
   );
 
   return {
     context,
-    target: { useBitmapCoordinateSpace } as unknown as CanvasRenderingTarget2D,
+    target: {
+      useBitmapCoordinateSpace,
+    } as unknown as CanvasRenderingTarget2D,
     useBitmapCoordinateSpace,
   };
 }
 
+function createViewport(width = 400, height = 300): HTMLElement {
+  const viewport = document.createElement('div');
+
+  jest.spyOn(viewport, 'getBoundingClientRect').mockReturnValue({
+    width,
+    height,
+    x: 0,
+    y: 0,
+    top: 0,
+    left: 0,
+    right: width,
+    bottom: height,
+    toJSON: () => ({}),
+  });
+
+  return viewport;
+}
+
 describe('CustomPriceAxisPaneView', () => {
   it('должен использовать bottom zOrder по умолчанию', () => {
     const view = new CustomPriceAxisPaneView({
@@ -61,6 +84,18 @@ describe('CustomPriceAxisPaneView', () => {
     expect(view.renderer()).toBe(view.renderer());
   });
 
+  it('update не должен пересоздавать renderer', () => {
+    const view = new CustomPriceAxisPaneView({
+      getAxisSegments: () => [],
+    });
+
+    const renderer = view.renderer();
+
+    view.update();
+
+    expect(view.renderer()).toBe(renderer);
+  });
+
   it('не должен рисовать при отсутствии сегментов', () => {
     const { target, useBitmapCoordinateSpace } = createTarget();
 
@@ -94,6 +129,24 @@ describe('CustomPriceAxisPaneView', () => {
     expect(context.restore).toHaveBeenCalledTimes(1);
   });
 
+  it('должен учитывать verticalPixelRatio', () => {
+    const { context, target } = createTarget(3);
+
+    const view = new CustomPriceAxisPaneView({
+      getAxisSegments: () => [
+        {
+          from: 10,
+          to: 30,
+          color: '#ff0000',
+        },
+      ],
+    });
+
+    view.renderer().draw(target);
+
+    expect(context.fillRect).toHaveBeenCalledWith(0, 30, 300, 60);
+  });
+
   it('должен корректно рисовать сегмент при обратном порядке координат', () => {
     const { context, target } = createTarget();
 
@@ -133,56 +186,104 @@ describe('CustomPriceAxisPaneView', () => {
     view.renderer().draw(target);
 
     expect(context.fillRect).toHaveBeenNthCalledWith(1, 0, 20, 300, 20);
+
+    expect(context.fillRect).toHaveBeenNthCalledWith(2, 0, 60, 300, 40);
+
+    expect(context.save).toHaveBeenCalledTimes(1);
+    expect(context.restore).toHaveBeenCalledTimes(1);
+  });
+
+  it('должен получать актуальные segments при каждом draw', () => {
+    const { context, target } = createTarget();
+
+    const getAxisSegments = jest
+      .fn()
+      .mockReturnValueOnce([
+        {
+          from: 10,
+          to: 20,
+          color: '#ff0000',
+        },
+      ])
+      .mockReturnValueOnce([
+        {
+          from: 30,
+          to: 50,
+          color: '#00ff00',
+        },
+      ]);
+
+    const view = new CustomPriceAxisPaneView({
+      getAxisSegments,
+    });
+
+    view.renderer().draw(target);
+    view.renderer().draw(target);
+
+    expect(getAxisSegments).toHaveBeenCalledTimes(2);
+
+    expect(context.fillRect).toHaveBeenNthCalledWith(1, 0, 20, 300, 20);
+
     expect(context.fillRect).toHaveBeenNthCalledWith(2, 0, 60, 300, 40);
   });
 });
 
 describe('CustomPriceAxisView', () => {
-  it('должен возвращать данные label', () => {
-    const getAxisLabel = jest.fn(() => ({
-      coordinate: 125,
-      text: '100.50',
-      textColor: '#ffffff',
-      backgroundColor: '#000000',
-    }));
-
+  it('должен использовать height viewport для проверки видимости', () => {
     const view = new CustomPriceAxisView({
-      getAxisLabel,
+      getAxisLabel: () => ({
+        coordinate: 250,
+        text: '100.50',
+        textColor: '#ffffff',
+        backgroundColor: '#000000',
+      }),
       labelKind: 'entry',
+      viewport: createViewport(100, 300),
     });
 
     expect(view.visible()).toBe(true);
-    expect(view.tickVisible()).toBe(false);
-    expect(view.coordinate()).toBe(125);
-    expect(view.text()).toBe('100.50');
-    expect(view.textColor()).toBe('#ffffff');
-    expect(view.backColor()).toBe('#000000');
-    expect(getAxisLabel).toHaveBeenCalledWith('entry');
   });
 
-  it('должен возвращать значения по умолчанию при отсутствии label', () => {
+  it('не должен использовать width viewport для проверки видимости', () => {
     const view = new CustomPriceAxisView({
-      getAxisLabel: () => null,
+      getAxisLabel: () => ({
+        coordinate: 150,
+        text: '100.50',
+        textColor: '#ffffff',
+        backgroundColor: '#000000',
+      }),
       labelKind: 'entry',
+      viewport: createViewport(400, 100),
     });
 
     expect(view.visible()).toBe(false);
-    expect(view.tickVisible()).toBe(false);
-    expect(view.coordinate()).toBe(0);
-    expect(view.text()).toBe('');
-    expect(view.textColor()).toBe('');
-    expect(view.backColor()).toBe('');
   });
 
-  it('не должен быть видимым при пустом тексте', () => {
+  it('должен считать нижнюю границу height видимой', () => {
+    const view = new CustomPriceAxisView({
+      getAxisLabel: () => ({
+        coordinate: 300,
+        text: '100.50',
+        textColor: '#ffffff',
+        backgroundColor: '#000000',
+      }),
+      labelKind: 'entry',
+      viewport: createViewport(100, 300),
+    });
+
+    expect(view.visible()).toBe(true);
+  });
+
+  it('не должен показывать label ниже height viewport', () => {
     const view = new CustomPriceAxisView({
       getAxisLabel: () => ({
-        coordinate: 100,
-        text: '',
+        coordinate: 301,
+        text: '100.50',
         textColor: '#ffffff',
         backgroundColor: '#000000',
       }),
       labelKind: 'entry',
+      viewport: createViewport(1000, 300),
     });
 
     expect(view.visible()).toBe(false);
diff --git a/src/core/Drawings/__tests__/drawings/CustomTimeAxis.test.ts b/src/core/Drawings/__tests__/drawings/axis/CustomTimeAxis.test.ts
similarity index 55%
rename from src/core/Drawings/__tests__/drawings/CustomTimeAxis.test.ts
rename to src/core/Drawings/__tests__/drawings/axis/CustomTimeAxis.test.ts
index 570590f9d8f72ac6d643d519c0ab8c89abbe8292..766ea651d4f77eae3e34920c43ef188eed2e8666 100644
--- a/src/core/Drawings/__tests__/drawings/CustomTimeAxis.test.ts
+++ b/src/core/Drawings/__tests__/drawings/axis/CustomTimeAxis.test.ts
@@ -23,18 +23,41 @@ function createTarget(horizontalPixelRatio = 2) {
         context,
         horizontalPixelRatio,
         verticalPixelRatio: 2,
-        bitmapSize: { width: 300, height: 200 },
+        bitmapSize: {
+          width: 300,
+          height: 200,
+        },
       });
     },
   );
 
   return {
     context,
-    target: { useBitmapCoordinateSpace } as unknown as CanvasRenderingTarget2D,
+    target: {
+      useBitmapCoordinateSpace,
+    } as unknown as CanvasRenderingTarget2D,
     useBitmapCoordinateSpace,
   };
 }
 
+function createViewport(width = 400, height = 300): HTMLElement {
+  const viewport = document.createElement('div');
+
+  jest.spyOn(viewport, 'getBoundingClientRect').mockReturnValue({
+    width,
+    height,
+    x: 0,
+    y: 0,
+    top: 0,
+    left: 0,
+    right: width,
+    bottom: height,
+    toJSON: () => ({}),
+  });
+
+  return viewport;
+}
+
 describe('CustomTimeAxisPaneView', () => {
   it('должен использовать bottom zOrder по умолчанию', () => {
     const view = new CustomTimeAxisPaneView({
@@ -61,6 +84,18 @@ describe('CustomTimeAxisPaneView', () => {
     expect(view.renderer()).toBe(view.renderer());
   });
 
+  it('update не должен пересоздавать renderer', () => {
+    const view = new CustomTimeAxisPaneView({
+      getAxisSegments: () => [],
+    });
+
+    const renderer = view.renderer();
+
+    view.update();
+
+    expect(view.renderer()).toBe(renderer);
+  });
+
   it('не должен рисовать при отсутствии сегментов', () => {
     const { target, useBitmapCoordinateSpace } = createTarget();
 
@@ -94,6 +129,24 @@ describe('CustomTimeAxisPaneView', () => {
     expect(context.restore).toHaveBeenCalledTimes(1);
   });
 
+  it('должен учитывать horizontalPixelRatio', () => {
+    const { context, target } = createTarget(3);
+
+    const view = new CustomTimeAxisPaneView({
+      getAxisSegments: () => [
+        {
+          from: 10,
+          to: 30,
+          color: '#ff0000',
+        },
+      ],
+    });
+
+    view.renderer().draw(target);
+
+    expect(context.fillRect).toHaveBeenCalledWith(30, 0, 60, 200);
+  });
+
   it('должен корректно рисовать сегмент при обратном порядке координат', () => {
     const { context, target } = createTarget();
 
@@ -133,56 +186,104 @@ describe('CustomTimeAxisPaneView', () => {
     view.renderer().draw(target);
 
     expect(context.fillRect).toHaveBeenNthCalledWith(1, 20, 0, 20, 200);
+
+    expect(context.fillRect).toHaveBeenNthCalledWith(2, 60, 0, 40, 200);
+
+    expect(context.save).toHaveBeenCalledTimes(1);
+    expect(context.restore).toHaveBeenCalledTimes(1);
+  });
+
+  it('должен получать актуальные segments при каждом draw', () => {
+    const { context, target } = createTarget();
+
+    const getAxisSegments = jest
+      .fn()
+      .mockReturnValueOnce([
+        {
+          from: 10,
+          to: 20,
+          color: '#ff0000',
+        },
+      ])
+      .mockReturnValueOnce([
+        {
+          from: 30,
+          to: 50,
+          color: '#00ff00',
+        },
+      ]);
+
+    const view = new CustomTimeAxisPaneView({
+      getAxisSegments,
+    });
+
+    view.renderer().draw(target);
+    view.renderer().draw(target);
+
+    expect(getAxisSegments).toHaveBeenCalledTimes(2);
+
+    expect(context.fillRect).toHaveBeenNthCalledWith(1, 20, 0, 20, 200);
+
     expect(context.fillRect).toHaveBeenNthCalledWith(2, 60, 0, 40, 200);
   });
 });
 
 describe('CustomTimeAxisView', () => {
-  it('должен возвращать данные label', () => {
-    const getAxisLabel = jest.fn(() => ({
-      coordinate: 150,
-      text: '09:30',
-      textColor: '#ffffff',
-      backgroundColor: '#000000',
-    }));
-
+  it('должен использовать width viewport для проверки видимости', () => {
     const view = new CustomTimeAxisView({
-      getAxisLabel,
+      getAxisLabel: () => ({
+        coordinate: 250,
+        text: '09:30',
+        textColor: '#ffffff',
+        backgroundColor: '#000000',
+      }),
       labelKind: 'start',
+      viewport: createViewport(300, 100),
     });
 
     expect(view.visible()).toBe(true);
-    expect(view.tickVisible()).toBe(false);
-    expect(view.coordinate()).toBe(150);
-    expect(view.text()).toBe('09:30');
-    expect(view.textColor()).toBe('#ffffff');
-    expect(view.backColor()).toBe('#000000');
-    expect(getAxisLabel).toHaveBeenCalledWith('start');
   });
 
-  it('должен возвращать значения по умолчанию при отсутствии label', () => {
+  it('не должен использовать height viewport для проверки видимости', () => {
     const view = new CustomTimeAxisView({
-      getAxisLabel: () => null,
+      getAxisLabel: () => ({
+        coordinate: 150,
+        text: '09:30',
+        textColor: '#ffffff',
+        backgroundColor: '#000000',
+      }),
       labelKind: 'start',
+      viewport: createViewport(100, 400),
     });
 
     expect(view.visible()).toBe(false);
-    expect(view.tickVisible()).toBe(false);
-    expect(view.coordinate()).toBe(0);
-    expect(view.text()).toBe('');
-    expect(view.textColor()).toBe('');
-    expect(view.backColor()).toBe('');
   });
 
-  it('не должен быть видимым при пустом тексте', () => {
+  it('должен считать правую границу width видимой', () => {
     const view = new CustomTimeAxisView({
       getAxisLabel: () => ({
-        coordinate: 150,
-        text: '',
+        coordinate: 300,
+        text: '09:30',
+        textColor: '#ffffff',
+        backgroundColor: '#000000',
+      }),
+      labelKind: 'start',
+      viewport: createViewport(300, 100),
+    });
+
+    expect(view.visible()).toBe(true);
+  });
+
+  it('не должен показывать label правее width viewport', () => {
+    const view = new CustomTimeAxisView({
+      getAxisLabel: () => ({
+        coordinate: 301,
+        text: '09:30',
         textColor: '#ffffff',
         backgroundColor: '#000000',
       }),
       labelKind: 'start',
+      viewport: createViewport(300, 1000),
     });
 
     expect(view.visible()).toBe(false);
diff --git a/src/core/Drawings/__tests__/drawings/axisLine.test.ts b/src/core/Drawings/__tests__/drawings/axisLine.test.ts
index ff40e0f8c8614a5bcf6c1f65c11e9f375aa37222..fcf950e873c85e85302dfb50b927a84dc1bd28f0 100644
--- a/src/core/Drawings/__tests__/drawings/axisLine.test.ts
+++ b/src/core/Drawings/__tests__/drawings/axisLine.test.ts
@@ -25,19 +25,8 @@ jest.mock('@src/theme', () => ({
   }),
 }));
 
-jest.mock('@src/types/defaults', () => ({
-  Defaults: {
-    dateFormat: 'date-format',
-    timeFormat: '24h',
-    showTime: true,
-  },
-}));
-
 jest.mock('@src/utils', () => ({
   formatPrice: (value: number) => String(value),
-}));
-
-jest.mock('@src/utils/formatter', () => ({
   formatDate: () => 'formatted-date',
 }));
 
@@ -310,17 +299,118 @@ describe('AxisLine', () => {
     });
   });
 
-  it('не должен возвращать label для unselected line', () => {
+  it('должен сохранять time label для unselected vertical line', () => {
+    const { drawing } = createAxisLine('vertical');
+
+    drawing.setState({
+      mode: 'ready',
+      time: 80 as Time,
+    });
+
+    expect(drawing.timeLabel('main')).toEqual({
+      coordinate: 80,
+      text: 'formatted-date',
+      textColor: '#ffffff',
+      backgroundColor: '#112233',
+    });
+  });
+
+  it('должен сохранять price label для unselected horizontal line', () => {
+    const { drawing } = createAxisLine('horizontal');
+
+    drawing.setState({
+      mode: 'ready',
+      price: 120,
+    });
+
+    expect(drawing.priceLabel('main')).toEqual({
+      coordinate: 120,
+      text: '120',
+      textColor: '#ffffff',
+      backgroundColor: '#112233',
+    });
+  });
+
+  it('не должен возвращать time label для hidden vertical line', () => {
     const { drawing } = createAxisLine('vertical');
 
     drawing.setState({
+      hidden: true,
+      mode: 'ready',
+      time: 80 as Time,
+    });
+
+    expect(drawing.timeLabel('main')).toBeNull();
+  });
+
+  it('не должен возвращать price label для hidden horizontal line', () => {
+    const { drawing } = createAxisLine('horizontal');
+
+    drawing.setState({
+      hidden: true,
       mode: 'ready',
+      price: 120,
+    });
+
+    expect(drawing.priceLabel('main')).toBeNull();
+  });
+
+  it('не должен возвращать time label до завершения создания vertical line', () => {
+    const { drawing } = createAxisLine('vertical');
+
+    drawing.setState({
+      mode: 'idle',
       time: 80 as Time,
     });
 
     expect(drawing.timeLabel('main')).toBeNull();
   });
 
+  it('не должен возвращать price label до завершения создания horizontal line', () => {
+    const { drawing } = createAxisLine('horizontal');
+
+    drawing.setState({
+      mode: 'idle',
+      price: 120,
+    });
+
+    expect(drawing.priceLabel('main')).toBeNull();
+  });
+
+  it('не должен возвращать time label для horizontal line', () => {
+    const { drawing } = createAxisLine('horizontal');
+
+    drawing.setState({
+      mode: 'ready',
+      price: 120,
+    });
+
+    expect(drawing.timeLabel('main')).toBeNull();
+  });
+
+  it('не должен возвращать price label для vertical line', () => {
+    const { drawing } = createAxisLine('vertical');
+
+    drawing.setState({
+      mode: 'ready',
+      time: 80 as Time,
+    });
+
+    expect(drawing.priceLabel('main')).toBeNull();
+  });
+
+  it('не должен возвращать label для неизвестного kind', () => {
+    const { drawing } = createAxisLine('vertical');
+
+    drawing.setState({
+      mode: 'ready',
+      time: 80 as Time,
+    });
+
+    expect(drawing.timeLabel('unknown')).toBeNull();
+    expect(drawing.priceLabel('unknown')).toBeNull();
+  });
+
   it('должен определять hover vertical line', () => {
     const { drawing } = createAxisLine('vertical');
 
diff --git a/src/core/Drawings/__tests__/drawings/base/areaDrawingBase.test.ts b/src/core/Drawings/__tests__/drawings/base/areaDrawingBase.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..429e4a556fe1723746cdb138e16f456f47daeae3
--- /dev/null
+++ b/src/core/Drawings/__tests__/drawings/base/areaDrawingBase.test.ts
@@ -0,0 +1,408 @@
+import { AreaDrawingBase } from '@src/core/Drawings/base';
+
+import { createDrawingTestEnvironment, createPointerEvent } from '../../drawingTestUtils';
+
+import type { Anchor, AxisLabel, Bounds, Point } from '@core/Drawings/types';
+import type { SettingsTab, SettingsValues } from '@src/types';
+import type { PrimitiveHoveredItem } from 'lightweight-charts';
+
+type HandleId = 'start' | 'end';
+
+type TestMode = 'idle' | 'drawing' | 'ready' | 'dragging';
+
+class TestAreaDrawingBase extends AreaDrawingBase<SettingsValues, HandleId> {
+  protected mode: TestMode = 'idle';
+  protected settings: SettingsValues = {};
+
+  protected dragPointerId: number | null = null;
+  protected activeDragTarget: HandleId | 'body' | null = null;
+
+  public readonly finishDrawingMock = jest.fn();
+  public readonly startDraggingMock = jest.fn();
+  public readonly moveWholeMock = jest.fn();
+  public readonly resizeMock = jest.fn();
+
+  private geometry: Bounds | null = null;
+  private containsPointValue = false;
+  private dragTarget: HandleId | 'body' | null = null;
+
+  public getRenderData(): Bounds | null {
+    return this.geometry;
+  }
+
+  public getState() {
+    return {
+      mode: this.mode,
+      startAnchor: this.startAnchor,
+      endAnchor: this.endAnchor,
+    };
+  }
+
+  public setState(): void {}
+
+  public getSettingsTabs(): SettingsTab[] {
+    return [];
+  }
+
+  public isCreationPending(): boolean {
+    return this.mode === 'idle' || this.mode === 'drawing';
+  }
+
+  public setMode(mode: TestMode): void {
+    this.mode = mode;
+  }
+
+  public setGeometry(geometry: Bounds | null): void {
+    this.geometry = geometry;
+  }
+
+  public setContainsPoint(value: boolean): void {
+    this.containsPointValue = value;
+  }
+
+  public setTestDragTarget(target: HandleId | 'body' | null): void {
+    this.dragTarget = target;
+  }
+
+  public setDragState(target: HandleId | 'body' | null, pointerId: number | null): void {
+    this.activeDragTarget = target;
+    this.dragPointerId = pointerId;
+  }
+
+  public start(point: Point): void {
+    this.startDrawing(point);
+  }
+
+  public update(point: Point): void {
+    this.updateDrawing(point);
+  }
+
+  public pointerDown(event: PointerEvent): void {
+    this.handlePointerDown(event);
+  }
+
+  public pointerMove(event: PointerEvent): void {
+    this.handlePointerMove(event);
+  }
+
+  public getTestAnchors(): {
+    startAnchor: Anchor | null;
+    endAnchor: Anchor | null;
+  } {
+    return {
+      startAnchor: this.startAnchor,
+      endAnchor: this.endAnchor,
+    };
+  }
+
+  public getTimeSegments() {
+    return this.getTimeAxisSegments();
+  }
+
+  public getPriceSegments() {
+    return this.getPriceAxisSegments();
+  }
+
+  public getBoundsLabel(kind: string, kinds: readonly ('left' | 'right' | 'top' | 'bottom')[]): AxisLabel | null {
+    return this.getBoundsAxisLabel(kind, kinds, (labelKind) => String(labelKind));
+  }
+
+  protected finishDrawing(): void {
+    this.finishDrawingMock();
+  }
+
+  protected containsPoint(): boolean {
+    return this.containsPointValue;
+  }
+
+  protected getDragTarget(): HandleId | 'body' | null {
+    return this.dragTarget;
+  }
+
+  protected startDragging(point: Point, pointerId: number, target: HandleId | 'body'): void {
+    this.startDraggingMock(point, pointerId, target);
+
+    this.activeDragTarget = target;
+
+    this.dragPointerId = pointerId;
+
+    this.mode = 'dragging';
+  }
+
+  protected moveWhole(point: Point): void {
+    this.moveWholeMock(point);
+  }
+
+  protected resize(point: Point): void {
+    this.resizeMock(point);
+  }
+
+  protected getGeometry(): Bounds | null {
+    return this.geometry;
+  }
+
+  protected getHoveredItem(): PrimitiveHoveredItem | null {
+    return null;
+  }
+
+  protected getTimeAxisLabel(): AxisLabel | null {
+    return null;
+  }
+
+  protected getPriceAxisLabel(): AxisLabel | null {
+    return null;
+  }
+}
+
+function createDrawing(selected = false) {
+  const env = createDrawingTestEnvironment({
+    selected,
+  });
+
+  const drawing = new TestAreaDrawingBase(env);
+
+  return {
+    ...env,
+    drawing,
+  };
+}
+
+describe('AreaDrawingBase', () => {
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
+  describe('drawing lifecycle', () => {
+    it('должен начинать drawing с одинаковых anchors', () => {
+      const { drawing } = createDrawing();
+
+      drawing.start({
+        x: 20,
+        y: 30,
+      });
+
+      expect(drawing.getState()).toEqual({
+        mode: 'drawing',
+        startAnchor: {
+          time: 20,
+          price: 30,
+        },
+        endAnchor: {
+          time: 20,
+          price: 30,
+        },
+      });
+    });
+
+    it('должен обновлять end anchor с clamp по container', () => {
+      const { drawing } = createDrawing();
+
+      drawing.start({
+        x: 20,
+        y: 30,
+      });
+
+      drawing.update({
+        x: 500,
+        y: -50,
+      });
+
+      expect(drawing.getTestAnchors().endAnchor).toEqual({
+        time: 400,
+        price: 0,
+      });
+    });
+
+    it('должен начинать drawing через первый pointerDown', () => {
+      const { drawing, container } = createDrawing();
+
+      const event = createPointerEvent(container, 20, 30);
+
+      drawing.pointerDown(event);
+
+      expect(drawing.getState().mode).toBe('drawing');
+
+      expect(event.preventDefault).toHaveBeenCalledTimes(1);
+
+      expect(event.stopPropagation).toHaveBeenCalledTimes(1);
+    });
+
+    it('должен завершать drawing через второй pointerDown', () => {
+      const { drawing, container } = createDrawing();
+
+      drawing.start({
+        x: 20,
+        y: 30,
+      });
+
+      drawing.pointerDown(createPointerEvent(container, 100, 120));
+
+      expect(drawing.finishDrawingMock).toHaveBeenCalledTimes(1);
+
+      expect(drawing.getTestAnchors().endAnchor).toEqual({
+        time: 100,
+        price: 120,
+      });
+    });
+  });
+
+  describe('selection and dragging', () => {
+    it('должен только выбрать ready drawing при первом pointerDown', () => {
+      const { drawing, container, interaction } = createDrawing();
+
+      drawing.setMode('ready');
+      drawing.setContainsPoint(true);
+
+      drawing.pointerDown(createPointerEvent(container, 50, 60));
+
+      expect(interaction.select).toHaveBeenCalledTimes(1);
+
+      expect(drawing.startDraggingMock).not.toHaveBeenCalled();
+    });
+
+    it('должен deselect selected drawing при pointerDown вне drag target', () => {
+      const { drawing, container, interaction } = createDrawing(true);
+
+      drawing.setMode('ready');
+      drawing.setTestDragTarget(null);
+
+      drawing.pointerDown(createPointerEvent(container, 50, 60));
+
+      expect(interaction.deselect).toHaveBeenCalledTimes(1);
+    });
+
+    it('должен начинать drag выбранного target', () => {
+      const { drawing, container } = createDrawing(true);
+
+      drawing.setMode('ready');
+      drawing.setTestDragTarget('start');
+
+      const event = createPointerEvent(container, 50, 60, 7);
+
+      drawing.pointerDown(event);
+
+      expect(drawing.startDraggingMock).toHaveBeenCalledWith(
+        {
+          x: 50,
+          y: 60,
+        },
+        7,
+        'start',
+      );
+
+      expect(event.preventDefault).toHaveBeenCalledTimes(1);
+
+      expect(event.stopPropagation).toHaveBeenCalledTimes(1);
+    });
+
+    it('должен двигать body во время body drag', () => {
+      const { drawing, container } = createDrawing(true);
+
+      drawing.setMode('dragging');
+      drawing.setDragState('body', 5);
+
+      const event = createPointerEvent(container, 80, 90, 5);
+
+      drawing.pointerMove(event);
+
+      expect(drawing.moveWholeMock).toHaveBeenCalledWith({
+        x: 80,
+        y: 90,
+      });
+
+      expect(drawing.resizeMock).not.toHaveBeenCalled();
+
+      expect(event.preventDefault).toHaveBeenCalledTimes(1);
+    });
+
+    it('должен resize handle во время handle drag', () => {
+      const { drawing, container } = createDrawing(true);
+
+      drawing.setMode('dragging');
+      drawing.setDragState('end', 5);
+
+      drawing.pointerMove(createPointerEvent(container, 80, 90, 5));
+
+      expect(drawing.resizeMock).toHaveBeenCalledWith({
+        x: 80,
+        y: 90,
+      });
+
+      expect(drawing.moveWholeMock).not.toHaveBeenCalled();
+    });
+
+    it('не должен обрабатывать drag другого pointerId', () => {
+      const { drawing, container } = createDrawing(true);
+
+      drawing.setMode('dragging');
+      drawing.setDragState('body', 5);
+
+      drawing.pointerMove(createPointerEvent(container, 80, 90, 6));
+
+      expect(drawing.moveWholeMock).not.toHaveBeenCalled();
+    });
+  });
+
+  describe('axis', () => {
+    it('должен строить time и price segments по geometry', () => {
+      const { drawing, setSelected } = createDrawing();
+
+      drawing.setGeometry({
+        left: 10,
+        right: 100,
+        top: 20,
+        bottom: 80,
+      });
+
+      setSelected(true);
+
+      expect(drawing.getTimeSegments()[0]).toMatchObject({
+        from: 10,
+        to: 100,
+      });
+
+      expect(drawing.getPriceSegments()[0]).toMatchObject({
+        from: 20,
+        to: 80,
+      });
+    });
+
+    it('должен создавать label из соответствующего geometry bound', () => {
+      const { drawing, setSelected } = createDrawing();
+
+      drawing.setGeometry({
+        left: 10,
+        right: 100,
+        top: 20,
+        bottom: 80,
+      });
+
+      setSelected(true);
+
+      expect(drawing.getBoundsLabel('left', ['left', 'right'])).toMatchObject({
+        coordinate: 10,
+        text: 'left',
+      });
+
+      expect(drawing.getBoundsLabel('bottom', ['top', 'bottom'])).toMatchObject({
+        coordinate: 80,
+        text: 'bottom',
+      });
+    });
+
+    it('не должен создавать label для неизвестного kind', () => {
+      const { drawing, setSelected } = createDrawing();
+
+      drawing.setGeometry({
+        left: 10,
+        right: 100,
+        top: 20,
+        bottom: 80,
+      });
+
+      setSelected(true);
+
+      expect(drawing.getBoundsLabel('unknown', ['left', 'right'])).toBeNull();
+    });
+  });
+});
diff --git a/src/core/Drawings/__tests__/drawings/base/drawingBase.test.ts b/src/core/Drawings/__tests__/drawings/base/drawingBase.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..220ba35707a5ee8cc0d6f2b5f77361f695e2756f
--- /dev/null
+++ b/src/core/Drawings/__tests__/drawings/base/drawingBase.test.ts
@@ -0,0 +1,597 @@
+import { DrawingBase } from '@src/core/Drawings/base';
+
+import { createDrawingTestEnvironment, createMouseEvent, createPointerEvent } from '../../drawingTestUtils';
+
+import type { Anchor, AxisLabel, AxisSegment, Bounds, Point, UpdatableView } from '@core/Drawings/types';
+import type { SettingsTab, SettingsValues } from '@src/types';
+import type {
+  IPrimitivePaneView,
+  PrimitiveHoveredItem,
+  SeriesAttachedParameter,
+  SeriesOptionsMap,
+  Time,
+} from 'lightweight-charts';
+
+jest.mock('@src/theme', () => ({
+  getThemeStore: () => ({
+    colors: {
+      chartPriceLineText: '#ffffff',
+      axisMarkerLabelFill: '#112233',
+      axisMarkerAreaFill: '#445566',
+    },
+  }),
+}));
+
+type TestMode = 'idle' | 'ready';
+
+class TestDrawingBase extends DrawingBase<SettingsValues, 'main'> {
+  protected mode: TestMode = 'ready';
+  protected settings: SettingsValues = {
+    lineWidth: 1,
+  };
+
+  private geometry: Bounds | null = null;
+  private hoveredItem: PrimitiveHoveredItem | null = null;
+
+  public getRenderData(): Bounds | null {
+    return this.geometry;
+  }
+
+  public getState() {
+    return {
+      hidden: this.hidden,
+      mode: this.mode,
+      settings: this.settings,
+    };
+  }
+
+  public setState(state: unknown): void {
+    if (!state || typeof state !== 'object') {
+      return;
+    }
+
+    const nextState = state as Partial<{
+      hidden: boolean;
+      mode: TestMode;
+    }>;
+
+    if (typeof nextState.hidden === 'boolean') {
+      this.hidden = nextState.hidden;
+    }
+
+    if (nextState.mode) {
+      this.mode = nextState.mode;
+    }
+  }
+
+  public getSettingsTabs(): SettingsTab[] {
+    return [];
+  }
+
+  public isCreationPending(): boolean {
+    return this.mode === 'idle';
+  }
+
+  public setMode(mode: TestMode): void {
+    this.mode = mode;
+  }
+
+  public setGeometry(geometry: Bounds | null): void {
+    this.geometry = geometry;
+  }
+
+  public setHoveredItem(item: PrimitiveHoveredItem | null): void {
+    this.hoveredItem = item;
+  }
+
+  public createAnchor(point: Point): Anchor | null {
+    return this.createAnchorFromPoint(point);
+  }
+
+  public getAnchorPoint(anchor: Anchor | null): Point | null {
+    return this.getPointFromAnchor(anchor);
+  }
+
+  public createLabel(
+    coordinate: number | null,
+    text: string,
+    style?: Partial<Pick<AxisLabel, 'textColor' | 'backgroundColor'>>,
+  ): AxisLabel | null {
+    return this.createAxisLabel(coordinate, text, style);
+  }
+
+  public createSegment(from: number, to: number, color?: string): AxisSegment {
+    return this.createAxisSegment(from, to, color);
+  }
+
+  public getBoundsSegments(axis: 'time' | 'price'): AxisSegment[] {
+    return this.getBoundsAxisSegments(axis, () => this.geometry);
+  }
+
+  public getInteractiveLabel(
+    kind: string,
+    kinds: readonly string[],
+    getLabel: (kind: string) => AxisLabel | null,
+  ): AxisLabel | null {
+    return this.getInteractiveAxisLabel(kind, kinds, getLabel);
+  }
+
+  public consume(event: MouseEvent): void {
+    this.consumeEvent(event);
+  }
+
+  public selectOnDown(event: PointerEvent, isHit: () => boolean): boolean {
+    return this.selectOnPointerDown(event, isHit);
+  }
+
+  public openSettings(
+    event: MouseEvent,
+    isHit: (point: Point) => boolean,
+    callback: (() => void) | undefined,
+    selectedOnly = false,
+  ): void {
+    this.openSettingsOnDoubleClick(event, isHit, callback, selectedOnly);
+  }
+
+  public initialize(
+    paneView: IPrimitivePaneView & UpdatableView,
+    timeLabels: readonly string[],
+    priceLabels: readonly string[],
+  ): void {
+    this.initializeViews(paneView, timeLabels, priceLabels);
+  }
+
+  public triggerRender(): void {
+    this.render();
+  }
+
+  protected getHoveredItem(): PrimitiveHoveredItem | null {
+    return this.hoveredItem;
+  }
+
+  protected getGeometry(): Bounds | null {
+    return this.geometry;
+  }
+
+  protected getTimeAxisSegments(): AxisSegment[] {
+    return [];
+  }
+
+  protected getPriceAxisSegments(): AxisSegment[] {
+    return [];
+  }
+
+  protected getTimeAxisLabel(): AxisLabel | null {
+    return null;
+  }
+
+  protected getPriceAxisLabel(): AxisLabel | null {
+    return null;
+  }
+}
+
+function createDrawing(options: Parameters<typeof createDrawingTestEnvironment>[0] = {}) {
+  const env = createDrawingTestEnvironment(options);
+  const drawing = new TestDrawingBase(env);
+
+  return {
+    ...env,
+    drawing,
+  };
+}
+
+function attachDrawing(
+  drawing: TestDrawingBase,
+  env: ReturnType<typeof createDrawingTestEnvironment>,
+  requestUpdate = jest.fn(),
+): jest.Mock {
+  drawing.attached({
+    chart: env.chart,
+    series: env.series,
+    requestUpdate,
+  } as unknown as SeriesAttachedParameter<Time, keyof SeriesOptionsMap>);
+
+  requestUpdate.mockClear();
+
+  return requestUpdate;
+}
+
+describe('DrawingBase', () => {
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
+  describe('anchors', () => {
+    it('должен создавать anchor из point', () => {
+      const { drawing } = createDrawing();
+
+      expect(
+        drawing.createAnchor({
+          x: 120,
+          y: 80,
+        }),
+      ).toEqual({
+        time: 120,
+        price: 80,
+      });
+    });
+
+    it('должен возвращать null, если anchor создать невозможно', () => {
+      const { drawing, timeScale } = createDrawing();
+
+      timeScale.coordinateToTime.mockReturnValue(null);
+
+      expect(
+        drawing.createAnchor({
+          x: 120,
+          y: 80,
+        }),
+      ).toBeNull();
+    });
+
+    it('должен преобразовывать anchor в point без округления', () => {
+      const { drawing } = createDrawing();
+
+      expect(
+        drawing.getAnchorPoint({
+          time: 120.25 as Time,
+          price: 80.75,
+        }),
+      ).toEqual({
+        x: 120.25,
+        y: 80.75,
+      });
+    });
+
+    it('должен возвращать null для пустого anchor', () => {
+      const { drawing } = createDrawing();
+
+      expect(drawing.getAnchorPoint(null)).toBeNull();
+    });
+  });
+
+  describe('axis helpers', () => {
+    it('должен создавать axis label с theme цветами', () => {
+      const { drawing } = createDrawing();
+
+      expect(drawing.createLabel(100, '100')).toEqual({
+        coordinate: 100,
+        text: '100',
+        textColor: '#ffffff',
+        backgroundColor: '#112233',
+      });
+    });
+
+    it('должен поддерживать пользовательские цвета axis label', () => {
+      const { drawing } = createDrawing();
+
+      expect(
+        drawing.createLabel(100, '100', {
+          textColor: '#111111',
+          backgroundColor: '#222222',
+        }),
+      ).toEqual({
+        coordinate: 100,
+        text: '100',
+        textColor: '#111111',
+        backgroundColor: '#222222',
+      });
+    });
+
+    it('должен возвращать null для label без coordinate', () => {
+      const { drawing } = createDrawing();
+
+      expect(drawing.createLabel(null, '100')).toBeNull();
+    });
+
+    it('должен возвращать null для label без текста', () => {
+      const { drawing } = createDrawing();
+
+      expect(drawing.createLabel(100, '')).toBeNull();
+    });
+
+    it('должен создавать axis segment', () => {
+      const { drawing } = createDrawing();
+
+      expect(drawing.createSegment(10, 20)).toEqual({
+        from: 10,
+        to: 20,
+        color: '#445566',
+      });
+    });
+
+    it('должен строить time segment по bounds только для interactive axis', () => {
+      const { drawing, setSelected } = createDrawing();
+
+      drawing.setGeometry({
+        left: 10,
+        right: 120,
+        top: 30,
+        bottom: 80,
+      });
+
+      expect(drawing.getBoundsSegments('time')).toEqual([]);
+
+      setSelected(true);
+
+      expect(drawing.getBoundsSegments('time')).toEqual([
+        {
+          from: 10,
+          to: 120,
+          color: '#445566',
+        },
+      ]);
+    });
+
+    it('должен строить price segment по bounds', () => {
+      const { drawing, setSelected } = createDrawing();
+
+      drawing.setGeometry({
+        left: 10,
+        right: 120,
+        top: 30,
+        bottom: 80,
+      });
+
+      setSelected(true);
+
+      expect(drawing.getBoundsSegments('price')).toEqual([
+        {
+          from: 30,
+          to: 80,
+          color: '#445566',
+        },
+      ]);
+    });
+
+    it('должен показывать interactive axis во время создания', () => {
+      const { drawing } = createDrawing();
+
+      drawing.setGeometry({
+        left: 10,
+        right: 120,
+        top: 30,
+        bottom: 80,
+      });
+
+      drawing.setMode('idle');
+
+      expect(drawing.getBoundsSegments('time')).toHaveLength(1);
+    });
+
+    it('должен возвращать interactive label только для разрешённого kind', () => {
+      const { drawing, setSelected } = createDrawing();
+
+      setSelected(true);
+
+      const getLabel = jest.fn(
+        (kind: string): AxisLabel => ({
+          coordinate: 100,
+          text: kind,
+          textColor: '#ffffff',
+          backgroundColor: '#112233',
+        }),
+      );
+
+      expect(drawing.getInteractiveLabel('start', ['start', 'end'], getLabel)).toEqual({
+        coordinate: 100,
+        text: 'start',
+        textColor: '#ffffff',
+        backgroundColor: '#112233',
+      });
+
+      expect(drawing.getInteractiveLabel('unknown', ['start', 'end'], getLabel)).toBeNull();
+    });
+  });
+
+  describe('views', () => {
+    it('должен регистрировать pane и axis views через initializeViews', () => {
+      const { drawing } = createDrawing();
+
+      const paneView = {
+        update: jest.fn(),
+        renderer: jest.fn(),
+        zOrder: jest.fn(),
+      } as unknown as IPrimitivePaneView & UpdatableView;
+
+      drawing.initialize(paneView, ['start', 'end'], ['price']);
+
+      expect(drawing.paneViews()).toEqual([paneView]);
+
+      expect(drawing.timeAxisPaneViews()).toHaveLength(1);
+
+      expect(drawing.priceAxisPaneViews()).toHaveLength(1);
+
+      expect(drawing.timeAxisViews()).toHaveLength(2);
+
+      expect(drawing.priceAxisViews()).toHaveLength(1);
+    });
+
+    it('должен обновлять зарегистрированный pane view', () => {
+      const { drawing } = createDrawing();
+
+      const paneView = {
+        update: jest.fn(),
+        renderer: jest.fn(),
+        zOrder: jest.fn(),
+      } as unknown as IPrimitivePaneView & UpdatableView;
+
+      drawing.initialize(paneView, [], []);
+
+      drawing.updateAllViews();
+
+      expect(paneView.update).toHaveBeenCalledTimes(1);
+    });
+  });
+
+  describe('settings', () => {
+    it('должен сразу отдавать текущие settings при подписке', () => {
+      const { drawing } = createDrawing();
+      const callback = jest.fn();
+
+      drawing.subscribeSettings(callback);
+
+      expect(callback).toHaveBeenCalledWith({
+        lineWidth: 1,
+      });
+    });
+
+    it('должен уведомлять подписчика после updateSettings', () => {
+      const { drawing } = createDrawing();
+      const callback = jest.fn();
+
+      drawing.subscribeSettings(callback);
+
+      callback.mockClear();
+
+      drawing.updateSettings({
+        lineWidth: 3,
+      });
+
+      expect(callback).toHaveBeenCalledWith({
+        lineWidth: 3,
+      });
+    });
+  });
+
+  describe('render lifecycle', () => {
+    it('должен вызывать updateAllViews и requestUpdate после render', () => {
+      const env = createDrawingTestEnvironment();
+      const drawing = new TestDrawingBase(env);
+      const updateAllViews = jest.spyOn(drawing, 'updateAllViews');
+
+      const requestUpdate = attachDrawing(drawing, env);
+
+      updateAllViews.mockClear();
+
+      drawing.hide();
+
+      expect(drawing.getState().hidden).toBe(true);
+
+      expect(updateAllViews).toHaveBeenCalledTimes(1);
+
+      expect(requestUpdate).toHaveBeenCalledTimes(1);
+    });
+
+    it('должен возвращать drawing после show', () => {
+      const { drawing } = createDrawing();
+
+      drawing.hide();
+      drawing.show();
+
+      expect(drawing.getState().hidden).toBe(false);
+    });
+
+    it('должен сразу завершать waitTillReady для ready drawing', async () => {
+      const { drawing } = createDrawing();
+
+      await expect(drawing.waitTillReady()).resolves.toBeUndefined();
+    });
+
+    it('должен считать idle drawing ожидающим создания', () => {
+      const { drawing } = createDrawing();
+
+      drawing.setMode('idle');
+
+      expect(drawing.isCreationPending()).toBe(true);
+
+      expect(drawing.shouldShowInObjectTree()).toBe(false);
+    });
+  });
+
+  describe('interaction helpers', () => {
+    it('должен consume event', () => {
+      const { drawing, container } = createDrawing();
+
+      const event = createMouseEvent(container, 10, 20);
+
+      drawing.consume(event);
+
+      expect(event.preventDefault).toHaveBeenCalledTimes(1);
+
+      expect(event.stopPropagation).toHaveBeenCalledTimes(1);
+    });
+
+    it('должен выбирать drawing при pointerDown по нему', () => {
+      const { drawing, container, interaction } = createDrawing();
+
+      const event = createPointerEvent(container, 10, 20);
+
+      expect(drawing.selectOnDown(event, () => true)).toBe(true);
+
+      expect(interaction.select).toHaveBeenCalledTimes(1);
+
+      expect(event.preventDefault).toHaveBeenCalledTimes(1);
+
+      expect(event.stopPropagation).toHaveBeenCalledTimes(1);
+    });
+
+    it('не должен повторно обрабатывать selection для уже выбранного drawing', () => {
+      const { drawing, container, setSelected } = createDrawing();
+
+      setSelected(true);
+
+      const event = createPointerEvent(container, 10, 20);
+
+      expect(drawing.selectOnDown(event, () => true)).toBe(false);
+    });
+
+    it('должен открывать settings при double click по drawing', () => {
+      const { drawing, container } = createDrawing();
+
+      const openSettings = jest.fn();
+      const event = createMouseEvent(container, 30, 40, 2);
+
+      drawing.openSettings(event, () => true, openSettings);
+
+      expect(openSettings).toHaveBeenCalledTimes(1);
+
+      expect(event.preventDefault).toHaveBeenCalledTimes(1);
+
+      expect(event.stopPropagation).toHaveBeenCalledTimes(1);
+    });
+
+    it('не должен открывать selectedOnly settings для невыбранного drawing', () => {
+      const { drawing, container } = createDrawing();
+
+      const openSettings = jest.fn();
+      const event = createMouseEvent(container, 30, 40, 2);
+
+      drawing.openSettings(event, () => true, openSettings, true);
+
+      expect(openSettings).not.toHaveBeenCalled();
+    });
+
+    it('должен заменять cursor на pointer для locked drawing', () => {
+      const { drawing, setLocked } = createDrawing();
+
+      drawing.setHoveredItem({
+        cursorStyle: 'move',
+        externalId: 'test',
+        zOrder: 'top',
+      });
+
+      setLocked(true);
+
+      expect(drawing.hitTest(10, 20)).toEqual({
+        cursorStyle: 'pointer',
+        externalId: 'test',
+        zOrder: 'top',
+      });
+    });
+  });
+
+  describe('rebind', () => {
+    it('должен отвязывать drawing от старой series и привязывать к новой', () => {
+      const firstEnv = createDrawingTestEnvironment();
+      const secondEnv = createDrawingTestEnvironment();
+      const drawing = new TestDrawingBase(firstEnv);
+
+      drawing.rebind(secondEnv.series);
+
+      expect(firstEnv.series.detachPrimitive).toHaveBeenCalledWith(drawing);
+
+      expect(secondEnv.series.attachPrimitive).toHaveBeenCalledWith(drawing);
+    });
+  });
+});
diff --git a/src/core/Drawings/__tests__/drawings/base/linearDrawingBase.test.ts b/src/core/Drawings/__tests__/drawings/base/linearDrawingBase.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..bb2c04bbeb7e3bcd3f452fbd0cd9ff1e5a2852d8
--- /dev/null
+++ b/src/core/Drawings/__tests__/drawings/base/linearDrawingBase.test.ts
@@ -0,0 +1,506 @@
+import { LinearDrawingBase, type TwoPointGeometry } from '@src/core/Drawings/base';
+
+import { createDrawingTestEnvironment, createPointerEvent } from '../../drawingTestUtils';
+
+import type { Anchor, AxisLabel, Point } from '@core/Drawings/types';
+import type { SettingsTab, SettingsValues } from '@src/types';
+import type { PrimitiveHoveredItem, Time } from 'lightweight-charts';
+
+type HandleId = 'start' | 'end';
+
+type TestMode = 'idle' | 'drawing' | 'ready' | 'dragging-start' | 'dragging-end' | 'dragging-body';
+
+interface TestState {
+  mode: TestMode;
+  startAnchor: Anchor | null;
+  endAnchor: Anchor | null;
+}
+
+class TestLinearDrawingBase extends LinearDrawingBase<SettingsValues, HandleId, TestState> {
+  protected mode: TestMode = 'ready';
+  protected settings: SettingsValues = {};
+
+  public readonly finishDrawingMock = jest.fn();
+  public readonly readyPointerDownMock = jest.fn();
+
+  private strokeColor: string | undefined;
+
+  public getRenderData(): TwoPointGeometry | null {
+    return this.getTwoPointGeometry();
+  }
+
+  public getState(): TestState {
+    return {
+      mode: this.mode,
+      startAnchor: this.startAnchor,
+      endAnchor: this.endAnchor,
+    };
+  }
+
+  public setState(): void {}
+
+  public getSettingsTabs(): SettingsTab[] {
+    return [];
+  }
+
+  public isCreationPending(): boolean {
+    return this.mode === 'idle' || this.mode === 'drawing';
+  }
+
+  public setMode(mode: TestMode): void {
+    this.mode = mode;
+  }
+
+  public setTestAnchors(startAnchor: Anchor | null, endAnchor: Anchor | null): void {
+    this.setAnchors(startAnchor, endAnchor);
+  }
+
+  public setStrokeColor(color: string | undefined): void {
+    this.strokeColor = color;
+  }
+
+  public getHandles() {
+    return this.getDrawingHandles();
+  }
+
+  public getTimeSegments() {
+    return this.getTimeAxisSegments();
+  }
+
+  public getPriceSegments() {
+    return this.getPriceAxisSegments();
+  }
+
+  public isNearLine(point: Point, tolerance: number): boolean {
+    return this.isPointNearLine(point, tolerance);
+  }
+
+  public start(point: Point): void {
+    this.startDrawing(point);
+  }
+
+  public update(point: Point): void {
+    this.updateDrawing(point);
+  }
+
+  public startDrag(mode: `dragging-${HandleId | 'body'}`, point: Point, pointerId: number): void {
+    this.startDragging(mode, point, pointerId);
+  }
+
+  public finishDrag(): void {
+    this.finishDragging();
+  }
+
+  public pointerDown(event: PointerEvent): void {
+    this.handlePointerDown(event);
+  }
+
+  public finishLinear(geometry: TwoPointGeometry | null, minimumSize: number, removeSelf: () => void): void {
+    this.finishLinearDrawing(geometry, minimumSize, removeSelf);
+  }
+
+  public getDragSnapshot(): TestState | null {
+    return this.dragStateSnapshot;
+  }
+
+  public getDragPointerId(): number | null {
+    return this.dragPointerId;
+  }
+
+  public getDragStartPoint(): Point | null {
+    return this.dragStartPoint;
+  }
+
+  protected getStartHandleId(): HandleId {
+    return 'start';
+  }
+
+  protected getEndHandleId(): HandleId {
+    return 'end';
+  }
+
+  protected getLineHandleStrokeColor(): string | undefined {
+    return this.strokeColor;
+  }
+
+  protected finishDrawing(): void {
+    this.finishDrawingMock();
+  }
+
+  protected handleReadyPointerDown(event: PointerEvent, point: Point): void {
+    this.readyPointerDownMock(event, point);
+  }
+
+  protected getHoveredItem(): PrimitiveHoveredItem | null {
+    return null;
+  }
+
+  protected getGeometry(): TwoPointGeometry | null {
+    return this.getTwoPointGeometry();
+  }
+
+  protected getTimeAxisLabel(): AxisLabel | null {
+    return null;
+  }
+
+  protected getPriceAxisLabel(): AxisLabel | null {
+    return null;
+  }
+}
+
+function createDrawing(selected = false) {
+  const env = createDrawingTestEnvironment({
+    selected,
+  });
+
+  const drawing = new TestLinearDrawingBase(env);
+
+  return {
+    ...env,
+    drawing,
+  };
+}
+
+function setAnchors(drawing: TestLinearDrawingBase): void {
+  drawing.setTestAnchors(
+    {
+      time: 10 as Time,
+      price: 20,
+    },
+    {
+      time: 110 as Time,
+      price: 120,
+    },
+  );
+}
+
+describe('LinearDrawingBase', () => {
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
+  describe('handles', () => {
+    it('должен создавать start и end handles', () => {
+      const { drawing } = createDrawing();
+
+      setAnchors(drawing);
+
+      expect(drawing.getHandles()).toEqual([
+        {
+          id: 'start',
+          x: 10,
+          y: 20,
+          shape: 'circle',
+        },
+        {
+          id: 'end',
+          x: 110,
+          y: 120,
+          shape: 'circle',
+        },
+      ]);
+    });
+
+    it('должен применять strokeColor к handles', () => {
+      const { drawing } = createDrawing();
+
+      setAnchors(drawing);
+
+      drawing.setStrokeColor('#123456');
+
+      expect(drawing.getHandles()).toEqual([
+        {
+          id: 'start',
+          x: 10,
+          y: 20,
+          shape: 'circle',
+          strokeColor: '#123456',
+        },
+        {
+          id: 'end',
+          x: 110,
+          y: 120,
+          shape: 'circle',
+          strokeColor: '#123456',
+        },
+      ]);
+    });
+  });
+
+  describe('axis segments', () => {
+    it('не должен возвращать axis segments для ready unselected drawing', () => {
+      const { drawing } = createDrawing();
+
+      setAnchors(drawing);
+
+      expect(drawing.getTimeSegments()).toEqual([]);
+
+      expect(drawing.getPriceSegments()).toEqual([]);
+    });
+
+    it('должен возвращать axis segments для selected drawing', () => {
+      const { drawing, setSelected } = createDrawing();
+
+      setAnchors(drawing);
+      setSelected(true);
+
+      expect(drawing.getTimeSegments()).toHaveLength(1);
+
+      expect(drawing.getTimeSegments()[0]).toMatchObject({
+        from: 10,
+        to: 110,
+      });
+
+      expect(drawing.getPriceSegments()[0]).toMatchObject({
+        from: 20,
+        to: 120,
+      });
+    });
+
+    it('должен возвращать axis segments во время создания', () => {
+      const { drawing } = createDrawing();
+
+      setAnchors(drawing);
+      drawing.setMode('drawing');
+
+      expect(drawing.getTimeSegments()).toHaveLength(1);
+    });
+  });
+
+  describe('hit testing', () => {
+    it('должен определять point рядом с line', () => {
+      const { drawing } = createDrawing();
+
+      drawing.setTestAnchors(
+        {
+          time: 0 as Time,
+          price: 0,
+        },
+        {
+          time: 100 as Time,
+          price: 0,
+        },
+      );
+
+      expect(
+        drawing.isNearLine(
+          {
+            x: 50,
+            y: 4,
+          },
+          5,
+        ),
+      ).toBe(true);
+
+      expect(
+        drawing.isNearLine(
+          {
+            x: 50,
+            y: 10,
+          },
+          5,
+        ),
+      ).toBe(false);
+    });
+  });
+
+  describe('drawing lifecycle', () => {
+    it('должен начинать drawing с одинаковых anchors', () => {
+      const { drawing } = createDrawing();
+
+      drawing.setMode('idle');
+
+      drawing.start({
+        x: 20,
+        y: 30,
+      });
+
+      expect(drawing.getState()).toEqual({
+        mode: 'drawing',
+        startAnchor: {
+          time: 20,
+          price: 30,
+        },
+        endAnchor: {
+          time: 20,
+          price: 30,
+        },
+      });
+    });
+
+    it('должен обновлять end anchor во время drawing', () => {
+      const { drawing } = createDrawing();
+
+      drawing.setMode('idle');
+
+      drawing.start({
+        x: 20,
+        y: 30,
+      });
+
+      drawing.update({
+        x: 80,
+        y: 90,
+      });
+
+      expect(drawing.getState().endAnchor).toEqual({
+        time: 80,
+        price: 90,
+      });
+    });
+
+    it('должен вызывать finishDrawing вторым pointerDown', () => {
+      const { drawing, container } = createDrawing();
+
+      drawing.setMode('idle');
+
+      drawing.pointerDown(createPointerEvent(container, 10, 20));
+
+      drawing.pointerDown(createPointerEvent(container, 100, 120));
+
+      expect(drawing.finishDrawingMock).toHaveBeenCalledTimes(1);
+    });
+
+    it('должен передавать ready pointerDown concrete drawing', () => {
+      const { drawing, container } = createDrawing();
+
+      drawing.setMode('ready');
+
+      const event = createPointerEvent(container, 40, 50);
+
+      drawing.pointerDown(event);
+
+      expect(drawing.readyPointerDownMock).toHaveBeenCalledWith(event, {
+        x: 40,
+        y: 50,
+      });
+    });
+  });
+
+  describe('dragging', () => {
+    it('должен сохранять state snapshot при начале drag', () => {
+      const { drawing, chart } = createDrawing();
+
+      setAnchors(drawing);
+
+      drawing.startDrag(
+        'dragging-body',
+        {
+          x: 50,
+          y: 60,
+        },
+        7,
+      );
+
+      expect(drawing.getDragPointerId()).toBe(7);
+
+      expect(drawing.getDragStartPoint()).toEqual({
+        x: 50,
+        y: 60,
+      });
+
+      expect(drawing.getDragSnapshot()).toEqual({
+        mode: 'dragging-body',
+        startAnchor: {
+          time: 10,
+          price: 20,
+        },
+        endAnchor: {
+          time: 110,
+          price: 120,
+        },
+      });
+
+      expect(chart.applyOptions).toHaveBeenCalledWith({
+        crosshair: {
+          mode: expect.any(Number),
+        },
+      });
+    });
+
+    it('должен очищать drag state после finishDragging', () => {
+      const { drawing } = createDrawing();
+
+      setAnchors(drawing);
+
+      drawing.startDrag(
+        'dragging-body',
+        {
+          x: 50,
+          y: 60,
+        },
+        7,
+      );
+
+      drawing.finishDrag();
+
+      expect(drawing.getState().mode).toBe('ready');
+
+      expect(drawing.getDragPointerId()).toBeNull();
+
+      expect(drawing.getDragStartPoint()).toBeNull();
+
+      expect(drawing.getDragSnapshot()).toBeNull();
+    });
+  });
+
+  describe('finishLinearDrawing', () => {
+    it('должен удалять слишком короткий drawing', () => {
+      const { drawing } = createDrawing();
+
+      const removeSelf = jest.fn();
+
+      drawing.finishLinear(
+        {
+          startPoint: {
+            x: 10,
+            y: 10,
+          },
+          endPoint: {
+            x: 12,
+            y: 12,
+          },
+          left: 10,
+          right: 12,
+          top: 10,
+          bottom: 12,
+        },
+        4,
+        removeSelf,
+      );
+
+      expect(removeSelf).toHaveBeenCalledTimes(1);
+    });
+
+    it('должен переводить корректный drawing в ready', () => {
+      const { drawing } = createDrawing();
+
+      drawing.setMode('drawing');
+
+      const removeSelf = jest.fn();
+
+      drawing.finishLinear(
+        {
+          startPoint: {
+            x: 10,
+            y: 10,
+          },
+          endPoint: {
+            x: 30,
+            y: 10,
+          },
+          left: 10,
+          right: 30,
+          top: 10,
+          bottom: 10,
+        },
+        4,
+        removeSelf,
+      );
+
+      expect(removeSelf).not.toHaveBeenCalled();
+
+      expect(drawing.getState().mode).toBe('ready');
+    });
+  });
+});
diff --git a/src/core/Drawings/__tests__/drawings/base/twoPointDrawingBase.test.ts b/src/core/Drawings/__tests__/drawings/base/twoPointDrawingBase.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b019767ab44473ab8da51555fed0d4dc9ceab651
--- /dev/null
+++ b/src/core/Drawings/__tests__/drawings/base/twoPointDrawingBase.test.ts
@@ -0,0 +1,519 @@
+import { type TwoPointAnchorKind, TwoPointDrawingBase, type TwoPointGeometry } from '@src/core/Drawings/base';
+
+import { createDrawingTestEnvironment } from '../../drawingTestUtils';
+
+import type { Anchor, AxisLabel, AxisSegment, Point } from '@core/Drawings/types';
+import type { SettingsTab, SettingsValues } from '@src/types';
+import type { PrimitiveHoveredItem, Time } from 'lightweight-charts';
+
+class TestTwoPointDrawingBase extends TwoPointDrawingBase<SettingsValues> {
+  protected mode: 'idle' | 'ready' = 'ready';
+  protected settings: SettingsValues = {};
+
+  public getRenderData(): TwoPointGeometry | null {
+    return this.getTwoPointGeometry();
+  }
+
+  public getState() {
+    return {
+      startAnchor: this.startAnchor,
+      endAnchor: this.endAnchor,
+    };
+  }
+
+  public setState(): void {}
+
+  public getSettingsTabs(): SettingsTab[] {
+    return [];
+  }
+
+  public isCreationPending(): boolean {
+    return this.mode === 'idle';
+  }
+
+  public setTestAnchors(startAnchor: Anchor | null, endAnchor: Anchor | null): void {
+    this.setAnchors(startAnchor, endAnchor);
+  }
+
+  public getTestAnchor(kind: TwoPointAnchorKind): Anchor | null {
+    return this.getAnchor(kind);
+  }
+
+  public setTestAnchor(kind: TwoPointAnchorKind, anchor: Anchor | null): void {
+    this.setAnchor(kind, anchor);
+  }
+
+  public createTestAnchor(point: Point): Anchor | null {
+    return this.createAnchor(point);
+  }
+
+  public setTestAnchorFromPoint(kind: TwoPointAnchorKind, point: Point): boolean {
+    return this.setAnchorFromPoint(kind, point);
+  }
+
+  public setTestAnchorsFromPoints(startPoint: Point, endPoint: Point): boolean {
+    return this.setAnchorsFromPoints(startPoint, endPoint);
+  }
+
+  public getTestTimeCoordinate(kind: TwoPointAnchorKind): number | null {
+    return this.getAnchorTimeCoordinate(kind);
+  }
+
+  public getTestPriceCoordinate(kind: TwoPointAnchorKind): number | null {
+    return this.getAnchorPriceCoordinate(kind);
+  }
+
+  public moveByPixels(
+    startAnchor: Anchor | null,
+    endAnchor: Anchor | null,
+    dragStartPoint: Point | null,
+    point: Point,
+  ): boolean {
+    return this.moveAnchorsByPixels(startAnchor, endAnchor, dragStartPoint, point);
+  }
+
+  public moveWithinContainer(
+    startAnchor: Anchor | null,
+    endAnchor: Anchor | null,
+    dragStartPoint: Point | null,
+    point: Point,
+    bounds: {
+      left: number;
+      right: number;
+      top: number;
+      bottom: number;
+    },
+  ): boolean {
+    return this.moveAnchorsWithinContainer(startAnchor, endAnchor, dragStartPoint, point, bounds);
+  }
+
+  public getTestGeometry(): TwoPointGeometry | null {
+    return this.getTwoPointGeometry();
+  }
+
+  public restoreTestAnchor(
+    kind: TwoPointAnchorKind,
+    state: Partial<{
+      startTime: Time | null;
+      endTime: Time | null;
+      startPrice: number | null;
+      endPrice: number | null;
+    }>,
+  ): void {
+    this.restoreAnchor(kind, state);
+  }
+
+  public getTestVolumeInRange(): number {
+    return this.getVolumeInRange();
+  }
+
+  public findTestIndexByTime(time: Time): number {
+    return this.findIndexByTime(time);
+  }
+
+  protected getHoveredItem(): PrimitiveHoveredItem | null {
+    return null;
+  }
+
+  protected getGeometry(): TwoPointGeometry | null {
+    return this.getTwoPointGeometry();
+  }
+
+  protected getTimeAxisSegments(): AxisSegment[] {
+    return [];
+  }
+
+  protected getPriceAxisSegments(): AxisSegment[] {
+    return [];
+  }
+
+  protected getTimeAxisLabel(): AxisLabel | null {
+    return null;
+  }
+
+  protected getPriceAxisLabel(): AxisLabel | null {
+    return null;
+  }
+}
+
+function createDrawing(options: Parameters<typeof createDrawingTestEnvironment>[0] = {}) {
+  const env = createDrawingTestEnvironment(options);
+  const drawing = new TestTwoPointDrawingBase(env);
+
+  return {
+    ...env,
+    drawing,
+  };
+}
+
+describe('TwoPointDrawingBase', () => {
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
+  describe('anchors', () => {
+    it('должен хранить start и end anchors', () => {
+      const { drawing } = createDrawing();
+
+      const startAnchor = {
+        time: 10 as Time,
+        price: 100,
+      };
+
+      const endAnchor = {
+        time: 20 as Time,
+        price: 200,
+      };
+
+      drawing.setTestAnchors(startAnchor, endAnchor);
+
+      expect(drawing.getTestAnchor('start')).toEqual(startAnchor);
+
+      expect(drawing.getTestAnchor('end')).toEqual(endAnchor);
+    });
+
+    it('должен создавать anchor из screen point', () => {
+      const { drawing } = createDrawing();
+
+      expect(
+        drawing.createTestAnchor({
+          x: 10,
+          y: 20,
+        }),
+      ).toEqual({
+        time: 10,
+        price: 20,
+      });
+    });
+
+    it('должен изменять один anchor из point', () => {
+      const { drawing } = createDrawing();
+
+      expect(
+        drawing.setTestAnchorFromPoint('start', {
+          x: 15,
+          y: 25,
+        }),
+      ).toBe(true);
+
+      expect(drawing.getTestAnchor('start')).toEqual({
+        time: 15,
+        price: 25,
+      });
+    });
+
+    it('не должен частично менять anchors, если одна из точек не преобразовалась', () => {
+      const { drawing, timeScale } = createDrawing();
+
+      drawing.setTestAnchors(
+        {
+          time: 1 as Time,
+          price: 10,
+        },
+        {
+          time: 2 as Time,
+          price: 20,
+        },
+      );
+
+      timeScale.coordinateToTime.mockReturnValueOnce(100 as Time).mockReturnValueOnce(null);
+
+      expect(
+        drawing.setTestAnchorsFromPoints(
+          {
+            x: 100,
+            y: 110,
+          },
+          {
+            x: 200,
+            y: 210,
+          },
+        ),
+      ).toBe(false);
+
+      expect(drawing.getState()).toEqual({
+        startAnchor: {
+          time: 1,
+          price: 10,
+        },
+        endAnchor: {
+          time: 2,
+          price: 20,
+        },
+      });
+    });
+  });
+
+  describe('coordinates and geometry', () => {
+    it('должен сохранять дробные coordinates anchors', () => {
+      const { drawing } = createDrawing();
+
+      drawing.setTestAnchors(
+        {
+          time: 10.25 as Time,
+          price: 30.75,
+        },
+        {
+          time: 20.5 as Time,
+          price: 40.125,
+        },
+      );
+
+      expect(drawing.getTestTimeCoordinate('start')).toBe(10.25);
+
+      expect(drawing.getTestPriceCoordinate('start')).toBe(30.75);
+    });
+
+    it('должен строить нормализованную geometry из anchors', () => {
+      const { drawing } = createDrawing();
+
+      drawing.setTestAnchors(
+        {
+          time: 80 as Time,
+          price: 90,
+        },
+        {
+          time: 20 as Time,
+          price: 30,
+        },
+      );
+
+      expect(drawing.getTestGeometry()).toEqual({
+        startPoint: {
+          x: 80,
+          y: 90,
+        },
+        endPoint: {
+          x: 20,
+          y: 30,
+        },
+        left: 20,
+        right: 80,
+        top: 30,
+        bottom: 90,
+      });
+    });
+
+    it('должен возвращать null geometry без двух anchors', () => {
+      const { drawing } = createDrawing();
+
+      drawing.setTestAnchor('start', {
+        time: 10 as Time,
+        price: 20,
+      });
+
+      expect(drawing.getTestGeometry()).toBeNull();
+    });
+  });
+
+  describe('moving', () => {
+    it('должен смещать оба anchors по времени и цене', () => {
+      const { drawing } = createDrawing();
+
+      const startAnchor = {
+        time: 10 as Time,
+        price: 100,
+      };
+
+      const endAnchor = {
+        time: 20 as Time,
+        price: 200,
+      };
+
+      expect(
+        drawing.moveByPixels(
+          startAnchor,
+          endAnchor,
+          {
+            x: 50,
+            y: 50,
+          },
+          {
+            x: 70,
+            y: 80,
+          },
+        ),
+      ).toBe(true);
+
+      expect(drawing.getState()).toEqual({
+        startAnchor: {
+          time: 30,
+          price: 130,
+        },
+        endAnchor: {
+          time: 40,
+          price: 230,
+        },
+      });
+    });
+
+    it('должен ограничивать смещение границами container', () => {
+      const { drawing } = createDrawing({
+        width: 200,
+        height: 200,
+      });
+
+      expect(
+        drawing.moveWithinContainer(
+          {
+            time: 50 as Time,
+            price: 40,
+          },
+          {
+            time: 150 as Time,
+            price: 140,
+          },
+          {
+            x: 50,
+            y: 50,
+          },
+          {
+            x: 200,
+            y: 200,
+          },
+          {
+            left: 50,
+            right: 150,
+            top: 40,
+            bottom: 140,
+          },
+        ),
+      ).toBe(true);
+
+      expect(drawing.getState()).toEqual({
+        startAnchor: {
+          time: 100,
+          price: 100,
+        },
+        endAnchor: {
+          time: 200,
+          price: 200,
+        },
+      });
+    });
+
+    it('не должен двигать anchors без dragStartPoint', () => {
+      const { drawing } = createDrawing();
+
+      expect(
+        drawing.moveByPixels(
+          {
+            time: 10 as Time,
+            price: 100,
+          },
+          {
+            time: 20 as Time,
+            price: 200,
+          },
+          null,
+          {
+            x: 30,
+            y: 40,
+          },
+        ),
+      ).toBe(false);
+    });
+  });
+
+  describe('state helpers', () => {
+    it('должен частично восстанавливать anchor из legacy state', () => {
+      const { drawing } = createDrawing();
+
+      drawing.setTestAnchor('start', {
+        time: 10 as Time,
+        price: 100,
+      });
+
+      drawing.restoreTestAnchor('start', {
+        startPrice: 150,
+      });
+
+      expect(drawing.getTestAnchor('start')).toEqual({
+        time: 10,
+        price: 150,
+      });
+    });
+
+    it('должен очищать anchor, если state содержит null time', () => {
+      const { drawing } = createDrawing();
+
+      drawing.setTestAnchor('start', {
+        time: 10 as Time,
+        price: 100,
+      });
+
+      drawing.restoreTestAnchor('start', {
+        startTime: null,
+      });
+
+      expect(drawing.getTestAnchor('start')).toBeNull();
+    });
+  });
+
+  describe('series helpers', () => {
+    it('должен находить index по времени', () => {
+      const { drawing } = createDrawing({
+        seriesData: [
+          {
+            time: 10,
+          },
+          {
+            time: 20,
+          },
+          {
+            time: 30,
+          },
+        ],
+      });
+
+      expect(drawing.findTestIndexByTime(20 as Time)).toBe(1);
+    });
+
+    it('должен суммировать volume из обычного и customValues поля', () => {
+      const { drawing } = createDrawing({
+        seriesData: [
+          {
+            time: 10,
+            volume: 5,
+          },
+          {
+            time: 20,
+            customValues: {
+              volume: 7,
+            },
+          },
+          {
+            time: 30,
+            volume: 3,
+          },
+        ],
+      });
+
+      drawing.setTestAnchors(
+        {
+          time: 10 as Time,
+          price: 100,
+        },
+        {
+          time: 30 as Time,
+          price: 200,
+        },
+      );
+
+      expect(drawing.getTestVolumeInRange()).toBe(15);
+    });
+
+    it('должен возвращать 0 volume без anchors', () => {
+      const { drawing } = createDrawing({
+        seriesData: [
+          {
+            time: 10,
+            volume: 5,
+          },
+        ],
+      });
+
+      expect(drawing.getTestVolumeInRange()).toBe(0);
+    });
+  });
+});
diff --git a/src/core/Drawings/__tests__/drawings/diapson.test.ts b/src/core/Drawings/__tests__/drawings/diapson.test.ts
index 2f73911810a10331e51626d0b29145850e1b1d3a..5544e0b8bb60d9854f0207b8146c8bb0ce02b9b4 100644
--- a/src/core/Drawings/__tests__/drawings/diapson.test.ts
+++ b/src/core/Drawings/__tests__/drawings/diapson.test.ts
@@ -35,22 +35,11 @@ jest.mock('@src/translations', () => ({
   t: (key: string) => key,
 }));
 
-jest.mock('@src/types/defaults', () => ({
-  Defaults: {
-    dateFormat: 'date',
-    timeFormat: '24h',
-    showTime: true,
-  },
-}));
-
 jest.mock('@src/utils', () => ({
   formatPercent: (value: number) => `${value}%`,
   formatPrice: (value: number) => `price:${value}`,
   formatSignedNumber: (value: number) => (value > 0 ? `+${value}` : String(value)),
   formatVolume: (value: number) => `volume:${value}`,
-}));
-
-jest.mock('@src/utils/formatter', () => ({
   formatDate: (value: number) => `date:${value}`,
 }));
 
diff --git a/src/core/Drawings/__tests__/drawings/fibonacciRetracement.test.ts b/src/core/Drawings/__tests__/drawings/fibonacciRetracement.test.ts
index 18ffb86f18c53f8d32edd8f004c5d10cc7526fef..5eca344bc39f50e29eb5c3cf5a99e5ff92d2ed03 100644
--- a/src/core/Drawings/__tests__/drawings/fibonacciRetracement.test.ts
+++ b/src/core/Drawings/__tests__/drawings/fibonacciRetracement.test.ts
@@ -40,19 +40,8 @@ jest.mock('@src/translations', () => ({
   t: (key: string) => key,
 }));
 
-jest.mock('@src/types/defaults', () => ({
-  Defaults: {
-    dateFormat: 'date',
-    timeFormat: '24h',
-    showTime: true,
-  },
-}));
-
 jest.mock('@src/utils', () => ({
   formatPrice: (value: number) => `price:${value}`,
-}));
-
-jest.mock('@src/utils/formatter', () => ({
   formatDate: (value: number) => `date:${value}`,
 }));
 
diff --git a/src/core/Drawings/__tests__/drawings/line.test.ts b/src/core/Drawings/__tests__/drawings/line.test.ts
index c53e3ba792cbb1ee34d96b99719ac50139dbfc89..b526813c1fdb9390931bce04d184ac24b4a6620b 100644
--- a/src/core/Drawings/__tests__/drawings/line.test.ts
+++ b/src/core/Drawings/__tests__/drawings/line.test.ts
@@ -32,19 +32,8 @@ jest.mock('@src/translations', () => ({
   t: (key: string) => key,
 }));
 
-jest.mock('@src/types/defaults', () => ({
-  Defaults: {
-    dateFormat: 'date',
-    timeFormat: '24h',
-    showTime: true,
-  },
-}));
-
 jest.mock('@src/utils', () => ({
   formatPrice: (value: number) => `price:${value}`,
-}));
-
-jest.mock('@src/utils/formatter', () => ({
   formatDate: (value: number) => `date:${value}`,
 }));
 
diff --git a/src/core/Drawings/__tests__/drawings/parallelChannel.test.ts b/src/core/Drawings/__tests__/drawings/parallelChannel.test.ts
index 9f4bf9c1ec7520132be36c4713819970dab3dbfb..02d72f09308db430b112904dc3ffe3cc0d3ac46d 100644
--- a/src/core/Drawings/__tests__/drawings/parallelChannel.test.ts
+++ b/src/core/Drawings/__tests__/drawings/parallelChannel.test.ts
@@ -30,19 +30,8 @@ jest.mock('@src/translations', () => ({
   t: (key: string) => key,
 }));
 
-jest.mock('@src/types/defaults', () => ({
-  Defaults: {
-    dateFormat: 'date',
-    timeFormat: '24h',
-    showTime: true,
-  },
-}));
-
 jest.mock('@src/utils', () => ({
   formatPrice: (value: number) => `price:${value}`,
-}));
-
-jest.mock('@src/utils/formatter', () => ({
   formatDate: (value: number) => `date:${value}`,
 }));
 
diff --git a/src/core/Drawings/__tests__/drawings/ray.test.ts b/src/core/Drawings/__tests__/drawings/ray.test.ts
index aea33af18054f31bf830ad863d4af5aa3627344d..0bbbe6155b6768225e7e600da96404b7ed8e2620 100644
--- a/src/core/Drawings/__tests__/drawings/ray.test.ts
+++ b/src/core/Drawings/__tests__/drawings/ray.test.ts
@@ -30,19 +30,8 @@ jest.mock('@src/translations', () => ({
   t: (key: string) => key,
 }));
 
-jest.mock('@src/types/defaults', () => ({
-  Defaults: {
-    dateFormat: 'date',
-    timeFormat: '24h',
-    showTime: true,
-  },
-}));
-
 jest.mock('@src/utils', () => ({
   formatPrice: (value: number) => `price:${value}`,
-}));
-
-jest.mock('@src/utils/formatter', () => ({
   formatDate: (value: number) => `date:${value}`,
 }));
 
diff --git a/src/core/Drawings/__tests__/drawings/rectangle.test.ts b/src/core/Drawings/__tests__/drawings/rectangle.test.ts
index 20cc89b7046282027b524310c7d6e1fd289895d1..63fee31235b92f58d2f21eeceb638b991cd20dea 100644
--- a/src/core/Drawings/__tests__/drawings/rectangle.test.ts
+++ b/src/core/Drawings/__tests__/drawings/rectangle.test.ts
@@ -28,19 +28,8 @@ jest.mock('@src/theme', () => ({
 
 jest.mock('@src/translations', () => ({ t: (value: string) => value }));
 
-jest.mock('@src/types/defaults', () => ({
-  Defaults: {
-    dateFormat: 'date',
-    timeFormat: '24h',
-    showTime: true,
-  },
-}));
-
 jest.mock('@src/utils', () => ({
   formatPrice: (value: number) => `price:${value}`,
-}));
-
-jest.mock('@src/utils/formatter', () => ({
   formatDate: (value: number) => `date:${value}`,
 }));
 
diff --git a/src/core/Drawings/__tests__/drawings/regressionTrend.test.ts b/src/core/Drawings/__tests__/drawings/regressionTrend.test.ts
index 399b3d0e45969fbbd106eb1c0e8e67c39e5ce886..144efda8783ec071aa2c75f229f44178ff2fb09c 100644
--- a/src/core/Drawings/__tests__/drawings/regressionTrend.test.ts
+++ b/src/core/Drawings/__tests__/drawings/regressionTrend.test.ts
@@ -28,17 +28,12 @@ jest.mock('@src/theme', () => ({
   }),
 }));
 
-jest.mock('@src/types/defaults', () => ({
-  Defaults: {
-    dateFormat: 'date',
-    timeFormat: '24h',
-    showTime: true,
-  },
-}));
-
 jest.mock('@src/translations', () => ({ t: (value: string) => value }));
-jest.mock('@src/utils', () => ({ formatPrice: (value: number) => `price:${value}` }));
-jest.mock('@src/utils/formatter', () => ({ formatDate: (value: number) => `date:${value}` }));
+
+jest.mock('@src/utils', () => ({
+  formatPrice: (value: number) => `price:${value}`,
+  formatDate: (value: number) => `date:${value}`,
+}));
 
 const seriesData = [
   { time: 10 as Time, close: 10 },
diff --git a/src/core/Drawings/__tests__/drawings/ruler.test.ts b/src/core/Drawings/__tests__/drawings/ruler.test.ts
index 69ebc65d964bcdcc12037333d609aa4e8bb2a976..7578edcc98b4fa0b6764a6ae0699a28259bcb09b 100644
--- a/src/core/Drawings/__tests__/drawings/ruler.test.ts
+++ b/src/core/Drawings/__tests__/drawings/ruler.test.ts
@@ -27,20 +27,9 @@ jest.mock('@src/theme', () => ({
 
 jest.mock('@src/translations', () => ({ t: (value: string) => value }));
 
-jest.mock('@src/types/defaults', () => ({
-  Defaults: {
-    dateFormat: 'date',
-    timeFormat: '24h',
-    showTime: true,
-  },
-}));
-
 jest.mock('@src/utils', () => ({
   formatPrice: (value: number) => `price:${value}`,
   formatVolume: (value: number) => `volume:${value}`,
-}));
-
-jest.mock('@src/utils/formatter', () => ({
   formatDate: (value: number) => `date:${value}`,
 }));
 
diff --git a/src/core/Drawings/__tests__/drawings/sliderPosition.test.ts b/src/core/Drawings/__tests__/drawings/sliderPosition.test.ts
index bd333e56259d939b4530cbab8370953c61ad49fb..cff0ad0473861310bebc470a0d603d01569ebdff 100644
--- a/src/core/Drawings/__tests__/drawings/sliderPosition.test.ts
+++ b/src/core/Drawings/__tests__/drawings/sliderPosition.test.ts
@@ -31,14 +31,6 @@ jest.mock('@src/translations', () => ({
   t: (value: string) => value,
 }));
 
-jest.mock('@src/types/defaults', () => ({
-  Defaults: {
-    dateFormat: 'date',
-    timeFormat: '24h',
-    showTime: true,
-  },
-}));
-
 jest.mock('@src/utils', () => ({
   formatPrice: (value: number, precision = 4) => {
     const factor = 10 ** precision;
@@ -53,9 +45,6 @@ jest.mock('@src/utils', () => ({
   formatSignedNumber: (value: number) => String(value),
   getPercentPrecision: () => 2,
   getPricePrecisionStep: () => 0.01,
-}));
-
-jest.mock('@src/utils/formatter', () => ({
   formatDate: (value: number) => `date:${value}`,
 }));
 
diff --git a/src/core/Drawings/__tests__/drawings/text.test.ts b/src/core/Drawings/__tests__/drawings/text.test.ts
index 2eec7aa12b30790be8648b153dd30d4ac8eede21..499436394dc88b0df8526070a59963600a3995ba 100644
--- a/src/core/Drawings/__tests__/drawings/text.test.ts
+++ b/src/core/Drawings/__tests__/drawings/text.test.ts
@@ -25,14 +25,6 @@ jest.mock('@src/theme', () => ({
 
 jest.mock('@src/translations', () => ({ t: (value: string) => value }));
 
-jest.mock('@src/types/defaults', () => ({
-  Defaults: {
-    dateFormat: 'date',
-    timeFormat: '24h',
-    showTime: true,
-  },
-}));
-
 jest.mock('@src/utils', () => ({
   formatDate: (value: number) => `date:${value}`,
   formatPrice: (value: number) => `price:${value}`,
diff --git a/src/core/Drawings/__tests__/drawings/volumeProfile.test.ts b/src/core/Drawings/__tests__/drawings/volumeProfile.test.ts
index 47fed6b915c25c7f2b25b086b436a8692c5f27cd..7cf8805b5a4381ee15b1da892e03ddafd4dba94c 100644
--- a/src/core/Drawings/__tests__/drawings/volumeProfile.test.ts
+++ b/src/core/Drawings/__tests__/drawings/volumeProfile.test.ts
@@ -23,21 +23,10 @@ jest.mock('@src/theme', () => ({
   }),
 }));
 
-jest.mock('@src/types/defaults', () => ({
-  Defaults: {
-    dateFormat: 'date',
-    timeFormat: '24h',
-    showTime: true,
-  },
-}));
-
 jest.mock('@src/translations', () => ({ t: (value: string) => value }));
 
 jest.mock('@src/utils', () => ({
   formatPrice: (value: number) => `price:${value}`,
-}));
-
-jest.mock('@src/utils/formatter', () => ({
   formatDate: (value: number) => `date:${value}`,
 }));
 
diff --git a/src/core/Drawings/__tests__/helpers.test.ts b/src/core/Drawings/__tests__/helpers.test.ts
index a91cba440acff5e0f18eb1244729c63b0fbbcaee..45ec7e01b5f8023a6398959d6e25581c1047b33c 100644
--- a/src/core/Drawings/__tests__/helpers.test.ts
+++ b/src/core/Drawings/__tests__/helpers.test.ts
@@ -17,7 +17,7 @@ import {
 } from '@core/Drawings/helpers';
 
 import type { SeriesApi } from '@core/Drawings/types';
-import type { Coordinate, IChartApi, Logical, Time } from 'lightweight-charts';
+import type { Coordinate, IChartApi, Time } from 'lightweight-charts';
 
 function createContainer(width = 400, height = 300, left = 10, top = 20) {
   const container = document.createElement('div');
@@ -42,14 +42,9 @@ function createChart() {
 
   const timeToCoordinate = jest.fn((time: Time): Coordinate | null => Number(time) as Coordinate);
 
-  const logicalToCoordinate = jest.fn((logical: Logical): Coordinate | null => {
-    return (Number(logical) * 10) as Coordinate;
-  });
-
   const timeScale = {
     coordinateToTime,
     timeToCoordinate,
-    logicalToCoordinate,
   };
 
   const chart = {
@@ -83,272 +78,487 @@ function createSeries(data: readonly { time: Time }[] = []) {
   };
 }
 
-describe('Drawing helpers', () => {
-  it('должен получать цену из y coordinate', () => {
-    const { series, coordinateToPrice } = createSeries();
+function mockTimeCoordinates(
+  timeToCoordinate: jest.Mock,
+  coordinates: Record<number, number>,
+  missingTimes: number[] = [],
+): void {
+  timeToCoordinate.mockImplementation((time: Time): Coordinate | null => {
+    if (typeof time !== 'number') {
+      return null;
+    }
 
-    expect(getPriceFromYCoordinate(series, 50)).toBe(50);
-    expect(coordinateToPrice).toHaveBeenCalledWith(50);
-  });
+    if (missingTimes.includes(time)) {
+      return null;
+    }
 
-  it('должен получать y coordinate из цены', () => {
-    const { series, priceToCoordinate } = createSeries();
+    const coordinate = coordinates[time];
 
-    expect(getYCoordinateFromPrice(series, 100)).toBe(100);
-    expect(priceToCoordinate).toHaveBeenCalledWith(100);
+    return coordinate === undefined ? null : (coordinate as Coordinate);
   });
+}
 
-  it('должен получать время из x coordinate', () => {
-    const { chart, timeScale } = createChart();
+describe('Drawing helpers', () => {
+  describe('price coordinates', () => {
+    it('должен получать цену из y coordinate', () => {
+      const { series, coordinateToPrice } = createSeries();
 
-    expect(getTimeFromXCoordinate(chart, 120)).toBe(120);
-    expect(timeScale.coordinateToTime).toHaveBeenCalledWith(120);
-  });
+      expect(getPriceFromYCoordinate(series, 50)).toBe(50);
+
+      expect(coordinateToPrice).toHaveBeenCalledWith(50);
+    });
 
-  it('должен возвращать прямую координату времени', () => {
-    const { chart } = createChart();
+    it('должен получать y coordinate из цены', () => {
+      const { series, priceToCoordinate } = createSeries();
 
-    expect(getXCoordinateFromTime(chart, 100 as Time)).toBe(100);
+      expect(getYCoordinateFromPrice(series, 100)).toBe(100);
+
+      expect(priceToCoordinate).toHaveBeenCalledWith(100);
+    });
   });
 
-  it('должен возвращать null без series, если координата времени отсутствует', () => {
-    const { chart, timeScale } = createChart();
+  describe('time coordinates', () => {
+    it('должен получать время из x coordinate', () => {
+      const { chart, timeScale } = createChart();
 
-    timeScale.timeToCoordinate.mockReturnValue(null);
+      expect(getTimeFromXCoordinate(chart, 120)).toBe(120);
 
-    expect(getXCoordinateFromTime(chart, 100 as Time)).toBeNull();
-  });
+      expect(timeScale.coordinateToTime).toHaveBeenCalledWith(120);
+    });
 
-  it('должен проецировать отсутствующее время через ближайшую свечу', () => {
-    const { chart, timeScale } = createChart();
+    it('должен возвращать прямую координату времени, если она существует', () => {
+      const { chart, timeScale } = createChart();
 
-    timeScale.timeToCoordinate.mockReturnValue(null);
+      expect(getXCoordinateFromTime(chart, 100 as Time)).toBe(100);
 
-    const { series } = createSeries([{ time: 10 as Time }, { time: 20 as Time }, { time: 30 as Time }]);
+      expect(timeScale.timeToCoordinate).toHaveBeenCalledWith(100);
+    });
 
-    expect(getXCoordinateFromTime(chart, 24 as Time, series)).toBe(10);
-    expect(timeScale.logicalToCoordinate).toHaveBeenCalledWith(1);
-  });
+    it('должен возвращать null без series, если координата времени отсутствует', () => {
+      const { chart, timeScale } = createChart();
 
-  it('должен выбирать предыдущую свечу при одинаковом расстоянии', () => {
-    const { chart, timeScale } = createChart();
+      timeScale.timeToCoordinate.mockReturnValue(null);
 
-    timeScale.timeToCoordinate.mockReturnValue(null);
+      expect(getXCoordinateFromTime(chart, 100 as Time)).toBeNull();
+    });
 
-    const { series } = createSeries([{ time: 10 as Time }, { time: 20 as Time }, { time: 30 as Time }]);
+    it('должен интерполировать координату между существующими свечами', () => {
+      const { chart, timeScale } = createChart();
 
-    expect(getXCoordinateFromTime(chart, 25 as Time, series)).toBe(10);
-  });
+      mockTimeCoordinates(
+        timeScale.timeToCoordinate,
+        {
+          20: 100,
+          30: 200,
+        },
+        [24],
+      );
 
-  it('должен использовать первую свечу для времени до диапазона', () => {
-    const { chart, timeScale } = createChart();
+      const { series } = createSeries([{ time: 20 as Time }, { time: 30 as Time }]);
 
-    timeScale.timeToCoordinate.mockReturnValue(null);
+      expect(getXCoordinateFromTime(chart, 24 as Time, series)).toBe(140);
+    });
 
-    const { series } = createSeries([{ time: 10 as Time }, { time: 20 as Time }]);
+    it('должен сохранять разницу координат для разных времён между одними свечами', () => {
+      const { chart, timeScale } = createChart();
 
-    expect(getXCoordinateFromTime(chart, 5 as Time, series)).toBe(0);
-  });
+      mockTimeCoordinates(
+        timeScale.timeToCoordinate,
+        {
+          20: 100,
+          30: 200,
+        },
+        [24, 25],
+      );
 
-  it('должен использовать последнюю свечу для времени после диапазона', () => {
-    const { chart, timeScale } = createChart();
+      const { series } = createSeries([{ time: 20 as Time }, { time: 30 as Time }]);
 
-    timeScale.timeToCoordinate.mockReturnValue(null);
+      expect(getXCoordinateFromTime(chart, 24 as Time, series)).toBe(140);
 
-    const { series } = createSeries([{ time: 10 as Time }, { time: 20 as Time }]);
+      expect(getXCoordinateFromTime(chart, 25 as Time, series)).toBe(150);
+    });
 
-    expect(getXCoordinateFromTime(chart, 30 as Time, series)).toBe(10);
-  });
+    it('должен экстраполировать координату для времени до диапазона', () => {
+      const { chart, timeScale } = createChart();
 
-  it('должен возвращать null при отсутствии данных series', () => {
-    const { chart, timeScale } = createChart();
+      mockTimeCoordinates(
+        timeScale.timeToCoordinate,
+        {
+          10: 100,
+          20: 200,
+        },
+        [5],
+      );
 
-    timeScale.timeToCoordinate.mockReturnValue(null);
+      const { series } = createSeries([{ time: 10 as Time }, { time: 20 as Time }]);
 
-    const { series } = createSeries();
+      expect(getXCoordinateFromTime(chart, 5 as Time, series)).toBe(50);
+    });
 
-    expect(getXCoordinateFromTime(chart, 30 as Time, series)).toBeNull();
-  });
+    it('должен экстраполировать координату для времени после диапазона', () => {
+      const { chart, timeScale } = createChart();
+
+      mockTimeCoordinates(
+        timeScale.timeToCoordinate,
+        {
+          10: 100,
+          20: 200,
+        },
+        [30],
+      );
 
-  it('должен возвращать null при невозможности спроецировать logical coordinate', () => {
-    const { chart, timeScale } = createChart();
+      const { series } = createSeries([{ time: 10 as Time }, { time: 20 as Time }]);
 
-    timeScale.timeToCoordinate.mockReturnValue(null);
-    timeScale.logicalToCoordinate.mockReturnValue(null);
+      expect(getXCoordinateFromTime(chart, 30 as Time, series)).toBe(300);
+    });
 
-    const { series } = createSeries([{ time: 10 as Time }]);
+    it('должен использовать координату единственной свечи, если в series одна точка', () => {
+      const { chart, timeScale } = createChart();
 
-    expect(getXCoordinateFromTime(chart, 10 as Time, series)).toBeNull();
-  });
+      mockTimeCoordinates(
+        timeScale.timeToCoordinate,
+        {
+          10: 100,
+        },
+        [15],
+      );
 
-  it('должен получать размеры container', () => {
-    const container = createContainer(500, 250);
+      const { series } = createSeries([{ time: 10 as Time }]);
 
-    expect(getContainerSize(container)).toEqual({
-      width: 500,
-      height: 250,
+      expect(getXCoordinateFromTime(chart, 15 as Time, series)).toBe(100);
     });
-  });
 
-  it('должен ограничивать point границами container', () => {
-    const container = createContainer(400, 300);
+    it('должен возвращать null при отсутствии данных series', () => {
+      const { chart, timeScale } = createChart();
 
-    expect(clampPointToContainer({ x: -20, y: 350 }, container)).toEqual({
-      x: 0,
-      y: 300,
+      timeScale.timeToCoordinate.mockReturnValue(null);
+
+      const { series } = createSeries();
+
+      expect(getXCoordinateFromTime(chart, 30 as Time, series)).toBeNull();
     });
-  });
 
-  it('должен получать raw pointer point относительно container', () => {
-    const container = createContainer(400, 300, 10, 20);
+    it('должен возвращать null, если координаты соседних свечей недоступны', () => {
+      const { chart, timeScale } = createChart();
 
-    const event = {
-      clientX: 110,
-      clientY: 70,
-    } as unknown as MouseEvent;
+      timeScale.timeToCoordinate.mockReturnValue(null);
 
-    expect(getRawPointerPoint(container, event)).toEqual({
-      x: 100,
-      y: 50,
+      const { series } = createSeries([{ time: 10 as Time }, { time: 20 as Time }]);
+
+      expect(getXCoordinateFromTime(chart, 15 as Time, series)).toBeNull();
     });
-  });
 
-  it('должен ограничивать pointer point границами container', () => {
-    const container = createContainer(400, 300, 10, 20);
+    it('должен возвращать null для отсутствующего ненумерического времени', () => {
+      const { chart, timeScale } = createChart();
 
-    const event = {
-      clientX: 500,
-      clientY: -10,
-    } as unknown as MouseEvent;
+      timeScale.timeToCoordinate.mockReturnValue(null);
 
-    expect(getPointerPoint(container, event)).toEqual({
-      x: 400,
-      y: 0,
+      const { series } = createSeries([{ time: 10 as Time }, { time: 20 as Time }]);
+
+      const time = {
+        year: 2026,
+        month: 9,
+        day: 21,
+      } as Time;
+
+      expect(getXCoordinateFromTime(chart, time, series)).toBeNull();
     });
-  });
 
-  it('должен определять близость point', () => {
-    expect(isNearPoint({ x: 108, y: 92 }, 100, 100, 8)).toBe(true);
-    expect(isNearPoint({ x: 109, y: 100 }, 100, 100, 8)).toBe(false);
-  });
+    it('должен возвращать null для невалидной прямой координаты без series', () => {
+      const { chart, timeScale } = createChart();
 
-  it('должен учитывать границу tolerance при проверке близости', () => {
-    expect(isNearPoint({ x: 108, y: 108 }, 100, 100, 8)).toBe(true);
-  });
+      timeScale.timeToCoordinate.mockReturnValue(Number.NaN as Coordinate);
 
-  it('должен определять point внутри bounds', () => {
-    const bounds = {
-      left: 10,
-      right: 100,
-      top: 20,
-      bottom: 80,
-    };
-
-    expect(isPointInBounds({ x: 50, y: 50 }, bounds)).toBe(true);
-    expect(isPointInBounds({ x: 5, y: 50 }, bounds)).toBe(false);
-    expect(isPointInBounds({ x: 5, y: 50 }, bounds, 5)).toBe(true);
+      expect(getXCoordinateFromTime(chart, 100 as Time)).toBeNull();
+    });
   });
 
-  it('должен нормализовать и ограничивать bounds', () => {
-    const container = createContainer(400, 300);
+  describe('container', () => {
+    it('должен получать размеры container', () => {
+      const container = createContainer(500, 250);
 
-    expect(normalizeBounds(450, -20, 350, -50, container)).toEqual({
-      left: 0,
-      right: 400,
-      top: 0,
-      bottom: 300,
+      expect(getContainerSize(container)).toEqual({
+        width: 500,
+        height: 250,
+      });
     });
-  });
 
-  it('должен смещать время на количество пикселей', () => {
-    const { chart } = createChart();
+    it('должен ограничивать point границами container', () => {
+      const container = createContainer(400, 300);
+
+      expect(
+        clampPointToContainer(
+          {
+            x: -20,
+            y: 350,
+          },
+          container,
+        ),
+      ).toEqual({
+        x: 0,
+        y: 300,
+      });
+    });
 
-    expect(shiftTimeByPixels(chart, 100 as Time, 20)).toBe(120);
-  });
+    it('должен получать raw pointer point относительно container', () => {
+      const container = createContainer(400, 300, 10, 20);
+
+      const event = {
+        clientX: 110,
+        clientY: 70,
+      } as unknown as MouseEvent;
 
-  it('должен возвращать null при отсутствии исходной координаты времени', () => {
-    const { chart, timeScale } = createChart();
+      expect(getRawPointerPoint(container, event)).toEqual({
+        x: 100,
+        y: 50,
+      });
+    });
+
+    it('должен ограничивать pointer point границами container', () => {
+      const container = createContainer(400, 300, 10, 20);
 
-    timeScale.timeToCoordinate.mockReturnValue(null);
+      const event = {
+        clientX: 500,
+        clientY: -10,
+      } as unknown as MouseEvent;
 
-    expect(shiftTimeByPixels(chart, 100 as Time, 20)).toBeNull();
+      expect(getPointerPoint(container, event)).toEqual({
+        x: 400,
+        y: 0,
+      });
+    });
   });
 
-  it('должен вычислять price delta', () => {
-    const { series } = createSeries();
+  describe('point helpers', () => {
+    it('должен определять близость point', () => {
+      expect(
+        isNearPoint(
+          {
+            x: 108,
+            y: 92,
+          },
+          100,
+          100,
+          8,
+        ),
+      ).toBe(true);
+
+      expect(
+        isNearPoint(
+          {
+            x: 109,
+            y: 100,
+          },
+          100,
+          100,
+          8,
+        ),
+      ).toBe(false);
+    });
 
-    expect(getPriceDelta(series, 20, 70)).toBe(50);
-  });
+    it('должен учитывать границу tolerance при проверке близости', () => {
+      expect(
+        isNearPoint(
+          {
+            x: 108,
+            y: 108,
+          },
+          100,
+          100,
+          8,
+        ),
+      ).toBe(true);
+    });
 
-  it('должен возвращать 0 price delta при отсутствии начальной цены', () => {
-    const { series, coordinateToPrice } = createSeries();
+    it('должен определять point внутри bounds', () => {
+      const bounds = {
+        left: 10,
+        right: 100,
+        top: 20,
+        bottom: 80,
+      };
+
+      expect(
+        isPointInBounds(
+          {
+            x: 50,
+            y: 50,
+          },
+          bounds,
+        ),
+      ).toBe(true);
+
+      expect(
+        isPointInBounds(
+          {
+            x: 5,
+            y: 50,
+          },
+          bounds,
+        ),
+      ).toBe(false);
+
+      expect(
+        isPointInBounds(
+          {
+            x: 5,
+            y: 50,
+          },
+          bounds,
+          5,
+        ),
+      ).toBe(true);
+    });
 
-    coordinateToPrice.mockReturnValueOnce(null);
+    it('должен нормализовать и ограничивать bounds', () => {
+      const container = createContainer(400, 300);
 
-    expect(getPriceDelta(series, 20, 70)).toBe(0);
+      expect(normalizeBounds(450, -20, 350, -50, container)).toEqual({
+        left: 0,
+        right: 400,
+        top: 0,
+        bottom: 300,
+      });
+    });
   });
 
-  it('должен возвращать 0 price delta при отсутствии конечной цены', () => {
-    const { series, coordinateToPrice } = createSeries();
+  describe('time shifting', () => {
+    it('должен смещать время на количество пикселей', () => {
+      const { chart } = createChart();
 
-    coordinateToPrice.mockReturnValueOnce(20).mockReturnValueOnce(null);
+      expect(shiftTimeByPixels(chart, 100 as Time, 20)).toBe(120);
+    });
 
-    expect(getPriceDelta(series, 20, 70)).toBe(0);
-  });
+    it('должен использовать projected coordinate при смещении отсутствующего времени', () => {
+      const { chart, timeScale } = createChart();
+
+      mockTimeCoordinates(
+        timeScale.timeToCoordinate,
+        {
+          20: 100,
+          30: 200,
+        },
+        [24],
+      );
 
-  it('должен вычислять диапазон цены в container', () => {
-    const container = createContainer(400, 300);
-    const { series, coordinateToPrice } = createSeries();
+      timeScale.coordinateToTime.mockImplementation((coordinate: Coordinate) => Number(coordinate) as Time);
 
-    coordinateToPrice.mockReturnValueOnce(150).mockReturnValueOnce(50);
+      const { series } = createSeries([{ time: 20 as Time }, { time: 30 as Time }]);
 
-    expect(getPriceRangeInContainer(series, container)).toEqual({
-      min: 50,
-      max: 150,
+      expect(shiftTimeByPixels(chart, 24 as Time, 20, series)).toBe(160);
     });
-  });
 
-  it('должен возвращать null диапазона при нулевой высоте', () => {
-    const container = createContainer(400, 0);
-    const { series } = createSeries();
+    it('должен возвращать null при отсутствии исходной координаты времени', () => {
+      const { chart, timeScale } = createChart();
+
+      timeScale.timeToCoordinate.mockReturnValue(null);
 
-    expect(getPriceRangeInContainer(series, container)).toBeNull();
+      expect(shiftTimeByPixels(chart, 100 as Time, 20)).toBeNull();
+    });
   });
 
-  it('должен возвращать null диапазона при отсутствии цены', () => {
-    const container = createContainer(400, 300);
-    const { series, coordinateToPrice } = createSeries();
+  describe('price delta', () => {
+    it('должен вычислять price delta', () => {
+      const { series } = createSeries();
 
-    coordinateToPrice.mockReturnValueOnce(null);
+      expect(getPriceDelta(series, 20, 70)).toBe(50);
+    });
 
-    expect(getPriceRangeInContainer(series, container)).toBeNull();
-  });
+    it('должен возвращать 0 price delta при отсутствии начальной цены', () => {
+      const { series, coordinateToPrice } = createSeries();
 
-  it('должен создавать anchor из point', () => {
-    const { chart } = createChart();
-    const { series } = createSeries();
+      coordinateToPrice.mockReturnValueOnce(null);
 
-    expect(getAnchorFromPoint(chart, series, { x: 100, y: 200 })).toEqual({
-      time: 100,
-      price: 200,
+      expect(getPriceDelta(series, 20, 70)).toBe(0);
+    });
+
+    it('должен возвращать 0 price delta при отсутствии конечной цены', () => {
+      const { series, coordinateToPrice } = createSeries();
+
+      coordinateToPrice.mockReturnValueOnce(20).mockReturnValueOnce(null);
+
+      expect(getPriceDelta(series, 20, 70)).toBe(0);
     });
   });
 
-  it('должен возвращать null anchor при отсутствии времени', () => {
-    const { chart, timeScale } = createChart();
-    const { series } = createSeries();
+  describe('price range', () => {
+    it('должен вычислять диапазон цены в container', () => {
+      const container = createContainer(400, 300);
+
+      const { series, coordinateToPrice } = createSeries();
 
-    timeScale.coordinateToTime.mockReturnValue(null);
+      coordinateToPrice.mockReturnValueOnce(150).mockReturnValueOnce(50);
 
-    expect(getAnchorFromPoint(chart, series, { x: 100, y: 200 })).toBeNull();
+      expect(getPriceRangeInContainer(series, container)).toEqual({
+        min: 50,
+        max: 150,
+      });
+    });
+
+    it('должен возвращать null диапазона при нулевой высоте', () => {
+      const container = createContainer(400, 0);
+
+      const { series } = createSeries();
+
+      expect(getPriceRangeInContainer(series, container)).toBeNull();
+    });
+
+    it('должен возвращать null диапазона при отсутствии цены', () => {
+      const container = createContainer(400, 300);
+
+      const { series, coordinateToPrice } = createSeries();
+
+      coordinateToPrice.mockReturnValueOnce(null);
+
+      expect(getPriceRangeInContainer(series, container)).toBeNull();
+    });
   });
 
-  it('должен возвращать null anchor при отсутствии цены', () => {
-    const { chart } = createChart();
-    const { series, coordinateToPrice } = createSeries();
+  describe('anchors', () => {
+    it('должен создавать anchor из point', () => {
+      const { chart } = createChart();
 
-    coordinateToPrice.mockReturnValue(null);
+      const { series } = createSeries();
 
-    expect(getAnchorFromPoint(chart, series, { x: 100, y: 200 })).toBeNull();
+      expect(
+        getAnchorFromPoint(chart, series, {
+          x: 100,
+          y: 200,
+        }),
+      ).toEqual({
+        time: 100,
+        price: 200,
+      });
+    });
+
+    it('должен возвращать null anchor при отсутствии времени', () => {
+      const { chart, timeScale } = createChart();
+
+      const { series } = createSeries();
+
+      timeScale.coordinateToTime.mockReturnValue(null);
+
+      expect(
+        getAnchorFromPoint(chart, series, {
+          x: 100,
+          y: 200,
+        }),
+      ).toBeNull();
+    });
+
+    it('должен возвращать null anchor при отсутствии цены', () => {
+      const { chart } = createChart();
+
+      const { series, coordinateToPrice } = createSeries();
+
+      coordinateToPrice.mockReturnValue(null);
+
+      expect(
+        getAnchorFromPoint(chart, series, {
+          x: 100,
+          y: 200,
+        }),
+      ).toBeNull();
+    });
   });
 });
diff --git a/src/core/Drawings/axis/CustomAxisView.ts b/src/core/Drawings/axis/CustomAxisView.ts
new file mode 100644
index 0000000000000000000000000000000000000000..da4136c7367f490bb8934de4428b79c30f92999a
--- /dev/null
+++ b/src/core/Drawings/axis/CustomAxisView.ts
@@ -0,0 +1,63 @@
+import type { AxisLabel } from '@core/Drawings/types';
+import type { ISeriesPrimitiveAxisView } from 'lightweight-charts';
+
+export interface CustomAxisViewParams {
+  getAxisLabel(labelKind: string): AxisLabel | null;
+  labelKind: string;
+  viewport: HTMLElement;
+}
+
+export abstract class CustomAxisView implements ISeriesPrimitiveAxisView {
+  private readonly getAxisLabel: (labelKind: string) => AxisLabel | null;
+  private readonly labelKind: string;
+  private readonly viewport: HTMLElement;
+
+  constructor({ getAxisLabel, labelKind, viewport }: CustomAxisViewParams) {
+    this.getAxisLabel = getAxisLabel;
+    this.labelKind = labelKind;
+    this.viewport = viewport;
+  }
+
+  public update(): void {
+    // Данные подписи читаются через getAxisLabel при каждом обращении
+  }
+
+  public visible(): boolean {
+    const label = this.getCurrentLabel();
+
+    if (!label?.text) {
+      return false;
+    }
+
+    const coordinate = Number(label.coordinate);
+    const size = this.getViewportSize(this.viewport.getBoundingClientRect());
+
+    return Number.isFinite(coordinate) && coordinate >= 0 && coordinate <= size;
+  }
+
+  protected abstract getViewportSize(rect: DOMRect): number;
+
+  public tickVisible(): boolean {
+    return false;
+  }
+
+  public coordinate(): number {
+    return this.getCurrentLabel()?.coordinate ?? 0;
+  }
+
+  public text(): string {
+    return this.getCurrentLabel()?.text ?? '';
+  }
+
+  public textColor(): string {
+    return this.getCurrentLabel()?.textColor ?? '';
+  }
+
+  public backColor(): string {
+    return this.getCurrentLabel()?.backgroundColor ?? '';
+  }
+
+  private getCurrentLabel(): AxisLabel | null {
+    return this.getAxisLabel(this.labelKind);
+  }
+}
diff --git a/src/core/Drawings/axis/CustomPriceAxis.ts b/src/core/Drawings/axis/CustomPriceAxis.ts
index d13e343b76319566bdf68673660cd5cec62df483..8176e5e3682609af032cc94cacf2e1262cd50dc6 100644
--- a/src/core/Drawings/axis/CustomPriceAxis.ts
+++ b/src/core/Drawings/axis/CustomPriceAxis.ts
@@ -1,23 +1,15 @@
 import { CanvasRenderingTarget2D } from 'fancy-canvas';
-import {
-  IPrimitivePaneRenderer,
-  IPrimitivePaneView,
-  ISeriesPrimitiveAxisView,
-  PrimitivePaneViewZOrder,
-} from 'lightweight-charts';
+import { IPrimitivePaneRenderer, IPrimitivePaneView, PrimitivePaneViewZOrder } from 'lightweight-charts';
 
-import type { AxisLabel, AxisSegment } from '@core/Drawings/types';
+import { CustomAxisView } from './CustomAxisView';
+
+import type { AxisSegment } from '@core/Drawings/types';
 
 interface CustomPriceAxisPaneViewParams {
   getAxisSegments(): AxisSegment[];
   zOrder?: PrimitivePaneViewZOrder;
 }
 
-interface CustomPriceAxisViewParams {
-  getAxisLabel(labelKind: string): AxisLabel | null;
-  labelKind: string;
-}
-
 export class CustomPriceAxisPaneView implements IPrimitivePaneView {
   private readonly rendererInstance: CustomPriceAxisPaneRenderer;
   private readonly paneZOrder: PrimitivePaneViewZOrder;
@@ -64,42 +56,8 @@ class CustomPriceAxisPaneRenderer implements IPrimitivePaneRenderer {
   }
 }
 
-export class CustomPriceAxisView implements ISeriesPrimitiveAxisView {
-  private readonly getAxisLabel: (labelKind: string) => AxisLabel | null;
-  private readonly labelKind: string;
-
-  constructor({ getAxisLabel, labelKind }: CustomPriceAxisViewParams) {
-    this.getAxisLabel = getAxisLabel;
-    this.labelKind = labelKind;
-  }
-
-  public update(): void {}
-
-  public visible(): boolean {
-    return Boolean(this.getCurrentLabel()?.text);
-  }
-
-  public tickVisible(): boolean {
-    return false;
-  }
-
-  public coordinate(): number {
-    return this.getCurrentLabel()?.coordinate ?? 0;
-  }
-
-  public text(): string {
-    return this.getCurrentLabel()?.text ?? '';
-  }
-
-  public textColor(): string {
-    return this.getCurrentLabel()?.textColor ?? '';
-  }
-
-  public backColor(): string {
-    return this.getCurrentLabel()?.backgroundColor ?? '';
-  }
-
-  private getCurrentLabel(): AxisLabel | null {
-    return this.getAxisLabel(this.labelKind);
+export class CustomPriceAxisView extends CustomAxisView {
+  protected getViewportSize(rect: DOMRect): number {
+    return rect.height;
   }
 }
diff --git a/src/core/Drawings/axis/CustomTimeAxis.ts b/src/core/Drawings/axis/CustomTimeAxis.ts
index 8cb6ce817cdb15964ac717823f430dc215400738..98b177b38643d979b24af61c26e48a3ae034f3a3 100644
--- a/src/core/Drawings/axis/CustomTimeAxis.ts
+++ b/src/core/Drawings/axis/CustomTimeAxis.ts
@@ -1,23 +1,15 @@
 import { CanvasRenderingTarget2D } from 'fancy-canvas';
-import {
-  IPrimitivePaneRenderer,
-  IPrimitivePaneView,
-  ISeriesPrimitiveAxisView,
-  PrimitivePaneViewZOrder,
-} from 'lightweight-charts';
+import { IPrimitivePaneRenderer, IPrimitivePaneView, PrimitivePaneViewZOrder } from 'lightweight-charts';
 
-import type { AxisLabel, AxisSegment } from '@core/Drawings/types';
+import { CustomAxisView } from './CustomAxisView';
+
+import type { AxisSegment } from '@core/Drawings/types';
 
 interface CustomTimeAxisPaneViewParams {
   getAxisSegments(): AxisSegment[];
   zOrder?: PrimitivePaneViewZOrder;
 }
 
-interface CustomTimeAxisViewParams {
-  getAxisLabel(labelKind: string): AxisLabel | null;
-  labelKind: string;
-}
-
 export class CustomTimeAxisPaneView implements IPrimitivePaneView {
   private readonly rendererInstance: CustomTimeAxisPaneRenderer;
   private readonly paneZOrder: PrimitivePaneViewZOrder;
@@ -64,42 +56,8 @@ class CustomTimeAxisPaneRenderer implements IPrimitivePaneRenderer {
   }
 }
 
-export class CustomTimeAxisView implements ISeriesPrimitiveAxisView {
-  private readonly getAxisLabel: (labelKind: string) => AxisLabel | null;
-  private readonly labelKind: string;
-
-  constructor({ getAxisLabel, labelKind }: CustomTimeAxisViewParams) {
-    this.getAxisLabel = getAxisLabel;
-    this.labelKind = labelKind;
-  }
-
-  public update(): void {}
-
-  public visible(): boolean {
-    return Boolean(this.getCurrentLabel()?.text);
-  }
-
-  public tickVisible(): boolean {
-    return false;
-  }
-
-  public coordinate(): number {
-    return this.getCurrentLabel()?.coordinate ?? 0;
-  }
-
-  public text(): string {
-    return this.getCurrentLabel()?.text ?? '';
-  }
-
-  public textColor(): string {
-    return this.getCurrentLabel()?.textColor ?? '';
-  }
-
-  public backColor(): string {
-    return this.getCurrentLabel()?.backgroundColor ?? '';
-  }
-
-  private getCurrentLabel(): AxisLabel | null {
-    return this.getAxisLabel(this.labelKind);
+export class CustomTimeAxisView extends CustomAxisView {
+  protected getViewportSize(rect: DOMRect): number {
+    return rect.width;
   }
 }
diff --git a/src/core/Drawings/axisLine/axisLine.ts b/src/core/Drawings/axisLine/axisLine.ts
index 11575873900784b788c923c2d4b3c9fc25e88011..9da496e9abda866ef0c076e722b784b3433974e3 100644
--- a/src/core/Drawings/axisLine/axisLine.ts
+++ b/src/core/Drawings/axisLine/axisLine.ts
@@ -1,3 +1,5 @@
+import { IPrimitivePaneView, PrimitiveHoveredItem, Time, UTCTimestamp } from 'lightweight-charts';
+
 import { CustomPriceAxisView, CustomTimeAxisView } from '@core/Drawings/axis';
 import {
   getPriceFromYCoordinate,
@@ -6,15 +8,12 @@ import {
   getYCoordinateFromPrice,
 } from '@core/Drawings/helpers';
 import { updateViews } from '@core/Drawings/utils';
-import { SeriesDrawingBase } from '@src/core/Drawings/SeriesDrawingBase';
 
-import { getThemeStore } from '@src/theme';
-import { Defaults } from '@src/types/defaults';
-import { formatPrice } from '@src/utils';
-import { formatDate } from '@src/utils/formatter';
+import { DrawingBase } from '@src/core/Drawings/base';
+import { Defaults } from '@src/types';
+import { formatDate, formatPrice } from '@src/utils';
 
 import { AxisLinePaneView } from './paneView';
-
 import {
   AxisLineSettings,
   AxisLineStyle,
@@ -25,9 +24,8 @@ import {
 
 import type { DrawingHandle } from '@core/Drawings/handles';
 import type { AxisLabel, AxisSegment, Point } from '@core/Drawings/types';
-import type { BaseDrawingParams, ISeriesDrawing } from '@src/core/Drawings/SeriesDrawingBase';
+import type { BaseDrawingParams, ISeriesDrawing } from '@src/core/Drawings/base/DrawingBase';
 import type { ChartOptionsModel, SettingsTab } from '@src/types';
-import type { IPrimitivePaneView, PrimitiveHoveredItem, Time, UTCTimestamp } from 'lightweight-charts';
 
 export type AxisLineDirection = 'vertical' | 'horizontal';
 
@@ -53,13 +51,13 @@ export interface AxisLineRenderData extends AxisLineStyle, AxisLineTextStyle {
 
 const LINE_HIT_TOLERANCE = 6;
 
-export class AxisLine extends SeriesDrawingBase<AxisLineSettings, AxisLineHandleKey> implements ISeriesDrawing {
+export class AxisLine extends DrawingBase<AxisLineSettings, AxisLineHandleKey> implements ISeriesDrawing {
   private readonly openSettings?: () => void;
+  private readonly direction: AxisLineDirection;
 
   protected settings: AxisLineSettings = createDefaultSettings();
   protected mode: AxisLineMode = 'idle';
 
-  private readonly direction: AxisLineDirection;
   private time: Time | null = null;
   private price: number | null = null;
   private dragPointerId: number | null = null;
@@ -88,33 +86,18 @@ export class AxisLine extends SeriesDrawingBase<AxisLineSettings, AxisLineHandle
 
     this.direction = direction;
     this.openSettings = openSettings;
-
     this.paneView = new AxisLinePaneView(this);
+    this.timeAxisView = this.createTimeAxisView('main');
+    this.priceAxisView = this.createPriceAxisView('main');
 
-    this.timeAxisView = new CustomTimeAxisView({
-      getAxisLabel: (kind) => this.getTimeAxisLabel(kind),
-      labelKind: 'main',
-    });
-
-    this.priceAxisView = new CustomPriceAxisView({
-      getAxisLabel: (kind) => this.getPriceAxisLabel(kind),
-      labelKind: 'main',
+    this.subscribeFormat(formatObservable, (format) => {
+      this.displayFormat = format;
     });
 
-    if (formatObservable) {
-      this.subscriptions.add(
-        formatObservable.subscribe((format) => {
-          this.displayFormat = format;
-          this.render();
-        }),
-      );
-    }
-
     this.series.attachPrimitive(this);
 
-    if (initialEvent && initialEvent.sourceEvent) {
-      const point = this.getEventPoint(initialEvent.sourceEvent);
-      this.startDrawing(point);
+    if (initialEvent?.sourceEvent) {
+      this.startDrawing(this.getEventPoint(initialEvent.sourceEvent));
     }
   }
 
@@ -132,22 +115,18 @@ export class AxisLine extends SeriesDrawingBase<AxisLineSettings, AxisLineHandle
     };
   }
 
-  public timeAxisPaneViews(): readonly IPrimitivePaneView[] {
-    return [];
-  }
-
-  public priceAxisPaneViews(): readonly IPrimitivePaneView[] {
-    return [];
-  }
-
   public setState(state: unknown): void {
+    if (!state || typeof state !== 'object') {
+      return;
+    }
+
     const nextState = state as Partial<AxisLineState>;
 
-    if ('hidden' in nextState && typeof nextState.hidden === 'boolean') {
+    if (typeof nextState.hidden === 'boolean') {
       this.hidden = nextState.hidden;
     }
 
-    if ('mode' in nextState && nextState.mode) {
+    if (nextState.mode) {
       this.mode = nextState.mode;
     }
 
@@ -159,7 +138,7 @@ export class AxisLine extends SeriesDrawingBase<AxisLineSettings, AxisLineHandle
       this.price = nextState.price ?? null;
     }
 
-    if ('settings' in nextState && nextState.settings) {
+    if (nextState.settings) {
       this.settings = {
         ...createDefaultSettings(),
         ...nextState.settings,
@@ -181,6 +160,14 @@ export class AxisLine extends SeriesDrawingBase<AxisLineSettings, AxisLineHandle
     return [this.paneView];
   }
 
+  public timeAxisPaneViews(): readonly IPrimitivePaneView[] {
+    return [];
+  }
+
+  public priceAxisPaneViews(): readonly IPrimitivePaneView[] {
+    return [];
+  }
+
   public timeAxisViews() {
     return this.direction === 'vertical' ? [this.timeAxisView] : [];
   }
@@ -232,9 +219,9 @@ export class AxisLine extends SeriesDrawingBase<AxisLineSettings, AxisLineHandle
     }
 
     const point = { x, y };
-    const data = this.getRenderData();
+    const coordinate = this.getCoordinate();
 
-    if (!data) {
+    if (coordinate === null) {
       return null;
     }
 
@@ -246,7 +233,7 @@ export class AxisLine extends SeriesDrawingBase<AxisLineSettings, AxisLineHandle
       };
     }
 
-    if (!this.isPointNearLine(point, data.coordinate)) {
+    if (!this.isPointNearLine(point, coordinate)) {
       return null;
     }
 
@@ -258,50 +245,36 @@ export class AxisLine extends SeriesDrawingBase<AxisLineSettings, AxisLineHandle
   }
 
   protected getTimeAxisLabel(kind: string): AxisLabel | null {
-    if (kind !== 'main' || this.direction !== 'vertical' || !this.isSelected() || this.time === null) {
+    if (
+      !this.isAxisLabelAvailable() ||
+      kind !== 'main' ||
+      this.direction !== 'vertical' ||
+      typeof this.time !== 'number'
+    ) {
       return null;
     }
 
     const coordinate = getXCoordinateFromTime(this.chart, this.time, this.series);
 
-    if (coordinate === null || typeof this.time !== 'number') {
-      return null;
-    }
-
-    const { colors } = getThemeStore();
-
-    return {
-      coordinate,
-      text: formatDate(
+    return this.createAxisLabel(
+      coordinate === null ? null : Number(coordinate),
+      formatDate(
         this.time as UTCTimestamp,
         this.displayFormat.dateFormat,
         this.displayFormat.timeFormat,
         this.displayFormat.showTime,
       ),
-      textColor: colors.chartPriceLineText,
-      backgroundColor: colors.axisMarkerLabelFill,
-    };
+    );
   }
 
   protected getPriceAxisLabel(kind: string): AxisLabel | null {
-    if (kind !== 'main' || this.direction !== 'horizontal' || !this.isSelected() || this.price === null) {
+    if (!this.isAxisLabelAvailable() || kind !== 'main' || this.direction !== 'horizontal' || this.price === null) {
       return null;
     }
 
     const coordinate = getYCoordinateFromPrice(this.series, this.price);
 
-    if (coordinate === null) {
-      return null;
-    }
-
-    const { colors } = getThemeStore();
-
-    return {
-      coordinate,
-      text: formatPrice(this.price) ?? '',
-      textColor: colors.chartPriceLineText,
-      backgroundColor: colors.axisMarkerLabelFill,
-    };
+    return this.createAxisLabel(coordinate === null ? null : Number(coordinate), formatPrice(this.price) ?? '');
   }
 
   protected getTimeAxisSegments(): AxisSegment[] {
@@ -321,23 +294,21 @@ export class AxisLine extends SeriesDrawingBase<AxisLineSettings, AxisLineHandle
       return;
     }
 
-    const data = this.getRenderData();
+    const coordinate = this.getCoordinate();
 
-    if (!data) {
+    if (coordinate === null) {
       return;
     }
 
     const point = this.getEventPoint(event as PointerEvent);
     const isNearHandle = this.isSelected() && Boolean(this.getDrawingHandleAtPoint(point));
-    const isNearLine = this.isPointNearLine(point, data.coordinate);
+    const isNearLine = this.isPointNearLine(point, coordinate);
 
     if (!isNearHandle && !isNearLine) {
       return;
     }
 
-    event.preventDefault();
-    event.stopPropagation();
-
+    this.consumeEvent(event);
     this.openSettings?.();
   };
 
@@ -349,9 +320,7 @@ export class AxisLine extends SeriesDrawingBase<AxisLineSettings, AxisLineHandle
     const point = this.getEventPoint(event);
 
     if (this.mode === 'idle') {
-      event.preventDefault();
-      event.stopPropagation();
-
+      this.consumeEvent(event);
       this.startDrawing(point);
       return;
     }
@@ -360,23 +329,21 @@ export class AxisLine extends SeriesDrawingBase<AxisLineSettings, AxisLineHandle
       return;
     }
 
-    const data = this.getRenderData();
+    const coordinate = this.getCoordinate();
 
-    if (!data) {
+    if (coordinate === null) {
       return;
     }
 
     const isNearHandle = this.isSelected() && Boolean(this.getDrawingHandleAtPoint(point));
-    const isNearLine = this.isPointNearLine(point, data.coordinate);
+    const isNearLine = this.isPointNearLine(point, coordinate);
 
     if (!this.isSelected()) {
       if (!isNearLine) {
         return;
       }
 
-      event.preventDefault();
-      event.stopPropagation();
-
+      this.consumeEvent(event);
       this.select();
       return;
     }
@@ -386,12 +353,9 @@ export class AxisLine extends SeriesDrawingBase<AxisLineSettings, AxisLineHandle
       return;
     }
 
-    event.preventDefault();
-    event.stopPropagation();
-
+    this.consumeEvent(event);
     this.mode = 'dragging';
     this.dragPointerId = event.pointerId;
-
     this.hideCrosshair();
     this.render();
   };
@@ -401,9 +365,7 @@ export class AxisLine extends SeriesDrawingBase<AxisLineSettings, AxisLineHandle
       return;
     }
 
-    event.preventDefault();
-    event.stopPropagation();
-
+    this.consumeEvent(event);
     this.updateLine(this.getEventPoint(event));
     this.render();
   };
@@ -415,7 +377,6 @@ export class AxisLine extends SeriesDrawingBase<AxisLineSettings, AxisLineHandle
 
     this.mode = 'ready';
     this.dragPointerId = null;
-
     this.resolveReady?.();
     this.showCrosshair();
     this.render();
@@ -424,7 +385,6 @@ export class AxisLine extends SeriesDrawingBase<AxisLineSettings, AxisLineHandle
   private startDrawing(point: Point): void {
     this.updateLine(point);
     this.mode = 'ready';
-
     this.resolveReady?.();
     this.render();
   }
diff --git a/src/core/Drawings/base/AreaDrawingBase.ts b/src/core/Drawings/base/AreaDrawingBase.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4213969d4abc33b23e3dc3a8e3f5707c1ba8e266
--- /dev/null
+++ b/src/core/Drawings/base/AreaDrawingBase.ts
@@ -0,0 +1,131 @@
+import { TwoPointDrawingBase } from './TwoPointDrawingBase';
+
+import type { AxisLabel, AxisSegment, Bounds, Point } from '@core/Drawings/types';
+import type { SettingsValues } from '@src/types';
+
+export abstract class AreaDrawingBase<
+  TSettings extends SettingsValues,
+  THandleId extends string,
+> extends TwoPointDrawingBase<TSettings, THandleId> {
+  protected abstract dragPointerId: number | null;
+
+  protected abstract activeDragTarget: THandleId | 'body' | null;
+
+  protected abstract finishDrawing(): void;
+
+  protected abstract containsPoint(point: Point): boolean;
+
+  protected abstract getDragTarget(point: Point): THandleId | 'body' | null;
+
+  protected abstract startDragging(point: Point, pointerId: number, target: THandleId | 'body'): void;
+
+  protected abstract moveWhole(point: Point): void;
+
+  protected abstract resize(point: Point): void;
+
+  protected abstract getGeometry(): Bounds | null;
+
+  protected startDrawing(point: Point): void {
+    const anchor = this.createAnchor(point);
+
+    if (!anchor) {
+      return;
+    }
+
+    this.setAnchors(anchor, anchor);
+    this.mode = 'drawing';
+    this.render();
+  }
+
+  protected updateDrawing(point: Point): void {
+    const clampedPoint = this.clampPointToContainer(point);
+
+    if (!this.setAnchorFromPoint('end', clampedPoint)) {
+      return;
+    }
+
+    this.render();
+  }
+
+  protected handlePointerDown = (event: PointerEvent): void => {
+    if (this.hidden || event.button !== 0) {
+      return;
+    }
+
+    const point = this.getEventPoint(event);
+
+    if (this.mode === 'idle') {
+      this.consumeEvent(event);
+      this.startDrawing(point);
+      return;
+    }
+
+    if (this.mode === 'drawing') {
+      this.consumeEvent(event);
+      this.updateDrawing(point);
+      this.finishDrawing();
+      return;
+    }
+
+    if (this.mode !== 'ready') {
+      return;
+    }
+
+    if (this.selectOnPointerDown(event, () => this.containsPoint(point))) {
+      return;
+    }
+
+    const dragTarget = this.getDragTarget(point);
+
+    if (!dragTarget) {
+      this.deselect();
+      return;
+    }
+
+    this.consumeEvent(event);
+    this.startDragging(point, event.pointerId, dragTarget);
+  };
+
+  protected handlePointerMove = (event: PointerEvent): void => {
+    const point = this.getEventPoint(event);
+
+    if (this.mode === 'drawing') {
+      this.updateDrawing(point);
+      return;
+    }
+
+    if (this.mode !== 'dragging' || this.dragPointerId !== event.pointerId) {
+      return;
+    }
+
+    event.preventDefault();
+
+    if (this.activeDragTarget === 'body') {
+      this.moveWhole(point);
+    } else {
+      this.resize(point);
+    }
+
+    this.render();
+  };
+
+  protected getTimeAxisSegments(): AxisSegment[] {
+    return this.getBoundsAxisSegments('time', () => this.getGeometry());
+  }
+
+  protected getPriceAxisSegments(): AxisSegment[] {
+    return this.getBoundsAxisSegments('price', () => this.getGeometry());
+  }
+
+  protected getBoundsAxisLabel<TKind extends keyof Bounds>(
+    kind: string,
+    kinds: readonly TKind[],
+    getText: (kind: TKind) => string,
+  ): AxisLabel | null {
+    return this.getInteractiveAxisLabel(kind, kinds, (labelKind) => {
+      const coordinate = this.getGeometry()?.[labelKind] ?? null;
+
+      return this.createAxisLabel(coordinate, getText(labelKind));
+    });
+  }
+}
diff --git a/src/core/Drawings/SeriesDrawingBase.ts b/src/core/Drawings/base/DrawingBase.ts
similarity index 53%
rename from src/core/Drawings/SeriesDrawingBase.ts
rename to src/core/Drawings/base/DrawingBase.ts
index 20d57ff0af50e013aab7fc2d8dd21d6568d4bd04..cb4076f3c59c49979072126b4a6e8ec72ab5610d 100644
--- a/src/core/Drawings/SeriesDrawingBase.ts
+++ b/src/core/Drawings/base/DrawingBase.ts
@@ -1,6 +1,30 @@
+import { CrosshairMode } from 'lightweight-charts';
+import { Subject, Subscription } from 'rxjs';
+
+import {
+  CustomPriceAxisPaneView,
+  CustomPriceAxisView,
+  CustomTimeAxisPaneView,
+  CustomTimeAxisView,
+} from '@core/Drawings/axis';
+import { DrawingHandlesPrimitive } from '@core/Drawings/handles';
 import {
+  getAnchorFromPoint,
+  getPointerPoint as getPointerPointFromEvent,
+  getRawPointerPoint,
+  getXCoordinateFromTime,
+  getYCoordinateFromPrice,
+} from '@core/Drawings/helpers';
+
+import { updateViews } from '@core/Drawings/utils';
+
+import { getThemeStore } from '@src/theme';
+
+import type { DrawingHandle } from '@core/Drawings/handles';
+import type { Anchor, AxisLabel, AxisSegment, Bounds, Point, SeriesApi, UpdatableView } from '@core/Drawings/types';
+import type { ChartOptionsModel, SettingsTab, SettingsValues } from '@src/types';
+import type {
   AutoscaleInfo,
-  CrosshairMode,
   IChartApi,
   IPrimitivePaneView,
   ISeriesApi,
@@ -9,21 +33,14 @@ import {
   Logical,
   MouseEventParams,
   PrimitiveHoveredItem,
+  PrimitivePaneViewZOrder,
   SeriesAttachedParameter,
   SeriesOptionsMap,
   SeriesType,
   Time,
   TouchMouseEventData,
 } from 'lightweight-charts';
-import { Observable, Subject, Subscription } from 'rxjs';
-
-import { DrawingHandlesPrimitive } from '@core/Drawings/handles';
-import { getPointerPoint as getPointerPointFromEvent, getRawPointerPoint } from '@core/Drawings/helpers';
-import { AxisLabel, AxisSegment, Point, SeriesApi } from '@core/Drawings/types';
-
-import { ChartOptionsModel, SettingsTab, SettingsValues } from '@src/types';
-
-import type { DrawingHandle } from '@core/Drawings/handles';
+import type { Observable } from 'rxjs';
 
 export interface DrawingInteraction {
   selected$: Observable<boolean>;
@@ -64,9 +81,7 @@ export interface ISeriesDrawing extends ISeriesPrimitive<Time> {
   getRenderData(): unknown;
 }
 
-export type StartPoint = unknown;
-
-interface SeriesDrawingBaseParams {
+interface DrawingBaseParams {
   container: HTMLElement;
   chart: IChartApi;
   series: SeriesApi;
@@ -84,10 +99,8 @@ export interface BaseDrawingParams {
   initialEvent?: MouseEventParams;
 }
 
-export abstract class SeriesDrawingBase<
-  TSettings extends SettingsValues = SettingsValues,
-  THandleId extends string = string,
-> implements ISeriesDrawing
+export abstract class DrawingBase<TSettings extends SettingsValues = SettingsValues, THandleId extends string = string>
+  implements ISeriesDrawing
 {
   protected hidden = false;
   protected chart: IChartApi;
@@ -107,7 +120,17 @@ export abstract class SeriesDrawingBase<
   protected resolveReady: (() => void) | null = null;
   protected requestUpdate: (() => void) | null = null;
 
-  constructor({ chart, series, container, interaction }: SeriesDrawingBaseParams) {
+  private registeredPaneViews: (IPrimitivePaneView & UpdatableView)[] = [];
+
+  private registeredTimePaneViews: CustomTimeAxisPaneView[] = [];
+
+  private registeredPricePaneViews: CustomPriceAxisPaneView[] = [];
+
+  private registeredTimeViews: CustomTimeAxisView[] = [];
+
+  private registeredPriceViews: CustomPriceAxisView[] = [];
+
+  constructor({ chart, series, container, interaction }: DrawingBaseParams) {
     this.chart = chart;
     this.series = series;
     this.container = container;
@@ -147,10 +170,8 @@ export abstract class SeriesDrawingBase<
     this.showCrosshair();
     this.unbindEvents();
     this.series.detachPrimitive(this as unknown as ISeriesPrimitive<Time>);
-
     this.series = series;
     this.requestUpdate = null;
-
     this.series.attachPrimitive(this as unknown as ISeriesPrimitive<Time>);
     this.render();
   }
@@ -245,7 +266,6 @@ export abstract class SeriesDrawingBase<
 
     if (!this.isLocked() || this.isCreationPending() || event.button !== 0) {
       this.handlePointerDown(event);
-
       return;
     }
 
@@ -253,7 +273,6 @@ export abstract class SeriesDrawingBase<
 
     if (this.getHoveredItem(point.x, point.y)) {
       this.select();
-
       return;
     }
 
@@ -291,12 +310,35 @@ export abstract class SeriesDrawingBase<
   public abstract getSettingsTabs(): SettingsTab[];
   public abstract isCreationPending(): boolean;
   public abstract setState(state: unknown): void;
-  public abstract updateAllViews(): void;
-  public abstract paneViews(): readonly IPrimitivePaneView[];
-  public abstract priceAxisPaneViews(): readonly IPrimitivePaneView[];
-  public abstract priceAxisViews(): readonly ISeriesPrimitiveAxisView[];
-  public abstract timeAxisPaneViews(): readonly IPrimitivePaneView[];
-  public abstract timeAxisViews(): readonly ISeriesPrimitiveAxisView[];
+  public updateAllViews(): void {
+    updateViews([
+      ...this.registeredPaneViews,
+      ...this.registeredTimePaneViews,
+      ...this.registeredPricePaneViews,
+      ...this.registeredTimeViews,
+      ...this.registeredPriceViews,
+    ]);
+  }
+
+  public paneViews(): readonly IPrimitivePaneView[] {
+    return [...this.registeredPaneViews];
+  }
+
+  public priceAxisPaneViews(): readonly IPrimitivePaneView[] {
+    return [...this.registeredPricePaneViews];
+  }
+
+  public priceAxisViews(): readonly ISeriesPrimitiveAxisView[] {
+    return [...this.registeredPriceViews];
+  }
+
+  public timeAxisPaneViews(): readonly IPrimitivePaneView[] {
+    return [...this.registeredTimePaneViews];
+  }
+
+  public timeAxisViews(): readonly ISeriesPrimitiveAxisView[] {
+    return [...this.registeredTimeViews];
+  }
 
   protected isSelected(): boolean {
     return this.interaction.isSelected();
@@ -306,6 +348,14 @@ export abstract class SeriesDrawingBase<
     return this.interaction.isLocked();
   }
 
+  protected isAxisLabelAvailable(): boolean {
+    return !this.hidden && !this.isCreationPending();
+  }
+
+  protected shouldShowInteractiveAxis(): boolean {
+    return this.isSelected() || this.isCreationPending();
+  }
+
   protected select(): void {
     this.interaction.select();
   }
@@ -326,6 +376,103 @@ export abstract class SeriesDrawingBase<
     return this.handlesPrimitive.findHandle(point);
   }
 
+  protected createAnchorFromPoint(point: Point): Anchor | null {
+    return getAnchorFromPoint(this.chart, this.series, point);
+  }
+
+  protected getPointFromAnchor(anchor: Anchor | null): Point | null {
+    if (!anchor) {
+      return null;
+    }
+
+    const x = getXCoordinateFromTime(this.chart, anchor.time, this.series);
+    const y = getYCoordinateFromPrice(this.series, anchor.price);
+
+    if (x === null || y === null) {
+      return null;
+    }
+
+    return {
+      x: Number(x),
+      y: Number(y),
+    };
+  }
+
+  protected createTimeAxisView(labelKind: string): CustomTimeAxisView {
+    return new CustomTimeAxisView({
+      getAxisLabel: (kind) => this.getTimeAxisLabel(kind),
+      labelKind,
+      viewport: this.container,
+    });
+  }
+
+  protected createPriceAxisView(labelKind: string): CustomPriceAxisView {
+    return new CustomPriceAxisView({
+      getAxisLabel: (kind) => this.getPriceAxisLabel(kind),
+      labelKind,
+      viewport: this.container,
+    });
+  }
+
+  protected createTimeAxisPaneView(zOrder: PrimitivePaneViewZOrder = 'bottom'): CustomTimeAxisPaneView {
+    return new CustomTimeAxisPaneView({
+      getAxisSegments: () => this.getTimeAxisSegments(),
+      zOrder,
+    });
+  }
+
+  protected createPriceAxisPaneView(zOrder: PrimitivePaneViewZOrder = 'bottom'): CustomPriceAxisPaneView {
+    return new CustomPriceAxisPaneView({
+      getAxisSegments: () => this.getPriceAxisSegments(),
+      zOrder,
+    });
+  }
+
+  protected createAxisLabel(
+    coordinate: number | null,
+    text: string,
+    style?: Partial<Pick<AxisLabel, 'textColor' | 'backgroundColor'>>,
+  ): AxisLabel | null {
+    if (coordinate === null || !text) {
+      return null;
+    }
+
+    const { colors } = getThemeStore();
+
+    return {
+      coordinate,
+      text,
+      textColor: style?.textColor ?? colors.chartPriceLineText,
+      backgroundColor: style?.backgroundColor ?? colors.axisMarkerLabelFill,
+    };
+  }
+
+  protected createAxisSegment(from: number, to: number, color?: string): AxisSegment {
+    const { colors } = getThemeStore();
+
+    return {
+      from,
+      to,
+      color: color ?? colors.axisMarkerAreaFill,
+    };
+  }
+
+  protected subscribeFormat(
+    formatObservable: Observable<ChartOptionsModel> | undefined,
+    callback: (format: ChartOptionsModel) => void,
+  ): void {
+    if (!formatObservable) {
+      return;
+    }
+
+    this.subscriptions.add(
+      formatObservable.subscribe((format) => {
+        callback(format);
+        this.render();
+      }),
+    );
+  }
+
   protected render(): void {
     this.updateAllViews();
     this.requestUpdate?.();
@@ -376,12 +523,29 @@ export abstract class SeriesDrawingBase<
   }
 
   // todo: хочется общую реализацию для каждой кнопки
-  protected handleClick(event: MouseEvent): void {}
-  protected handleContextMenu(event: MouseEvent): void {}
-  protected handleDoubleClick(event: MouseEvent): void {}
-  protected handlePointerMove(event: PointerEvent): void {}
-  protected handlePointerUp(event: PointerEvent): void {}
-  protected handlePointerDown(event: PointerEvent | TouchMouseEventData): void {}
+  protected handleClick(_event: MouseEvent): void {
+    // Обработка клика необязательна; нужные инструменты переопределяют этот метод
+  }
+
+  protected handleContextMenu(_event: MouseEvent): void {
+    // Контекстное меню обрабатывают только инструменты, которым оно требуется
+  }
+
+  protected handleDoubleClick(_event: MouseEvent): void {
+    // Двойной клик обрабатывают только инструменты с соответствующим действием
+  }
+
+  protected handlePointerMove(_event: PointerEvent): void {
+    // Инструменты без обработки перемещения указателя используют эту реализацию
+  }
+
+  protected handlePointerUp(_event: PointerEvent): void {
+    // Инструменты без обработки отпускания указателя используют эту реализацию
+  }
+
+  protected handlePointerDown(_event: PointerEvent | TouchMouseEventData): void {
+    // Некоторые инструменты используют события графика вместо нажатий указателя
+  }
 
   protected abstract getHoveredItem(x: number, y: number): PrimitiveHoveredItem | null;
   protected abstract getGeometry(): unknown; // todo: make proper type
@@ -390,6 +554,85 @@ export abstract class SeriesDrawingBase<
   protected abstract getTimeAxisLabel(kind: string): AxisLabel | null;
   protected abstract getPriceAxisLabel(kind: string): AxisLabel | null;
 
+  protected getBoundsAxisSegments(axis: 'time' | 'price', getBounds: () => Bounds | null): AxisSegment[] {
+    if (!this.shouldShowInteractiveAxis()) {
+      return [];
+    }
+
+    const bounds = getBounds();
+
+    if (!bounds) {
+      return [];
+    }
+
+    return axis === 'time'
+      ? [this.createAxisSegment(bounds.left, bounds.right)]
+      : [this.createAxisSegment(bounds.top, bounds.bottom)];
+  }
+
+  protected getInteractiveAxisLabel<TKind extends string>(
+    kind: string,
+    kinds: readonly TKind[],
+    getLabel: (kind: TKind) => AxisLabel | null,
+  ): AxisLabel | null {
+    const labelKind = kinds.find((candidate) => candidate === kind);
+
+    if (!this.shouldShowInteractiveAxis() || labelKind === undefined) {
+      return null;
+    }
+
+    return getLabel(labelKind);
+  }
+
+  protected consumeEvent(event: MouseEvent): void {
+    event.preventDefault();
+    event.stopPropagation();
+  }
+
+  protected selectOnPointerDown(event: PointerEvent, isHit: () => boolean): boolean {
+    if (this.isSelected()) {
+      return false;
+    }
+
+    if (isHit()) {
+      this.consumeEvent(event);
+      this.select();
+    }
+
+    return true;
+  }
+
+  protected openSettingsOnDoubleClick(
+    event: MouseEvent,
+    isHit: (point: Point) => boolean,
+    openSettings: (() => void) | undefined,
+    selectedOnly = false,
+  ): void {
+    if (this.hidden || this.mode !== 'ready' || (selectedOnly && !this.isSelected())) {
+      return;
+    }
+
+    if (!isHit(this.getEventPoint(event as PointerEvent))) {
+      return;
+    }
+
+    this.consumeEvent(event);
+    openSettings?.();
+  }
+
+  protected initializeViews(
+    paneView: IPrimitivePaneView & UpdatableView,
+    timeLabels: readonly string[],
+    priceLabels: readonly string[],
+    zOrder: PrimitivePaneViewZOrder = 'bottom',
+  ): void {
+    this.registeredPaneViews = [paneView];
+    this.registeredTimePaneViews = [this.createTimeAxisPaneView(zOrder)];
+    this.registeredPricePaneViews = [this.createPriceAxisPaneView(zOrder)];
+    this.registeredTimeViews = timeLabels.map((kind) => this.createTimeAxisView(kind));
+    this.registeredPriceViews = priceLabels.map((kind) => this.createPriceAxisView(kind));
+  }
+
   private bindInteraction(): void {
     if (this.isInteractionBound) {
       return;
diff --git a/src/core/Drawings/base/LinearDrawingBase.ts b/src/core/Drawings/base/LinearDrawingBase.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1d91ed0c9bfaadcbea456638ecd57e2ef6c2d247
--- /dev/null
+++ b/src/core/Drawings/base/LinearDrawingBase.ts
@@ -0,0 +1,165 @@
+import { getDistanceToSegment } from '@core/Drawings/utils';
+
+import { TwoPointDrawingBase, TwoPointGeometry } from './TwoPointDrawingBase';
+
+import type { DrawingHandle } from '@core/Drawings/handles';
+import type { AxisSegment, Point } from '@core/Drawings/types';
+import type { SettingsValues } from '@src/types';
+
+export abstract class LinearDrawingBase<
+  TSettings extends SettingsValues = SettingsValues,
+  THandleId extends string = string,
+  TState = unknown,
+> extends TwoPointDrawingBase<TSettings, THandleId> {
+  protected dragPointerId: number | null = null;
+
+  protected dragStartPoint: Point | null = null;
+
+  protected dragStateSnapshot: TState | null = null;
+
+  public abstract getState(): TState;
+
+  protected abstract getStartHandleId(): THandleId;
+
+  protected abstract getEndHandleId(): THandleId;
+
+  protected getLineHandleStrokeColor(): string | undefined {
+    return undefined;
+  }
+
+  protected getDrawingHandles(): readonly DrawingHandle<THandleId>[] {
+    const geometry = this.getTwoPointGeometry();
+
+    if (!geometry) {
+      return [];
+    }
+
+    const strokeColor = this.getLineHandleStrokeColor();
+
+    const handleStyle = strokeColor
+      ? {
+          strokeColor,
+        }
+      : {};
+
+    return [
+      {
+        id: this.getStartHandleId(),
+        ...geometry.startPoint,
+        shape: 'circle',
+        ...handleStyle,
+      },
+      {
+        id: this.getEndHandleId(),
+        ...geometry.endPoint,
+        shape: 'circle',
+        ...handleStyle,
+      },
+    ];
+  }
+
+  protected getTimeAxisSegments(): AxisSegment[] {
+    return this.getBoundsAxisSegments('time', () => this.getTwoPointGeometry());
+  }
+
+  protected getPriceAxisSegments(): AxisSegment[] {
+    return this.getBoundsAxisSegments('price', () => this.getTwoPointGeometry());
+  }
+
+  protected isPointNearLine(point: Point, tolerance: number): boolean {
+    const geometry = this.getTwoPointGeometry();
+
+    if (!geometry) {
+      return false;
+    }
+
+    return getDistanceToSegment(point, geometry.startPoint, geometry.endPoint) <= tolerance;
+  }
+
+  protected startDrawing(point: Point): void {
+    const anchor = this.createAnchor(point);
+
+    if (!anchor) {
+      return;
+    }
+
+    this.setAnchors(anchor, anchor);
+    this.mode = 'drawing';
+    this.render();
+  }
+
+  protected updateDrawing(point: Point): void {
+    if (!this.setAnchorFromPoint('end', point)) {
+      return;
+    }
+
+    this.render();
+  }
+
+  protected startDragging(mode: `dragging-${THandleId | 'body'}`, point: Point, pointerId: number): void {
+    this.mode = mode;
+    this.dragPointerId = pointerId;
+    this.dragStartPoint = point;
+    this.dragStateSnapshot = this.getState();
+    this.hideCrosshair();
+    this.render();
+  }
+
+  protected finishDragging(): void {
+    this.mode = 'ready';
+    this.resolveReady?.();
+    this.dragPointerId = null;
+    this.dragStartPoint = null;
+    this.dragStateSnapshot = null;
+    this.showCrosshair();
+    this.render();
+  }
+
+  protected abstract finishDrawing(): void;
+  protected abstract handleReadyPointerDown(event: PointerEvent, point: Point): void;
+
+  protected handlePointerDown = (event: PointerEvent): void => {
+    if (this.hidden || event.button !== 0) {
+      return;
+    }
+
+    const point = this.getEventPoint(event);
+
+    if (this.mode === 'idle' || this.mode === 'drawing') {
+      this.consumeEvent(event);
+
+      if (this.mode === 'idle') {
+        this.startDrawing(point);
+      } else {
+        this.updateDrawing(point);
+        this.finishDrawing();
+      }
+
+      return;
+    }
+
+    if (this.mode === 'ready') {
+      this.handleReadyPointerDown(event, point);
+    }
+  };
+
+  protected finishLinearDrawing(geometry: TwoPointGeometry | null, minimumSize: number, removeSelf: () => void): void {
+    if (!geometry) {
+      return;
+    }
+
+    const lineSize = Math.hypot(
+      geometry.endPoint.x - geometry.startPoint.x,
+      geometry.endPoint.y - geometry.startPoint.y,
+    );
+
+    if (lineSize < minimumSize) {
+      removeSelf();
+      return;
+    }
+
+    this.mode = 'ready';
+    this.resolveReady?.();
+    this.render();
+  }
+}
diff --git a/src/core/Drawings/base/TwoPointDrawingBase.ts b/src/core/Drawings/base/TwoPointDrawingBase.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ea5c7293ae518b3eed0e8800afb06f779f6b65a9
--- /dev/null
+++ b/src/core/Drawings/base/TwoPointDrawingBase.ts
@@ -0,0 +1,252 @@
+import { clamp } from 'lodash-es';
+
+import {
+  clampPointToContainer as clampPointToContainerInElement,
+  getContainerSize as getElementContainerSize,
+  getPriceDelta,
+  shiftTimeByPixels,
+} from '@core/Drawings/helpers';
+import { DrawingBase } from '@src/core/Drawings/base/DrawingBase';
+
+import type { Anchor, Bounds, ContainerSize, Point } from '@core/Drawings/types';
+import type { SettingsValues } from '@src/types';
+import type { Time } from 'lightweight-charts';
+
+export type TwoPointAnchorKind = 'start' | 'end';
+
+export interface TwoPointGeometry extends Bounds {
+  startPoint: Point;
+  endPoint: Point;
+}
+
+export abstract class TwoPointDrawingBase<
+  TSettings extends SettingsValues = SettingsValues,
+  THandleId extends string = string,
+> extends DrawingBase<TSettings, THandleId> {
+  protected startAnchor: Anchor | null = null;
+  protected endAnchor: Anchor | null = null;
+
+  protected getAnchor(kind: TwoPointAnchorKind): Anchor | null {
+    return kind === 'start' ? this.startAnchor : this.endAnchor;
+  }
+
+  protected setAnchor(kind: TwoPointAnchorKind, anchor: Anchor | null): void {
+    if (kind === 'start') {
+      this.startAnchor = anchor;
+      return;
+    }
+
+    this.endAnchor = anchor;
+  }
+
+  protected setAnchors(startAnchor: Anchor | null, endAnchor: Anchor | null): void {
+    this.startAnchor = startAnchor;
+    this.endAnchor = endAnchor;
+  }
+
+  protected createAnchor(point: Point): Anchor | null {
+    return this.createAnchorFromPoint(point);
+  }
+
+  protected setAnchorFromPoint(kind: TwoPointAnchorKind, point: Point): boolean {
+    const anchor = this.createAnchor(point);
+
+    if (!anchor) {
+      return false;
+    }
+
+    this.setAnchor(kind, anchor);
+
+    return true;
+  }
+
+  protected setAnchorsFromPoints(startPoint: Point, endPoint: Point): boolean {
+    const startAnchor = this.createAnchor(startPoint);
+    const endAnchor = this.createAnchor(endPoint);
+
+    if (!startAnchor || !endAnchor) {
+      return false;
+    }
+
+    this.setAnchors(startAnchor, endAnchor);
+
+    return true;
+  }
+
+  protected getAnchorTimeCoordinate(kind: TwoPointAnchorKind): number | null {
+    const point = this.getPointFromAnchor(this.getAnchor(kind));
+
+    return point?.x ?? null;
+  }
+
+  protected getAnchorPriceCoordinate(kind: TwoPointAnchorKind): number | null {
+    const point = this.getPointFromAnchor(this.getAnchor(kind));
+
+    return point?.y ?? null;
+  }
+
+  protected moveAnchorsByPixels(
+    startAnchor: Anchor | null,
+    endAnchor: Anchor | null,
+    dragStartPoint: Point | null,
+    point: Point,
+  ): boolean {
+    if (!dragStartPoint) {
+      return false;
+    }
+
+    const offsetX = point.x - dragStartPoint.x;
+    const priceOffset = getPriceDelta(this.series, dragStartPoint.y, point.y);
+
+    return this.moveAnchors(startAnchor, endAnchor, offsetX, priceOffset);
+  }
+
+  protected moveAnchorsWithinContainer(
+    startAnchor: Anchor | null,
+    endAnchor: Anchor | null,
+    dragStartPoint: Point | null,
+    point: Point,
+    bounds: Bounds,
+  ): boolean {
+    if (!dragStartPoint) {
+      return false;
+    }
+
+    const { width, height } = this.getContainerSize();
+    const rawOffsetX = point.x - dragStartPoint.x;
+    const rawOffsetY = point.y - dragStartPoint.y;
+    const offsetX = clamp(rawOffsetX, -bounds.left, width - bounds.right);
+    const offsetY = clamp(rawOffsetY, -bounds.top, height - bounds.bottom);
+    const priceOffset = getPriceDelta(this.series, dragStartPoint.y, dragStartPoint.y + offsetY);
+
+    return this.moveAnchors(startAnchor, endAnchor, offsetX, priceOffset);
+  }
+
+  protected getContainerSize(): ContainerSize {
+    return getElementContainerSize(this.container);
+  }
+
+  protected clampPointToContainer(point: Point): Point {
+    return clampPointToContainerInElement(point, this.container);
+  }
+
+  protected getTwoPointGeometry(): TwoPointGeometry | null {
+    const startPoint = this.getPointFromAnchor(this.startAnchor);
+    const endPoint = this.getPointFromAnchor(this.endAnchor);
+
+    if (!startPoint || !endPoint) {
+      return null;
+    }
+
+    return {
+      startPoint,
+      endPoint,
+      left: Math.min(startPoint.x, endPoint.x),
+      right: Math.max(startPoint.x, endPoint.x),
+      top: Math.min(startPoint.y, endPoint.y),
+      bottom: Math.max(startPoint.y, endPoint.y),
+    };
+  }
+
+  private moveAnchors(
+    startAnchor: Anchor | null,
+    endAnchor: Anchor | null,
+    offsetX: number,
+    priceOffset: number,
+  ): boolean {
+    if (!startAnchor || !endAnchor) {
+      return false;
+    }
+
+    const nextStartTime = shiftTimeByPixels(this.chart, startAnchor.time, offsetX, this.series);
+    const nextEndTime = shiftTimeByPixels(this.chart, endAnchor.time, offsetX, this.series);
+
+    if (nextStartTime === null || nextEndTime === null) {
+      return false;
+    }
+
+    this.startAnchor = {
+      time: nextStartTime,
+      price: startAnchor.price + priceOffset,
+    };
+
+    this.endAnchor = {
+      time: nextEndTime,
+      price: endAnchor.price + priceOffset,
+    };
+
+    return true;
+  }
+
+  protected restoreAnchor(
+    kind: TwoPointAnchorKind,
+    state: Partial<{
+      startTime: Anchor['time'] | null;
+      endTime: Anchor['time'] | null;
+      startPrice: number | null;
+      endPrice: number | null;
+    }>,
+  ): void {
+    const timeKey = kind === 'start' ? 'startTime' : 'endTime';
+    const priceKey = kind === 'start' ? 'startPrice' : 'endPrice';
+
+    if (!(timeKey in state) && !(priceKey in state)) {
+      return;
+    }
+
+    const anchor = this.getAnchor(kind);
+    const time = timeKey in state ? (state[timeKey] ?? null) : (anchor?.time ?? null);
+    const price = priceKey in state ? (state[priceKey] ?? null) : (anchor?.price ?? null);
+    this.setAnchor(kind, time !== null && price !== null ? { time, price } : null);
+  }
+
+  protected getVolumeInRange(): number {
+    if (!this.startAnchor || !this.endAnchor) {
+      return 0;
+    }
+
+    const data = this.series.data() ?? [];
+
+    if (!data.length) {
+      return 0;
+    }
+
+    const startIndex = this.findIndexByTime(this.startAnchor.time);
+    const endIndex = this.findIndexByTime(this.endAnchor.time);
+
+    if (startIndex < 0 || endIndex < 0) {
+      return 0;
+    }
+
+    const from = Math.min(startIndex, endIndex);
+    const to = Math.max(startIndex, endIndex);
+    let volume = 0;
+
+    for (let index = from; index <= to; index += 1) {
+      const item = data[index] as unknown as Record<string, unknown> | undefined;
+
+      if (!item) {
+        continue;
+      }
+
+      if (typeof item.volume === 'number') {
+        volume += item.volume;
+        continue;
+      }
+
+      const customValues = item.customValues as Record<string, unknown> | undefined;
+
+      if (customValues && typeof customValues.volume === 'number') {
+        volume += customValues.volume;
+      }
+    }
+
+    return volume;
+  }
+
+  protected findIndexByTime(time: Time): number {
+    const data = this.series.data() ?? [];
+
+    return data.findIndex((item) => Number(item.time) === Number(time));
+  }
+}
diff --git a/src/core/Drawings/base/index.ts b/src/core/Drawings/base/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..21fce817a94cab8620d0efb3bd5dcef537ba9d87
--- /dev/null
+++ b/src/core/Drawings/base/index.ts
@@ -0,0 +1,8 @@
+export { AreaDrawingBase } from './AreaDrawingBase';
+export { DrawingBase } from './DrawingBase';
+export { LinearDrawingBase } from './LinearDrawingBase';
+export { TwoPointDrawingBase } from './TwoPointDrawingBase';
+
+export type { BaseDrawingParams, DrawingInteraction, ISeriesDrawing } from './DrawingBase';
+
+export type { TwoPointAnchorKind, TwoPointGeometry } from './TwoPointDrawingBase';
diff --git a/src/core/Drawings/diapson/diapson.ts b/src/core/Drawings/diapson/diapson.ts
index cb50bb26ff0c556a9b554ec5f86a686935a14d81..a97859b80bfba9a1ef7d8c215a340ded34e99ae4 100644
--- a/src/core/Drawings/diapson/diapson.ts
+++ b/src/core/Drawings/diapson/diapson.ts
@@ -1,30 +1,14 @@
+import { PrimitiveHoveredItem, Time, UTCTimestamp } from 'lightweight-charts';
 import { clamp } from 'lodash-es';
 
-import {
-  CustomPriceAxisPaneView,
-  CustomPriceAxisView,
-  CustomTimeAxisPaneView,
-  CustomTimeAxisView,
-} from '@core/Drawings/axis';
-import {
-  clampPointToContainer as clampPointToContainerInElement,
-  getAnchorFromPoint,
-  getContainerSize as getElementContainerSize,
-  getXCoordinateFromTime,
-  getYCoordinateFromPrice,
-  isPointInBounds,
-} from '@core/Drawings/helpers';
-import { updateViews } from '@core/Drawings/utils';
-import { SeriesDrawingBase } from '@src/core/Drawings/SeriesDrawingBase';
-
-import { getThemeStore } from '@src/theme';
+import { isPointInBounds } from '@core/Drawings/helpers';
+
+import { AreaDrawingBase, BaseDrawingParams, ISeriesDrawing } from '@src/core/Drawings/base';
 import { t } from '@src/translations';
-import { Defaults } from '@src/types/defaults';
-import { formatPercent, formatPrice, formatSignedNumber, formatVolume } from '@src/utils';
-import { formatDate } from '@src/utils/formatter';
+import { type ChartOptionsModel, Defaults, type SettingsTab } from '@src/types';
+import { formatDate, formatPercent, formatPrice, formatSignedNumber, formatVolume } from '@src/utils';
 
 import { DiapsonPaneView } from './paneView';
-
 import {
   createDefaultSettings,
   DiapsonSettings,
@@ -34,10 +18,7 @@ import {
 } from './settings';
 
 import type { DrawingHandle } from '@core/Drawings/handles';
-import type { Anchor, AxisLabel, AxisSegment, Point } from '@core/Drawings/types';
-import type { BaseDrawingParams, ISeriesDrawing } from '@src/core/Drawings/SeriesDrawingBase';
-import type { ChartOptionsModel, SettingsTab } from '@src/types';
-import type { IPrimitivePaneView, PrimitiveHoveredItem, Time, UTCTimestamp } from 'lightweight-charts';
+import type { AxisLabel, Point } from '@core/Drawings/types';
 
 export type DiapsonRangeMode = 'date' | 'price';
 
@@ -97,22 +78,16 @@ const BODY_HIT_TOLERANCE = 6;
 const MIN_RECTANGLE_WIDTH = 6;
 const MIN_RECTANGLE_HEIGHT = 6;
 
-export class Diapson extends SeriesDrawingBase<DiapsonSettings, DiapsonHandleKey> implements ISeriesDrawing {
+export class Diapson extends AreaDrawingBase<DiapsonSettings, DiapsonHandleKey> implements ISeriesDrawing {
   private removeSelf?: () => void;
   private openSettings?: () => void;
 
   protected settings: DiapsonSettings = createDefaultSettings();
-
   protected mode: DiapsonMode = 'idle';
-  private rangeMode: DiapsonRangeMode;
-
-  private startTime: Time | null = null;
-  private endTime: Time | null = null;
-  private startPrice: number | null = null;
-  private endPrice: number | null = null;
 
-  private activeDragTarget: DiapsonHandle = null;
-  private dragPointerId: number | null = null;
+  private rangeMode: DiapsonRangeMode;
+  protected activeDragTarget: DiapsonHandle = null;
+  protected dragPointerId: number | null = null;
   private dragStartPoint: Point | null = null;
   private dragGeometrySnapshot: DiapsonGeometry | null = null;
 
@@ -125,14 +100,6 @@ export class Diapson extends SeriesDrawingBase<DiapsonSettings, DiapsonHandleKey
   private readonly stepSize: number;
   private readonly stepLabel: string;
 
-  private readonly paneView: DiapsonPaneView;
-  private readonly timeAxisPaneView: CustomTimeAxisPaneView;
-  private readonly priceAxisPaneView: CustomPriceAxisPaneView;
-  private readonly leftTimeAxisView: CustomTimeAxisView;
-  private readonly rightTimeAxisView: CustomTimeAxisView;
-  private readonly topPriceAxisView: CustomPriceAxisView;
-  private readonly bottomPriceAxisView: CustomPriceAxisView;
-
   constructor({
     chart,
     series,
@@ -146,63 +113,23 @@ export class Diapson extends SeriesDrawingBase<DiapsonSettings, DiapsonHandleKey
     stepLabel = '',
     initialEvent,
   }: DiapsonParams) {
-    super({
-      chart,
-      series,
-      container,
-      interaction,
-    });
+    super({ chart, series, container, interaction });
 
     this.rangeMode = rangeMode;
     this.removeSelf = removeSelf;
     this.openSettings = openSettings;
     this.stepSize = stepSize > 0 ? stepSize : 1;
     this.stepLabel = stepLabel;
+    this.initializeViews(new DiapsonPaneView(this), ['left', 'right'], ['top', 'bottom']);
 
-    this.paneView = new DiapsonPaneView(this);
-
-    this.timeAxisPaneView = new CustomTimeAxisPaneView({
-      getAxisSegments: () => this.getTimeAxisSegments(),
+    this.subscribeFormat(formatObservable, (format) => {
+      this.displayFormat = format;
     });
 
-    this.priceAxisPaneView = new CustomPriceAxisPaneView({
-      getAxisSegments: () => this.getPriceAxisSegments(),
-    });
-
-    this.leftTimeAxisView = new CustomTimeAxisView({
-      getAxisLabel: (kind) => this.getTimeAxisLabel(kind),
-      labelKind: 'left',
-    });
-
-    this.rightTimeAxisView = new CustomTimeAxisView({
-      getAxisLabel: (kind) => this.getTimeAxisLabel(kind),
-      labelKind: 'right',
-    });
-
-    this.topPriceAxisView = new CustomPriceAxisView({
-      getAxisLabel: (kind) => this.getPriceAxisLabel(kind),
-      labelKind: 'top',
-    });
-
-    this.bottomPriceAxisView = new CustomPriceAxisView({
-      getAxisLabel: (kind) => this.getPriceAxisLabel(kind),
-      labelKind: 'bottom',
-    });
-
-    if (formatObservable) {
-      this.subscriptions.add(
-        formatObservable.subscribe((format) => {
-          this.displayFormat = format;
-          this.render();
-        }),
-      );
-    }
-
     this.series.attachPrimitive(this);
 
-    if (initialEvent && initialEvent.sourceEvent) {
-      const point = this.getEventPoint(initialEvent.sourceEvent);
-      this.startDrawing(point);
+    if (initialEvent?.sourceEvent) {
+      this.startDrawing(this.getEventPoint(initialEvent.sourceEvent));
     }
   }
 
@@ -224,10 +151,10 @@ export class Diapson extends SeriesDrawingBase<DiapsonSettings, DiapsonHandleKey
       hidden: this.hidden,
       mode: this.mode,
       rangeMode: this.rangeMode,
-      startTime: this.startTime,
-      endTime: this.endTime,
-      startPrice: this.startPrice,
-      endPrice: this.endPrice,
+      startTime: this.startAnchor?.time ?? null,
+      endTime: this.endAnchor?.time ?? null,
+      startPrice: this.startAnchor?.price ?? null,
+      endPrice: this.endAnchor?.price ?? null,
       settings: { ...this.settings },
     };
   }
@@ -238,14 +165,11 @@ export class Diapson extends SeriesDrawingBase<DiapsonSettings, DiapsonHandleKey
     }
 
     const nextState = state as Partial<DiapsonState>;
-
     this.hidden = typeof nextState.hidden === 'boolean' ? nextState.hidden : this.hidden;
     this.mode = nextState.mode ?? this.mode;
     this.rangeMode = nextState.rangeMode ?? this.rangeMode;
-    this.startTime = 'startTime' in nextState ? (nextState.startTime ?? null) : this.startTime;
-    this.endTime = 'endTime' in nextState ? (nextState.endTime ?? null) : this.endTime;
-    this.startPrice = 'startPrice' in nextState ? (nextState.startPrice ?? null) : this.startPrice;
-    this.endPrice = 'endPrice' in nextState ? (nextState.endPrice ?? null) : this.endPrice;
+    this.restoreAnchor('start', nextState);
+    this.restoreAnchor('end', nextState);
 
     if ('settings' in nextState && nextState.settings) {
       this.settings = {
@@ -261,38 +185,6 @@ export class Diapson extends SeriesDrawingBase<DiapsonSettings, DiapsonHandleKey
     return getDiapsonSettingsTabs(this.settings);
   }
 
-  public updateAllViews(): void {
-    updateViews([
-      this.paneView,
-      this.timeAxisPaneView,
-      this.priceAxisPaneView,
-      this.leftTimeAxisView,
-      this.rightTimeAxisView,
-      this.topPriceAxisView,
-      this.bottomPriceAxisView,
-    ]);
-  }
-
-  public paneViews(): readonly IPrimitivePaneView[] {
-    return [this.paneView];
-  }
-
-  public timeAxisPaneViews(): readonly IPrimitivePaneView[] {
-    return [this.timeAxisPaneView];
-  }
-
-  public priceAxisPaneViews(): readonly IPrimitivePaneView[] {
-    return [this.priceAxisPaneView];
-  }
-
-  public timeAxisViews() {
-    return [this.leftTimeAxisView, this.rightTimeAxisView];
-  }
-
-  public priceAxisViews() {
-    return [this.topPriceAxisView, this.bottomPriceAxisView];
-  }
-
   public getRenderData(): DiapsonRenderData | null {
     if (this.hidden) {
       return null;
@@ -334,94 +226,12 @@ export class Diapson extends SeriesDrawingBase<DiapsonSettings, DiapsonHandleKey
     ];
   }
 
-  protected getTimeAxisSegments(): AxisSegment[] {
-    if (!this.isSelected() && !this.isCreationPending()) {
-      return [];
-    }
-
-    const bounds = this.getTimeBounds();
-
-    if (!bounds) {
-      return [];
-    }
-
-    const { colors } = getThemeStore();
-
-    return [
-      {
-        from: bounds.left,
-        to: bounds.right,
-        color: colors.axisMarkerAreaFill,
-      },
-    ];
-  }
-
-  protected getPriceAxisSegments(): AxisSegment[] {
-    if (!this.isSelected() && !this.isCreationPending()) {
-      return [];
-    }
-
-    const bounds = this.getPriceBounds();
-
-    if (!bounds) {
-      return [];
-    }
-
-    const { colors } = getThemeStore();
-
-    return [
-      {
-        from: bounds.top,
-        to: bounds.bottom,
-        color: colors.axisMarkerAreaFill,
-      },
-    ];
-  }
-
   protected getTimeAxisLabel(kind: string): AxisLabel | null {
-    if ((!this.isSelected() && !this.isCreationPending()) || (kind !== 'left' && kind !== 'right')) {
-      return null;
-    }
-
-    const labelKind = kind as TimeLabelKind;
-    const coordinate = this.getTimeCoordinate(labelKind);
-    const text = this.getTimeText(labelKind);
-
-    if (coordinate === null || !text) {
-      return null;
-    }
-
-    const { colors } = getThemeStore();
-
-    return {
-      coordinate,
-      text,
-      textColor: colors.chartPriceLineText,
-      backgroundColor: colors.axisMarkerLabelFill,
-    };
+    return this.getBoundsAxisLabel(kind, ['left', 'right'], (labelKind) => this.getTimeText(labelKind));
   }
 
   protected getPriceAxisLabel(kind: string): AxisLabel | null {
-    if ((!this.isSelected() && !this.isCreationPending()) || (kind !== 'top' && kind !== 'bottom')) {
-      return null;
-    }
-
-    const labelKind = kind as PriceLabelKind;
-    const coordinate = this.getPriceCoordinate(labelKind);
-    const text = this.getPriceText(labelKind);
-
-    if (coordinate === null || !text) {
-      return null;
-    }
-
-    const { colors } = getThemeStore();
-
-    return {
-      coordinate,
-      text,
-      textColor: colors.chartPriceLineText,
-      backgroundColor: colors.axisMarkerLabelFill,
-    };
+    return this.getBoundsAxisLabel(kind, ['top', 'bottom'], (labelKind) => this.getPriceText(labelKind));
   }
 
   protected getHoveredItem(x: number, y: number): PrimitiveHoveredItem | null {
@@ -464,98 +274,49 @@ export class Diapson extends SeriesDrawingBase<DiapsonSettings, DiapsonHandleKey
     };
   }
 
-  protected handleDoubleClick = (event: MouseEvent): void => {
-    if (this.hidden || this.mode !== 'ready') {
-      return;
-    }
-
-    const point = this.getEventPoint(event as PointerEvent);
-
-    if (!this.getDrawingHandleAtPoint(point) && !this.containsPoint(point)) {
-      return;
-    }
-
-    event.preventDefault();
-    event.stopPropagation();
-
-    this.openSettings?.();
-  };
-
-  protected handlePointerDown = (event: PointerEvent): void => {
-    if (this.hidden || event.button !== 0) {
-      return;
-    }
-
-    const point = this.getEventPoint(event);
-
-    if (this.mode === 'idle') {
-      event.preventDefault();
-      event.stopPropagation();
-
-      this.startDrawing(point);
-      return;
-    }
-
-    if (this.mode === 'drawing') {
-      event.preventDefault();
-      event.stopPropagation();
-
-      this.updateDrawing(point);
-      this.finishDrawing();
-      return;
-    }
-
-    if (this.mode !== 'ready') {
-      return;
-    }
-
-    if (!this.isSelected()) {
-      if (!this.containsPoint(point)) {
-        return;
-      }
-
-      event.preventDefault();
-      event.stopPropagation();
-
-      this.select();
-      return;
-    }
-
-    const dragTarget = this.getDragTarget(point);
+  protected getGeometry(): DiapsonGeometry | null {
+    const rawStartPoint = this.getPointFromAnchor(this.startAnchor);
+    const rawEndPoint = this.getPointFromAnchor(this.endAnchor);
 
-    if (!dragTarget) {
-      this.deselect();
-      return;
+    if (!rawStartPoint || !rawEndPoint) {
+      return null;
     }
 
-    event.preventDefault();
-    event.stopPropagation();
-
-    this.startDragging(point, event.pointerId, dragTarget);
-  };
-
-  protected handlePointerMove = (event: PointerEvent): void => {
-    const point = this.getEventPoint(event);
+    const { width, height } = this.getContainerSize();
 
-    if (this.mode === 'drawing') {
-      this.updateDrawing(point);
-      return;
-    }
+    const startPoint = {
+      x: clamp(Math.round(rawStartPoint.x), 0, width),
+      y: clamp(Math.round(rawStartPoint.y), 0, height),
+    };
 
-    if (this.mode !== 'dragging' || this.dragPointerId !== event.pointerId) {
-      return;
-    }
+    const endPoint = {
+      x: clamp(Math.round(rawEndPoint.x), 0, width),
+      y: clamp(Math.round(rawEndPoint.y), 0, height),
+    };
 
-    event.preventDefault();
+    const left = Math.min(startPoint.x, endPoint.x);
+    const right = Math.max(startPoint.x, endPoint.x);
+    const top = Math.min(startPoint.y, endPoint.y);
+    const bottom = Math.max(startPoint.y, endPoint.y);
 
-    if (this.activeDragTarget === 'body') {
-      this.moveWhole(point);
-      this.render();
-      return;
-    }
+    return {
+      left,
+      right,
+      top,
+      bottom,
+      width: right - left,
+      height: bottom - top,
+      startPoint,
+      endPoint,
+    };
+  }
 
-    this.resizeRectangle(point);
-    this.render();
+  protected handleDoubleClick = (event: MouseEvent): void => {
+    this.openSettingsOnDoubleClick(
+      event,
+      (point) => this.getDrawingHandleAtPoint(point) !== null || this.containsPoint(point),
+      () => this.openSettings?.(),
+    );
   };
 
   protected handlePointerUp = (event: PointerEvent): void => {
@@ -566,36 +327,29 @@ export class Diapson extends SeriesDrawingBase<DiapsonSettings, DiapsonHandleKey
     this.finishDragging();
   };
 
-  private startDrawing(point: Point): void {
+  protected startDrawing(point: Point): void {
     const anchor = this.createAnchor(this.clampPointToContainer(point));
 
     if (!anchor) {
       return;
     }
 
-    this.startTime = anchor.time;
-    this.endTime = anchor.time;
-    this.startPrice = anchor.price;
-    this.endPrice = anchor.price;
-
+    this.setAnchors(anchor, anchor);
     this.mode = 'drawing';
     this.render();
   }
 
-  private updateDrawing(point: Point): void {
-    const anchor = this.createAnchor(this.clampPointToContainer(point));
+  protected updateDrawing(point: Point): void {
+    const nextPoint = this.clampPointToContainer(point);
 
-    if (!anchor) {
+    if (!this.setAnchorFromPoint('end', nextPoint)) {
       return;
     }
 
-    this.endTime = anchor.time;
-    this.endPrice = anchor.price;
-
     this.render();
   }
 
-  private finishDrawing(): void {
+  protected finishDrawing(): void {
     const geometry = this.getGeometry();
 
     if (!geometry) {
@@ -615,24 +369,21 @@ export class Diapson extends SeriesDrawingBase<DiapsonSettings, DiapsonHandleKey
 
     this.mode = 'ready';
     this.resolveReady?.();
-
     this.render();
   }
 
-  private startDragging(point: Point, pointerId: number, dragTarget: Exclude<DiapsonHandle, null>): void {
+  protected startDragging(point: Point, pointerId: number, dragTarget: Exclude<DiapsonHandle, null>): void {
     this.mode = 'dragging';
     this.activeDragTarget = dragTarget;
     this.dragPointerId = pointerId;
     this.dragStartPoint = point;
     this.dragGeometrySnapshot = this.getGeometry();
-
     this.render();
   }
 
   private finishDragging(): void {
     this.mode = 'ready';
     this.resolveReady?.();
-
     this.clearInteractionState();
     this.render();
   }
@@ -648,19 +399,16 @@ export class Diapson extends SeriesDrawingBase<DiapsonSettings, DiapsonHandleKey
     this.hidden = false;
     this.deselect();
     this.mode = 'idle';
-    this.startTime = null;
-    this.endTime = null;
-    this.startPrice = null;
-    this.endPrice = null;
+    this.setAnchors(null, null);
     this.clearInteractionState();
     this.render();
   }
 
-  private getDragTarget(point: Point): Exclude<DiapsonHandle, null> | null {
+  protected getDragTarget(point: Point): Exclude<DiapsonHandle, null> | null {
     return this.getDrawingHandleAtPoint(point)?.id ?? (this.containsPoint(point) ? 'body' : null);
   }
 
-  private moveWhole(point: Point): void {
+  protected moveWhole(point: Point): void {
     const geometry = this.dragGeometrySnapshot;
     const { dragStartPoint } = this;
 
@@ -669,10 +417,8 @@ export class Diapson extends SeriesDrawingBase<DiapsonSettings, DiapsonHandleKey
     }
 
     const { width, height } = this.getContainerSize();
-
     const rawOffsetX = point.x - dragStartPoint.x;
     const rawOffsetY = point.y - dragStartPoint.y;
-
     const offsetX = clamp(rawOffsetX, -geometry.left, width - geometry.right);
     const offsetY = clamp(rawOffsetY, -geometry.top, height - geometry.bottom);
 
@@ -689,7 +435,7 @@ export class Diapson extends SeriesDrawingBase<DiapsonSettings, DiapsonHandleKey
     this.setAnchorsFromPoints(nextStartPoint, nextEndPoint);
   }
 
-  private resizeRectangle(point: Point): void {
+  protected resize(point: Point): void {
     const geometry = this.dragGeometrySnapshot;
 
     if (!geometry || !this.activeDragTarget || this.activeDragTarget === 'body') {
@@ -706,67 +452,6 @@ export class Diapson extends SeriesDrawingBase<DiapsonSettings, DiapsonHandleKey
     this.setAnchorsFromPoints(geometry.startPoint, nextPoint);
   }
 
-  private setAnchorsFromPoints(startPoint: Point, endPoint: Point): void {
-    const startAnchor = this.createAnchor(startPoint);
-    const endAnchor = this.createAnchor(endPoint);
-
-    if (!startAnchor || !endAnchor) {
-      return;
-    }
-
-    this.startTime = startAnchor.time;
-    this.startPrice = startAnchor.price;
-    this.endTime = endAnchor.time;
-    this.endPrice = endAnchor.price;
-  }
-
-  private createAnchor(point: Point): Anchor | null {
-    return getAnchorFromPoint(this.chart, this.series, point);
-  }
-
-  protected getGeometry(): DiapsonGeometry | null {
-    if (this.startTime === null || this.endTime === null || this.startPrice === null || this.endPrice === null) {
-      return null;
-    }
-
-    const startX = getXCoordinateFromTime(this.chart, this.startTime, this.series);
-    const endX = getXCoordinateFromTime(this.chart, this.endTime, this.series);
-    const startY = getYCoordinateFromPrice(this.series, this.startPrice);
-    const endY = getYCoordinateFromPrice(this.series, this.endPrice);
-
-    if (startX === null || endX === null || startY === null || endY === null) {
-      return null;
-    }
-
-    const { width, height } = this.getContainerSize();
-
-    const startPoint = {
-      x: clamp(Math.round(Number(startX)), 0, width),
-      y: clamp(Math.round(Number(startY)), 0, height),
-    };
-
-    const endPoint = {
-      x: clamp(Math.round(Number(endX)), 0, width),
-      y: clamp(Math.round(Number(endY)), 0, height),
-    };
-
-    const left = Math.round(Math.min(startPoint.x, endPoint.x));
-    const right = Math.round(Math.max(startPoint.x, endPoint.x));
-    const top = Math.round(Math.min(startPoint.y, endPoint.y));
-    const bottom = Math.round(Math.max(startPoint.y, endPoint.y));
-
-    return {
-      left,
-      right,
-      top,
-      bottom,
-      width: right - left,
-      height: bottom - top,
-      startPoint,
-      endPoint,
-    };
-  }
-
   private getLabelLines(): string[] {
     if (this.rangeMode === 'date') {
       const metrics = this.getDateMetrics();
@@ -821,7 +506,7 @@ export class Diapson extends SeriesDrawingBase<DiapsonSettings, DiapsonHandleKey
     const leftTime = this.getLeftTimeValue();
     const rightTime = this.getRightTimeValue();
 
-    if (!leftTime || !rightTime) {
+    if (leftTime === null || rightTime === null) {
       return null;
     }
 
@@ -831,7 +516,6 @@ export class Diapson extends SeriesDrawingBase<DiapsonSettings, DiapsonHandleKey
     const hours = Math.floor((durationSeconds % 86400) / 3600);
     const minutes = Math.floor((durationSeconds % 3600) / 60);
     const seconds = durationSeconds % 60;
-
     const elapsedParts: string[] = [];
 
     if (days > 0) {
@@ -860,26 +544,26 @@ export class Diapson extends SeriesDrawingBase<DiapsonSettings, DiapsonHandleKey
   }
 
   private getPriceMetrics(): PriceMetrics | null {
-    if (this.startPrice === null || this.endPrice === null) {
+    if (!this.startAnchor || !this.endAnchor) {
       return null;
     }
 
-    const delta = this.endPrice - this.startPrice;
+    const delta = this.endAnchor.price - this.startAnchor.price;
 
     return {
       delta,
-      percent: this.startPrice !== 0 ? (delta / Math.abs(this.startPrice)) * 100 : 0,
+      percent: this.startAnchor.price !== 0 ? (delta / Math.abs(this.startAnchor.price)) * 100 : 0,
       steps: delta / this.stepSize,
     };
   }
 
   private getBarsCount(): number {
-    if (this.startTime === null || this.endTime === null) {
+    if (!this.startAnchor || !this.endAnchor) {
       return 0;
     }
 
-    const startIndex = this.findIndexByTime(this.startTime);
-    const endIndex = this.findIndexByTime(this.endTime);
+    const startIndex = this.findIndexByTime(this.startAnchor.time);
+    const endIndex = this.findIndexByTime(this.endAnchor.time);
 
     if (startIndex < 0 || endIndex < 0) {
       return 0;
@@ -888,107 +572,10 @@ export class Diapson extends SeriesDrawingBase<DiapsonSettings, DiapsonHandleKey
     return Math.abs(endIndex - startIndex);
   }
 
-  private getVolumeInRange(): number {
-    if (this.startTime === null || this.endTime === null) {
-      return 0;
-    }
-
-    const data = this.series.data() ?? [];
-
-    if (!data.length) {
-      return 0;
-    }
-
-    const startIndex = this.findIndexByTime(this.startTime);
-    const endIndex = this.findIndexByTime(this.endTime);
-
-    if (startIndex < 0 || endIndex < 0) {
-      return 0;
-    }
-
-    const from = Math.min(startIndex, endIndex);
-    const to = Math.max(startIndex, endIndex);
-
-    let volume = 0;
-
-    for (let index = from; index <= to; index += 1) {
-      const item = data[index] as unknown as Record<string, unknown> | undefined;
-
-      if (!item) {
-        continue;
-      }
-
-      if (typeof item.volume === 'number') {
-        volume += item.volume;
-        continue;
-      }
-
-      const customValues = item.customValues as Record<string, unknown> | undefined;
-
-      if (customValues && typeof customValues.volume === 'number') {
-        volume += customValues.volume;
-      }
-    }
-
-    return volume;
-  }
-
-  private findIndexByTime(time: Time): number {
-    const data = this.series.data() ?? [];
-
-    return data.findIndex((item) => Number(item.time) === Number(time));
-  }
-
-  private getTimeBounds(): { left: number; right: number } | null {
-    const geometry = this.getGeometry();
-
-    if (!geometry) {
-      return null;
-    }
-
-    return {
-      left: geometry.left,
-      right: geometry.right,
-    };
-  }
-
-  private getPriceBounds(): { top: number; bottom: number } | null {
-    const geometry = this.getGeometry();
-
-    if (!geometry) {
-      return null;
-    }
-
-    return {
-      top: geometry.top,
-      bottom: geometry.bottom,
-    };
-  }
-
-  private getTimeCoordinate(kind: TimeLabelKind): number | null {
-    const geometry = this.getGeometry();
-
-    if (!geometry) {
-      return null;
-    }
-
-    return kind === 'left' ? geometry.left : geometry.right;
-  }
-
-  private getPriceCoordinate(kind: PriceLabelKind): number | null {
-    const geometry = this.getGeometry();
-
-    if (!geometry) {
-      return null;
-    }
-
-    return kind === 'top' ? geometry.top : geometry.bottom;
-  }
-
   private getTimeText(kind: TimeLabelKind): string {
     const time = kind === 'left' ? this.getLeftTimeValue() : this.getRightTimeValue();
 
-    if (!time) {
+    if (typeof time !== 'number') {
       return '';
     }
 
@@ -1003,81 +590,69 @@ export class Diapson extends SeriesDrawingBase<DiapsonSettings, DiapsonHandleKey
   private getPriceText(kind: PriceLabelKind): string {
     const price = kind === 'top' ? this.getTopPriceValue() : this.getBottomPriceValue();
 
-    if (price === null) {
-      return '';
-    }
-
-    return formatPrice(price) ?? '';
+    return price === null ? '' : (formatPrice(price) ?? '');
   }
 
   private getLeftTimeValue(): Time | null {
-    if (this.startTime === null || this.endTime === null) {
+    if (!this.startAnchor || !this.endAnchor) {
       return null;
     }
 
-    const startX = getXCoordinateFromTime(this.chart, this.startTime, this.series);
-    const endX = getXCoordinateFromTime(this.chart, this.endTime, this.series);
+    const geometry = this.getTwoPointGeometry();
 
-    if (startX === null || endX === null) {
-      return this.startTime;
+    if (!geometry) {
+      return this.startAnchor.time;
     }
 
-    return Number(startX) <= Number(endX) ? this.startTime : this.endTime;
+    return geometry.startPoint.x <= geometry.endPoint.x ? this.startAnchor.time : this.endAnchor.time;
   }
 
   private getRightTimeValue(): Time | null {
-    if (this.startTime === null || this.endTime === null) {
+    if (!this.startAnchor || !this.endAnchor) {
       return null;
     }
 
-    const startX = getXCoordinateFromTime(this.chart, this.startTime, this.series);
-    const endX = getXCoordinateFromTime(this.chart, this.endTime, this.series);
+    const geometry = this.getTwoPointGeometry();
 
-    if (startX === null || endX === null) {
-      return this.endTime;
+    if (!geometry) {
+      return this.endAnchor.time;
     }
 
-    return Number(startX) <= Number(endX) ? this.endTime : this.startTime;
+    return geometry.startPoint.x <= geometry.endPoint.x ? this.endAnchor.time : this.startAnchor.time;
   }
 
   private getTopPriceValue(): number | null {
-    if (this.startPrice === null || this.endPrice === null) {
+    if (!this.startAnchor || !this.endAnchor) {
       return null;
     }
 
-    const startY = getYCoordinateFromPrice(this.series, this.startPrice);
-    const endY = getYCoordinateFromPrice(this.series, this.endPrice);
+    const geometry = this.getTwoPointGeometry();
 
-    if (startY === null || endY === null) {
-      return Math.max(this.startPrice, this.endPrice);
+    if (!geometry) {
+      return Math.max(this.startAnchor.price, this.endAnchor.price);
     }
 
-    return Number(startY) <= Number(endY) ? this.startPrice : this.endPrice;
+    return geometry.startPoint.y <= geometry.endPoint.y ? this.startAnchor.price : this.endAnchor.price;
   }
 
   private getBottomPriceValue(): number | null {
-    if (this.startPrice === null || this.endPrice === null) {
+    if (!this.startAnchor || !this.endAnchor) {
       return null;
     }
 
-    const startY = getYCoordinateFromPrice(this.series, this.startPrice);
-    const endY = getYCoordinateFromPrice(this.series, this.endPrice);
+    const geometry = this.getTwoPointGeometry();
 
-    if (startY === null || endY === null) {
-      return Math.min(this.startPrice, this.endPrice);
+    if (!geometry) {
+      return Math.min(this.startAnchor.price, this.endAnchor.price);
     }
 
-    return Number(startY) <= Number(endY) ? this.endPrice : this.startPrice;
+    return geometry.startPoint.y <= geometry.endPoint.y ? this.endAnchor.price : this.startAnchor.price;
   }
 
-  private containsPoint(point: Point): boolean {
+  protected containsPoint(point: Point): boolean {
     const geometry = this.getGeometry();
 
-    if (!geometry) {
-      return false;
-    }
-
-    return isPointInBounds(point, geometry, BODY_HIT_TOLERANCE);
+    return geometry !== null && isPointInBounds(point, geometry, BODY_HIT_TOLERANCE);
   }
 
   private getCursorStyle(handle: Exclude<DiapsonHandle, null>): PrimitiveHoveredItem['cursorStyle'] {
@@ -1097,12 +672,4 @@ export class Diapson extends SeriesDrawingBase<DiapsonSettings, DiapsonHandleKey
 
     return sameDirection ? 'nwse-resize' : 'nesw-resize';
   }
-
-  private getContainerSize(): { width: number; height: number } {
-    return getElementContainerSize(this.container);
-  }
-
-  private clampPointToContainer(point: Point): Point {
-    return clampPointToContainerInElement(point, this.container);
-  }
 }
diff --git a/src/core/Drawings/fibonacciRetracement/fibonacciRetracement.ts b/src/core/Drawings/fibonacciRetracement/fibonacciRetracement.ts
index 49278926a26f4ff43fc812452ed81713d55b96ae..a288a14cad591c7118567b6e2718e3ac59e7ea6b 100644
--- a/src/core/Drawings/fibonacciRetracement/fibonacciRetracement.ts
+++ b/src/core/Drawings/fibonacciRetracement/fibonacciRetracement.ts
@@ -1,28 +1,10 @@
-import { clamp } from 'lodash-es';
+import { PrimitiveHoveredItem, Time, UTCTimestamp } from 'lightweight-charts';
 
-import {
-  CustomPriceAxisPaneView,
-  CustomPriceAxisView,
-  CustomTimeAxisPaneView,
-  CustomTimeAxisView,
-} from '@core/Drawings/axis';
-import {
-  clampPointToContainer as clampPointToContainerInElement,
-  getAnchorFromPoint,
-  getContainerSize as getElementContainerSize,
-  getPriceDelta as getPriceDeltaFromCoordinates,
-  getXCoordinateFromTime,
-  getYCoordinateFromPrice,
-  isPointInBounds,
-  shiftTimeByPixels,
-} from '@core/Drawings/helpers';
-import { updateViews } from '@core/Drawings/utils';
-import { SeriesDrawingBase } from '@src/core/Drawings/SeriesDrawingBase';
-
-import { getThemeStore } from '@src/theme';
-import { Defaults } from '@src/types/defaults';
-import { formatPrice } from '@src/utils';
-import { formatDate } from '@src/utils/formatter';
+import { getYCoordinateFromPrice, isPointInBounds } from '@core/Drawings/helpers';
+import { AreaDrawingBase, BaseDrawingParams, ISeriesDrawing } from '@src/core/Drawings/base';
+
+import { type ChartOptionsModel, Defaults, type SettingsTab, type SettingsValues } from '@src/types';
+import { formatDate, formatPrice } from '@src/utils';
 
 import { FibonacciRetracementPaneView } from './paneView';
 import {
@@ -37,10 +19,7 @@ import {
 } from './settings';
 
 import type { DrawingHandle } from '@core/Drawings/handles';
-import type { AxisLabel, AxisSegment, Bounds, Point } from '@core/Drawings/types';
-import type { BaseDrawingParams, ISeriesDrawing } from '@src/core/Drawings/SeriesDrawingBase';
-import type { ChartOptionsModel, SettingsTab, SettingsValues } from '@src/types';
-import type { IPrimitivePaneView, PrimitiveHoveredItem, Time, UTCTimestamp } from 'lightweight-charts';
+import type { Anchor, AxisLabel, Bounds, Point } from '@core/Drawings/types';
 
 type FibonacciRetracementMode = 'idle' | 'drawing' | 'ready' | 'dragging';
 type FibonacciRetracementHandle = 'body' | 'start' | 'end' | null;
@@ -60,6 +39,11 @@ interface FibonacciRetracementState {
   settings: FibonacciRetracementSettings;
 }
 
+interface FibonacciDragSnapshot {
+  startAnchor: Anchor | null;
+  endAnchor: Anchor | null;
+}
+
 export interface FibonacciRetracementLevelRenderData {
   id: string;
   value: number;
@@ -102,7 +86,7 @@ const MIN_DISTANCE = 6;
 const PERCENT_DIVIDER = 100;
 
 export class FibonacciRetracement
-  extends SeriesDrawingBase<FibonacciRetracementSettings, FibonacciRetracementHandleKey>
+  extends AreaDrawingBase<FibonacciRetracementSettings, FibonacciRetracementHandleKey>
   implements ISeriesDrawing
 {
   private removeSelf?: () => void;
@@ -111,15 +95,10 @@ export class FibonacciRetracement
   protected settings: FibonacciRetracementSettings = createDefaultSettings();
   protected mode: FibonacciRetracementMode = 'idle';
 
-  private startTime: Time | null = null;
-  private endTime: Time | null = null;
-  private startPrice: number | null = null;
-  private endPrice: number | null = null;
-
-  private activeDragTarget: FibonacciRetracementHandle = null;
-  private dragPointerId: number | null = null;
+  protected activeDragTarget: FibonacciRetracementHandle = null;
+  protected dragPointerId: number | null = null;
   private dragStartPoint: Point | null = null;
-  private dragStateSnapshot: FibonacciRetracementState | null = null;
+  private dragAnchorSnapshot: FibonacciDragSnapshot | null = null;
   private dragGeometrySnapshot: FibonacciRetracementGeometry | null = null;
 
   private displayFormat: ChartOptionsModel = {
@@ -128,14 +107,6 @@ export class FibonacciRetracement
     showTime: Defaults.showTime,
   };
 
-  private readonly paneView: FibonacciRetracementPaneView;
-  private readonly timeAxisPaneView: CustomTimeAxisPaneView;
-  private readonly priceAxisPaneView: CustomPriceAxisPaneView;
-  private readonly startTimeAxisView: CustomTimeAxisView;
-  private readonly endTimeAxisView: CustomTimeAxisView;
-  private readonly topPriceAxisView: CustomPriceAxisView;
-  private readonly bottomPriceAxisView: CustomPriceAxisView;
-
   constructor({
     chart,
     series,
@@ -150,51 +121,16 @@ export class FibonacciRetracement
 
     this.removeSelf = removeSelf;
     this.openSettings = openSettings;
+    this.initializeViews(new FibonacciRetracementPaneView(this), ['start', 'end'], ['top', 'bottom']);
 
-    this.paneView = new FibonacciRetracementPaneView(this);
-
-    this.timeAxisPaneView = new CustomTimeAxisPaneView({
-      getAxisSegments: () => this.getTimeAxisSegments(),
+    this.subscribeFormat(formatObservable, (format) => {
+      this.displayFormat = format;
     });
 
-    this.priceAxisPaneView = new CustomPriceAxisPaneView({
-      getAxisSegments: () => this.getPriceAxisSegments(),
-    });
-
-    this.startTimeAxisView = new CustomTimeAxisView({
-      getAxisLabel: (kind) => this.getTimeAxisLabel(kind),
-      labelKind: 'start',
-    });
-
-    this.endTimeAxisView = new CustomTimeAxisView({
-      getAxisLabel: (kind) => this.getTimeAxisLabel(kind),
-      labelKind: 'end',
-    });
-
-    this.topPriceAxisView = new CustomPriceAxisView({
-      getAxisLabel: (kind) => this.getPriceAxisLabel(kind),
-      labelKind: 'top',
-    });
-
-    this.bottomPriceAxisView = new CustomPriceAxisView({
-      getAxisLabel: (kind) => this.getPriceAxisLabel(kind),
-      labelKind: 'bottom',
-    });
-
-    if (formatObservable) {
-      this.subscriptions.add(
-        formatObservable.subscribe((format) => {
-          this.displayFormat = format;
-          this.render();
-        }),
-      );
-    }
-
     this.series.attachPrimitive(this);
 
-    if (initialEvent && initialEvent.sourceEvent) {
-      const point = this.getEventPoint(initialEvent.sourceEvent);
-      this.startDrawing(point);
+    if (initialEvent?.sourceEvent) {
+      this.startDrawing(this.getEventPoint(initialEvent.sourceEvent));
     }
   }
 
@@ -206,10 +142,10 @@ export class FibonacciRetracement
     return {
       hidden: this.hidden,
       mode: this.mode,
-      startTime: this.startTime,
-      endTime: this.endTime,
-      startPrice: this.startPrice,
-      endPrice: this.endPrice,
+      startTime: this.startAnchor?.time ?? null,
+      endTime: this.endAnchor?.time ?? null,
+      startPrice: this.startAnchor?.price ?? null,
+      endPrice: this.endAnchor?.price ?? null,
       settings: cloneFibonacciRetracementSettings(this.settings),
     };
   }
@@ -220,14 +156,10 @@ export class FibonacciRetracement
     }
 
     const next = state as Partial<FibonacciRetracementState>;
-
     this.hidden = next.hidden ?? this.hidden;
     this.mode = next.mode ?? this.mode;
-
-    this.startTime = next.startTime ?? this.startTime;
-    this.endTime = next.endTime ?? this.endTime;
-    this.startPrice = next.startPrice ?? this.startPrice;
-    this.endPrice = next.endPrice ?? this.endPrice;
+    this.restoreStartAnchor(next);
+    this.restoreEndAnchor(next);
 
     if (next.settings) {
       this.settings = mergeFibonacciRetracementSettings(createDefaultSettings(), next.settings);
@@ -249,38 +181,6 @@ export class FibonacciRetracement
     this.render();
   }
 
-  public updateAllViews(): void {
-    updateViews([
-      this.paneView,
-      this.timeAxisPaneView,
-      this.priceAxisPaneView,
-      this.startTimeAxisView,
-      this.endTimeAxisView,
-      this.topPriceAxisView,
-      this.bottomPriceAxisView,
-    ]);
-  }
-
-  public paneViews(): readonly IPrimitivePaneView[] {
-    return [this.paneView];
-  }
-
-  public timeAxisPaneViews(): readonly IPrimitivePaneView[] {
-    return [this.timeAxisPaneView];
-  }
-
-  public priceAxisPaneViews(): readonly IPrimitivePaneView[] {
-    return [this.priceAxisPaneView];
-  }
-
-  public timeAxisViews() {
-    return [this.startTimeAxisView, this.endTimeAxisView];
-  }
-
-  public priceAxisViews() {
-    return [this.topPriceAxisView, this.bottomPriceAxisView];
-  }
-
   public getRenderData(): FibonacciRetracementRenderData | null {
     const geometry = this.hidden ? null : this.getGeometry();
 
@@ -351,177 +251,24 @@ export class FibonacciRetracement
     };
   }
 
-  protected getTimeAxisSegments(): AxisSegment[] {
-    const bounds = this.isSelected() || this.isCreationPending() ? this.getTimeBounds() : null;
-
-    if (!bounds) {
-      return [];
-    }
-
-    const { colors } = getThemeStore();
-
-    return [
-      {
-        from: bounds.left,
-        to: bounds.right,
-        color: colors.axisMarkerAreaFill,
-      },
-    ];
-  }
-
-  protected getPriceAxisSegments(): AxisSegment[] {
-    const bounds = this.isSelected() || this.isCreationPending() ? this.getPriceBounds() : null;
-
-    if (!bounds) {
-      return [];
-    }
-
-    const { colors } = getThemeStore();
-
-    return [
-      {
-        from: bounds.top,
-        to: bounds.bottom,
-        color: colors.axisMarkerAreaFill,
-      },
-    ];
-  }
-
   protected getTimeAxisLabel(kind: string): AxisLabel | null {
-    if ((!this.isSelected() && !this.isCreationPending()) || (kind !== 'start' && kind !== 'end')) {
-      return null;
-    }
-
-    const coordinate = this.getTimeCoordinate(kind);
-    const text = this.getTimeText(kind);
-
-    if (coordinate === null || !text) {
-      return null;
-    }
-
-    const { colors } = getThemeStore();
-
-    return {
-      coordinate,
-      text,
-      textColor: colors.chartPriceLineText,
-      backgroundColor: colors.axisMarkerLabelFill,
-    };
+    return this.getInteractiveAxisLabel(kind, ['start', 'end'], (labelKind) => {
+      return this.createAxisLabel(this.getAnchorTimeCoordinate(labelKind), this.getTimeText(labelKind));
+    });
   }
 
   protected getPriceAxisLabel(kind: string): AxisLabel | null {
-    if ((!this.isSelected() && !this.isCreationPending()) || (kind !== 'top' && kind !== 'bottom')) {
-      return null;
-    }
-
-    const coordinate = this.getPriceCoordinate(kind);
-    const text = this.getPriceText(kind);
-
-    if (coordinate === null || !text) {
-      return null;
-    }
-
-    const { colors } = getThemeStore();
-
-    return {
-      coordinate,
-      text,
-      textColor: colors.chartPriceLineText,
-      backgroundColor: colors.axisMarkerLabelFill,
-    };
+    return this.getInteractiveAxisLabel(kind, ['top', 'bottom'], (labelKind) => {
+      return this.createAxisLabel(this.getPriceCoordinate(labelKind), this.getPriceText(labelKind));
+    });
   }
 
   protected handleDoubleClick = (event: MouseEvent): void => {
-    if (this.hidden || this.mode !== 'ready') {
-      return;
-    }
-
-    const point = this.getEventPoint(event as PointerEvent);
-
-    if (!this.containsPoint(point) && !this.getDrawingHandleAtPoint(point)) {
-      return;
-    }
-
-    event.preventDefault();
-    event.stopPropagation();
-
-    this.openSettings?.();
-  };
-
-  protected handlePointerDown = (event: PointerEvent): void => {
-    if (this.hidden || event.button !== 0) {
-      return;
-    }
-
-    const point = this.getEventPoint(event);
-
-    if (this.mode === 'idle') {
-      event.preventDefault();
-      event.stopPropagation();
-
-      this.startDrawing(point);
-      return;
-    }
-
-    if (this.mode === 'drawing') {
-      event.preventDefault();
-      event.stopPropagation();
-
-      this.updateDrawing(point);
-      this.finishDrawing();
-      return;
-    }
-
-    if (this.mode !== 'ready') {
-      return;
-    }
-
-    if (!this.isSelected()) {
-      if (!this.containsPoint(point)) {
-        return;
-      }
-
-      event.preventDefault();
-      event.stopPropagation();
-
-      this.select();
-      return;
-    }
-
-    const dragTarget = this.getDragTarget(point);
-
-    if (!dragTarget) {
-      this.deselect();
-      return;
-    }
-
-    event.preventDefault();
-    event.stopPropagation();
-
-    this.startDragging(point, event.pointerId, dragTarget);
-  };
-
-  protected handlePointerMove = (event: PointerEvent): void => {
-    const point = this.getEventPoint(event);
-
-    if (this.mode === 'drawing') {
-      this.updateDrawing(point);
-      return;
-    }
-
-    if (this.mode !== 'dragging' || this.dragPointerId !== event.pointerId) {
-      return;
-    }
-
-    event.preventDefault();
-
-    if (this.activeDragTarget === 'body') {
-      this.moveWhole(point);
-    } else {
-      this.resize(point);
-    }
-
-    this.render();
+    this.openSettingsOnDoubleClick(
+      event,
+      (point) => Boolean(this.containsPoint(point) || this.getDrawingHandleAtPoint(point)),
+      () => this.openSettings?.(),
+    );
   };
 
   protected handlePointerUp = (event: PointerEvent): void => {
@@ -531,41 +278,36 @@ export class FibonacciRetracement
 
     this.mode = 'ready';
     this.resolveReady?.();
-
     this.clearInteractionState();
     this.render();
   };
 
-  private startDrawing(point: Point): void {
-    const anchor = this.createAnchor(point);
-
-    if (!anchor) {
-      return;
-    }
-
-    this.startTime = anchor.time;
-    this.endTime = anchor.time;
-    this.startPrice = anchor.price;
-    this.endPrice = anchor.price;
-    this.mode = 'drawing';
-
-    this.render();
-  }
-
-  private updateDrawing(point: Point): void {
-    const anchor = this.createAnchor(clampPointToContainerInElement(point, this.container));
+  protected getGeometry(): FibonacciRetracementGeometry | null {
+    const baseGeometry = this.getTwoPointGeometry();
 
-    if (!anchor) {
-      return;
+    if (!baseGeometry) {
+      return null;
     }
 
-    this.endTime = anchor.time;
-    this.endPrice = anchor.price;
+    const levels = this.getLevels();
+    const top = Math.min(baseGeometry.startPoint.y, baseGeometry.endPoint.y, ...levels.map((level) => level.y));
+    const bottom = Math.max(baseGeometry.startPoint.y, baseGeometry.endPoint.y, ...levels.map((level) => level.y));
 
-    this.render();
+    return {
+      startPoint: baseGeometry.startPoint,
+      endPoint: baseGeometry.endPoint,
+      left: baseGeometry.left,
+      right: baseGeometry.right,
+      top,
+      bottom,
+      width: baseGeometry.right - baseGeometry.left,
+      height: bottom - top,
+      levels,
+      areas: this.getAreas(levels),
+    };
   }
 
-  private finishDrawing(): void {
+  protected finishDrawing(): void {
     const geometry = this.getGeometry();
 
     if (
@@ -584,19 +326,23 @@ export class FibonacciRetracement
 
     this.mode = 'ready';
     this.resolveReady?.();
-
     this.render();
   }
 
-  private startDragging(point: Point, pointerId: number, dragTarget: Exclude<FibonacciRetracementHandle, null>): void {
+  protected startDragging(
+    point: Point,
+    pointerId: number,
+    dragTarget: Exclude<FibonacciRetracementHandle, null>,
+  ): void {
     this.mode = 'dragging';
-
     this.activeDragTarget = dragTarget;
     this.dragPointerId = pointerId;
     this.dragStartPoint = point;
-    this.dragStateSnapshot = this.getState();
+    this.dragAnchorSnapshot = {
+      startAnchor: this.startAnchor,
+      endAnchor: this.endAnchor,
+    };
     this.dragGeometrySnapshot = this.getGeometry();
-
     this.render();
   }
 
@@ -604,148 +350,43 @@ export class FibonacciRetracement
     this.activeDragTarget = null;
     this.dragPointerId = null;
     this.dragStartPoint = null;
-    this.dragStateSnapshot = null;
+    this.dragAnchorSnapshot = null;
     this.dragGeometrySnapshot = null;
   }
 
   private resetToIdle(): void {
     this.hidden = false;
     this.mode = 'idle';
-
-    this.startTime = null;
-    this.endTime = null;
-    this.startPrice = null;
-    this.endPrice = null;
-
+    this.setAnchors(null, null);
     this.clearInteractionState();
     this.render();
   }
 
-  private getDragTarget(point: Point): Exclude<FibonacciRetracementHandle, null> | null {
+  protected getDragTarget(point: Point): Exclude<FibonacciRetracementHandle, null> | null {
     return this.getDrawingHandleAtPoint(point)?.id ?? (this.containsPoint(point) ? 'body' : null);
   }
 
-  private moveWhole(point: Point): void {
-    const snapshot = this.dragStateSnapshot;
+  protected moveWhole(point: Point): void {
+    const snapshot = this.dragAnchorSnapshot;
     const geometry = this.dragGeometrySnapshot;
 
-    if (!snapshot || !geometry || !this.dragStartPoint) {
+    if (!snapshot || !geometry) {
       return;
     }
 
-    if (
-      snapshot.startTime === null ||
-      snapshot.endTime === null ||
-      snapshot.startPrice === null ||
-      snapshot.endPrice === null
-    ) {
-      return;
-    }
-
-    const containerSize = getElementContainerSize(this.container);
-
-    const rawOffsetX = point.x - this.dragStartPoint.x;
-    const rawOffsetY = point.y - this.dragStartPoint.y;
-
-    const minOffsetX = -geometry.left;
-    const maxOffsetX = containerSize.width - geometry.right;
-    const clampedOffsetX = clamp(rawOffsetX, minOffsetX, maxOffsetX);
-
-    const minOffsetY = -geometry.top;
-    const maxOffsetY = containerSize.height - geometry.bottom;
-    const clampedOffsetY = clamp(rawOffsetY, minOffsetY, maxOffsetY);
-
-    const nextStartTime = shiftTimeByPixels(this.chart, snapshot.startTime, clampedOffsetX, this.series);
-    const nextEndTime = shiftTimeByPixels(this.chart, snapshot.endTime, clampedOffsetX, this.series);
-
-    if (nextStartTime === null || nextEndTime === null) {
-      return;
-    }
-
-    const priceOffset = getPriceDeltaFromCoordinates(
-      this.series,
-      this.dragStartPoint.y,
-      this.dragStartPoint.y + clampedOffsetY,
-    );
-
-    this.startTime = nextStartTime;
-    this.endTime = nextEndTime;
-    this.startPrice = snapshot.startPrice + priceOffset;
-    this.endPrice = snapshot.endPrice + priceOffset;
+    this.moveAnchorsWithinContainer(snapshot.startAnchor, snapshot.endAnchor, this.dragStartPoint, point, geometry);
   }
 
-  private resize(point: Point): void {
+  protected resize(point: Point): void {
     if (!this.activeDragTarget || this.activeDragTarget === 'body') {
       return;
     }
 
-    const anchor = this.createAnchor(clampPointToContainerInElement(point, this.container));
-
-    if (!anchor) {
-      return;
-    }
-
-    if (this.activeDragTarget === 'start') {
-      this.startTime = anchor.time;
-      this.startPrice = anchor.price;
-      return;
-    }
-
-    this.endTime = anchor.time;
-    this.endPrice = anchor.price;
-  }
-
-  private createAnchor(point: Point): { time: Time; price: number } | null {
-    return getAnchorFromPoint(this.chart, this.series, point);
-  }
-
-  protected getGeometry(): FibonacciRetracementGeometry | null {
-    if (this.startTime === null || this.endTime === null || this.startPrice === null || this.endPrice === null) {
-      return null;
-    }
-
-    const startX = getXCoordinateFromTime(this.chart, this.startTime, this.series);
-    const endX = getXCoordinateFromTime(this.chart, this.endTime, this.series);
-    const startY = getYCoordinateFromPrice(this.series, this.startPrice);
-    const endY = getYCoordinateFromPrice(this.series, this.endPrice);
-
-    if (startX === null || endX === null || startY === null || endY === null) {
-      return null;
-    }
-
-    const startPoint = {
-      x: Math.round(Number(startX)),
-      y: Math.round(Number(startY)),
-    };
-
-    const endPoint = {
-      x: Math.round(Number(endX)),
-      y: Math.round(Number(endY)),
-    };
-
-    const left = Math.round(Math.min(startPoint.x, endPoint.x));
-    const right = Math.round(Math.max(startPoint.x, endPoint.x));
-    const levels = this.getLevels();
-
-    const top = Math.min(startPoint.y, endPoint.y, ...levels.map((level) => level.y));
-    const bottom = Math.max(startPoint.y, endPoint.y, ...levels.map((level) => level.y));
-
-    return {
-      startPoint,
-      endPoint,
-      left,
-      right,
-      top,
-      bottom,
-      width: right - left,
-      height: bottom - top,
-      levels,
-      areas: this.getAreas(levels),
-    };
+    this.setAnchorFromPoint(this.activeDragTarget, this.clampPointToContainer(point));
   }
 
   private getLevels(): FibonacciRetracementLevelRenderData[] {
-    if (this.startPrice === null || this.endPrice === null) {
+    if (!this.startAnchor || !this.endAnchor) {
       return [];
     }
 
@@ -761,7 +402,7 @@ export class FibonacciRetracement
         id: level.id,
         value: level.value,
         price,
-        y: Math.round(Number(y)),
+        y: Number(y),
         color: level.color,
         text: this.getLevelText(level.value, price),
       });
@@ -771,8 +412,8 @@ export class FibonacciRetracement
   }
 
   private getLevelPrice(value: number): number {
-    const startPrice = this.startPrice ?? 0;
-    const endPrice = this.endPrice ?? 0;
+    const startPrice = this.startAnchor?.price ?? 0;
+    const endPrice = this.endAnchor?.price ?? 0;
 
     return this.settings.reverse
       ? startPrice + (endPrice - startPrice) * value
@@ -811,28 +452,6 @@ export class FibonacciRetracement
     return parts.join(' ');
   }
 
-  private getTimeBounds(): { left: number; right: number } | null {
-    const geometry = this.getGeometry();
-
-    return geometry ? { left: geometry.left, right: geometry.right } : null;
-  }
-
-  private getPriceBounds(): { top: number; bottom: number } | null {
-    const geometry = this.getGeometry();
-
-    return geometry ? { top: geometry.top, bottom: geometry.bottom } : null;
-  }
-
-  private getTimeCoordinate(kind: TimeLabelKind): number | null {
-    const geometry = this.getGeometry();
-
-    if (!geometry) {
-      return null;
-    }
-
-    return kind === 'start' ? geometry.startPoint.x : geometry.endPoint.x;
-  }
-
   private getPriceCoordinate(kind: PriceLabelKind): number | null {
     const geometry = this.getGeometry();
 
@@ -844,14 +463,14 @@ export class FibonacciRetracement
   }
 
   private getTimeText(kind: TimeLabelKind): string {
-    const time = kind === 'start' ? this.startTime : this.endTime;
+    const anchor = this.getAnchor(kind);
 
-    if (typeof time !== 'number') {
+    if (!anchor || typeof anchor.time !== 'number') {
       return '';
     }
 
     return formatDate(
-      time as UTCTimestamp,
+      anchor.time as UTCTimestamp,
       this.displayFormat.dateFormat,
       this.displayFormat.timeFormat,
       this.displayFormat.showTime,
@@ -867,7 +486,7 @@ export class FibonacciRetracement
   private getPriceValueForLabel(kind: PriceLabelKind): number | null {
     const geometry = this.getGeometry();
 
-    if (!geometry) {
+    if (!geometry || !this.startAnchor || !this.endAnchor) {
       return null;
     }
 
@@ -879,11 +498,11 @@ export class FibonacciRetracement
     }
 
     return kind === 'top'
-      ? Math.max(this.startPrice ?? 0, this.endPrice ?? 0)
-      : Math.min(this.startPrice ?? 0, this.endPrice ?? 0);
+      ? Math.max(this.startAnchor.price, this.endAnchor.price)
+      : Math.min(this.startAnchor.price, this.endAnchor.price);
   }
 
-  private containsPoint(point: Point): boolean {
+  protected containsPoint(point: Point): boolean {
     const geometry = this.getGeometry();
 
     if (!geometry) {
@@ -905,4 +524,22 @@ export class FibonacciRetracement
 
     return xInRange && geometry.levels.some((level) => Math.abs(point.y - level.y) <= LINE_HIT_TOLERANCE);
   }
+
+  private restoreStartAnchor(state: Partial<FibonacciRetracementState>): void {
+    const time = state.startTime ?? this.startAnchor?.time ?? null;
+    const price = state.startPrice ?? this.startAnchor?.price ?? null;
+
+    if (time !== null && price !== null) {
+      this.startAnchor = { time, price };
+    }
+  }
+
+  private restoreEndAnchor(state: Partial<FibonacciRetracementState>): void {
+    const time = state.endTime ?? this.endAnchor?.time ?? null;
+    const price = state.endPrice ?? this.endAnchor?.price ?? null;
+
+    if (time !== null && price !== null) {
+      this.endAnchor = { time, price };
+    }
+  }
 }
diff --git a/src/core/Drawings/helpers.ts b/src/core/Drawings/helpers.ts
index c536f3f4f17c35fe264b456810a666e930b6a346..5c8c57f5e24192b4693023c3b4bfcfc9e03040f8 100644
--- a/src/core/Drawings/helpers.ts
+++ b/src/core/Drawings/helpers.ts
@@ -2,15 +2,15 @@ import { clamp } from 'lodash-es';
 
 import type { Anchor, Bounds, ContainerSize, Point, SeriesApi } from './types';
 
-import type { Coordinate, IChartApi, Logical, Time, TouchMouseEventData } from 'lightweight-charts';
+import type { Coordinate, IChartApi, Time, TouchMouseEventData } from 'lightweight-charts';
 
 interface SeriesTimeItem {
   time: Time;
 }
 
-interface TimePoint {
-  time: number;
-  logical: number;
+interface SeriesTimePoint {
+  time: Time;
+  timestamp: number;
 }
 
 export function getPriceFromYCoordinate(series: SeriesApi, yCoordinate: number): number | null {
@@ -36,19 +36,7 @@ export function getXCoordinateFromTime(chart: IChartApi, time: Time, series?: Se
     return null;
   }
 
-  const logical = getNearestLogicalFromTime(series, time);
-
-  if (logical === null) {
-    return null;
-  }
-
-  const projectedCoordinate = chart.timeScale().logicalToCoordinate(logical as Logical);
-
-  if (!isValidCoordinate(projectedCoordinate)) {
-    return null;
-  }
-
-  return projectedCoordinate;
+  return getProjectedXCoordinate(chart, series, time);
 }
 
 export function getContainerSize(container: HTMLElement): ContainerSize {
@@ -124,6 +112,7 @@ export function shiftTimeByPixels(chart: IChartApi, time: Time, offsetX: number,
 
 export function getPriceDelta(series: SeriesApi, fromY: number, toY: number): number {
   const fromPrice = getPriceFromYCoordinate(series, fromY);
+
   const toPrice = getPriceFromYCoordinate(series, toY);
 
   if (fromPrice === null || toPrice === null) {
@@ -136,7 +125,10 @@ export function getPriceDelta(series: SeriesApi, fromY: number, toY: number): nu
 export function getPriceRangeInContainer(
   series: SeriesApi,
   container: HTMLElement,
-): { min: number; max: number } | null {
+): {
+  min: number;
+  max: number;
+} | null {
   const { height } = getContainerSize(container);
 
   if (!height) {
@@ -144,6 +136,7 @@ export function getPriceRangeInContainer(
   }
 
   const topPrice = getPriceFromYCoordinate(series, 0);
+
   const bottomPrice = getPriceFromYCoordinate(series, height);
 
   if (topPrice === null || bottomPrice === null) {
@@ -158,6 +151,7 @@ export function getPriceRangeInContainer(
 
 export function getAnchorFromPoint(chart: IChartApi, series: SeriesApi, point: Point): Anchor | null {
   const time = getTimeFromXCoordinate(chart, point.x);
+
   const price = getPriceFromYCoordinate(series, point.y);
 
   if (time === null || price === null) {
@@ -170,7 +164,7 @@ export function getAnchorFromPoint(chart: IChartApi, series: SeriesApi, point: P
   };
 }
 
-function getNearestLogicalFromTime(series: SeriesApi, time: Time): number | null {
+function getProjectedXCoordinate(chart: IChartApi, series: SeriesApi, time: Time): Coordinate | null {
   const targetTime = getNumericTime(time);
 
   if (targetTime === null) {
@@ -183,14 +177,18 @@ function getNearestLogicalFromTime(series: SeriesApi, time: Time): number | null
     return null;
   }
 
+  if (points.length === 1) {
+    return getCoordinateForTime(chart, points[0].time);
+  }
+
   const lastIndex = points.length - 1;
 
-  if (targetTime <= points[0].time) {
-    return points[0].logical;
+  if (targetTime <= points[0].timestamp) {
+    return interpolateCoordinate(chart, targetTime, points[0], points[1]);
   }
 
-  if (targetTime >= points[lastIndex].time) {
-    return points[lastIndex].logical;
+  if (targetTime >= points[lastIndex].timestamp) {
+    return interpolateCoordinate(chart, targetTime, points[lastIndex - 1], points[lastIndex]);
   }
 
   let left = 0;
@@ -198,63 +196,73 @@ function getNearestLogicalFromTime(series: SeriesApi, time: Time): number | null
 
   while (left <= right) {
     const middleIndex = Math.floor((left + right) / 2);
-    const middleTime = points[middleIndex].time;
 
-    if (middleTime === targetTime) {
-      return points[middleIndex].logical;
+    const middlePoint = points[middleIndex];
+
+    if (middlePoint.timestamp === targetTime) {
+      return getCoordinateForTime(chart, middlePoint.time);
     }
 
-    if (middleTime < targetTime) {
+    if (middlePoint.timestamp < targetTime) {
       left = middleIndex + 1;
     } else {
       right = middleIndex - 1;
     }
   }
 
-  // Если точного времени нет на текущем таймфрейме, left и right становятся соседними свечами вокруг targetTime
-  // Для отображения дровинга берём ближайшую существующую свечу, но исходный state дровинга не меняем
-  const previousPoint = points[right] ?? null;
-  const nextPoint = points[left] ?? null;
-
-  return getNearestLogicalByTime(targetTime, previousPoint, nextPoint);
+  return interpolateCoordinate(chart, targetTime, points[right], points[left]);
 }
 
-function getNearestLogicalByTime(
+function interpolateCoordinate(
+  chart: IChartApi,
   targetTime: number,
-  previousPoint: TimePoint | null,
-  nextPoint: TimePoint | null,
-): number | null {
-  if (!previousPoint && !nextPoint) {
+  startPoint: SeriesTimePoint,
+  endPoint: SeriesTimePoint,
+): Coordinate | null {
+  const startCoordinate = getCoordinateForTime(chart, startPoint.time);
+
+  const endCoordinate = getCoordinateForTime(chart, endPoint.time);
+
+  if (startCoordinate === null || endCoordinate === null) {
     return null;
   }
 
-  if (!previousPoint) {
-    return nextPoint?.logical ?? null;
+  const timeRange = endPoint.timestamp - startPoint.timestamp;
+
+  if (timeRange === 0) {
+    return startCoordinate;
   }
 
-  if (!nextPoint) {
-    return previousPoint.logical;
+  const ratio = (targetTime - startPoint.timestamp) / timeRange;
+
+  const coordinate = Number(startCoordinate) + (Number(endCoordinate) - Number(startCoordinate)) * ratio;
+
+  if (!Number.isFinite(coordinate)) {
+    return null;
   }
 
-  const previousDistance = Math.abs(targetTime - previousPoint.time);
-  const nextDistance = Math.abs(nextPoint.time - targetTime);
+  return coordinate as Coordinate;
+}
+
+function getCoordinateForTime(chart: IChartApi, time: Time): Coordinate | null {
+  const coordinate = chart.timeScale().timeToCoordinate(time);
 
-  return nextDistance < previousDistance ? nextPoint.logical : previousPoint.logical;
+  return isValidCoordinate(coordinate) ? coordinate : null;
 }
 
-function getSeriesTimePoints(series: SeriesApi): TimePoint[] {
+function getSeriesTimePoints(series: SeriesApi): SeriesTimePoint[] {
   const data = series.data() as readonly SeriesTimeItem[];
 
-  return data.reduce<TimePoint[]>((points, item, logical) => {
-    const time = getNumericTime(item.time);
+  return data.reduce<SeriesTimePoint[]>((points, item) => {
+    const timestamp = getNumericTime(item.time);
 
-    if (time === null) {
+    if (timestamp === null) {
       return points;
     }
 
     points.push({
-      time,
-      logical,
+      time: item.time,
+      timestamp,
     });
 
     return points;
diff --git a/src/core/Drawings/line/lineDrawing.ts b/src/core/Drawings/line/lineDrawing.ts
index 880c0f6d8a01cae6dd150e915715b91c7d0dc89f..128255b28d9bd901e225f6d8c796e10de919980b 100644
--- a/src/core/Drawings/line/lineDrawing.ts
+++ b/src/core/Drawings/line/lineDrawing.ts
@@ -1,44 +1,23 @@
-import { IPrimitivePaneView, PrimitiveHoveredItem, UTCTimestamp } from 'lightweight-charts';
-
-import {
-  CustomPriceAxisPaneView,
-  CustomPriceAxisView,
-  CustomTimeAxisPaneView,
-  CustomTimeAxisView,
-} from '@core/Drawings/axis';
-import {
-  getAnchorFromPoint,
-  getPriceDelta as getPriceDeltaFromCoordinates,
-  getXCoordinateFromTime,
-  getYCoordinateFromPrice,
-  shiftTimeByPixels,
-} from '@core/Drawings/helpers';
-import { getDistanceToSegment, updateViews } from '@core/Drawings/utils';
-import { SeriesDrawingBase } from '@src/core/Drawings/SeriesDrawingBase';
-
-import { getThemeStore } from '@src/theme';
-import { type ChartOptionsModel, LineMarker, type SettingsTab } from '@src/types';
-import { Defaults } from '@src/types/defaults';
-import { formatPrice } from '@src/utils';
-import { formatDate } from '@src/utils/formatter';
+import { PrimitiveHoveredItem, UTCTimestamp } from 'lightweight-charts';
+
+import { BaseDrawingParams, ISeriesDrawing, LinearDrawingBase, TwoPointGeometry } from '@src/core/Drawings/base';
+
+import { type ChartOptionsModel, Defaults, LineMarker, type SettingsTab } from '@src/types';
+import { formatDate, formatPrice } from '@src/utils';
 
 import { LineDrawingPaneView } from './paneView';
-import {
-  createDefaultSettings,
-  getLineDrawingSettingsTabs,
-  LineDrawingMarkers,
-  LineDrawingSettings,
-  LineDrawingStyle,
-  LineDrawingTextStyle,
-} from './settings';
-
-import type { DrawingHandle } from '@core/Drawings/handles';
-import type { Anchor, AxisLabel, AxisSegment, Point } from '@core/Drawings/types';
-import type { BaseDrawingParams, ISeriesDrawing } from '@src/core/Drawings/SeriesDrawingBase';
+import { createDefaultSettings, getLineDrawingSettingsTabs } from './settings';
+
+import type { LineDrawingMarkers, LineDrawingSettings, LineDrawingStyle, LineDrawingTextStyle } from './settings';
+
+import type { Anchor, AxisLabel, Point } from '@core/Drawings/types';
 
 type LineDrawingMode = 'idle' | 'drawing' | 'ready' | 'dragging-start' | 'dragging-end' | 'dragging-body';
+
 type LineDrawingHandleKey = 'start' | 'end';
+
 type TimeLabelKind = 'start' | 'end';
+
 type PriceLabelKind = 'start' | 'end';
 
 interface LineDrawingParams extends BaseDrawingParams {
@@ -53,52 +32,29 @@ interface LineDrawingState {
   settings: LineDrawingSettings;
 }
 
-interface LineDrawingGeometry {
-  startPoint: Point;
-  endPoint: Point;
-  left: number;
-  right: number;
-  top: number;
-  bottom: number;
-}
-
-export interface LineDrawingRenderData extends LineDrawingGeometry, LineDrawingStyle, LineDrawingTextStyle {}
+export interface LineDrawingRenderData extends TwoPointGeometry, LineDrawingStyle, LineDrawingTextStyle {}
 
 const LINE_HIT_TOLERANCE = 6;
 const MIN_LINE_SIZE = 4;
 
 export class LineDrawing
-  extends SeriesDrawingBase<LineDrawingSettings, LineDrawingHandleKey>
+  extends LinearDrawingBase<LineDrawingSettings, LineDrawingHandleKey, LineDrawingState>
   implements ISeriesDrawing
 {
   private removeSelf?: () => void;
   private openSettings?: () => void;
+
   private readonly defaultMarkers: LineDrawingMarkers;
 
   protected settings: LineDrawingSettings;
   protected mode: LineDrawingMode = 'idle';
 
-  private startAnchor: Anchor | null = null;
-  private endAnchor: Anchor | null = null;
-
-  private dragPointerId: number | null = null;
-  private dragStartPoint: Point | null = null;
-  private dragStateSnapshot: LineDrawingState | null = null;
-
   private displayFormat: ChartOptionsModel = {
     dateFormat: Defaults.dateFormat,
     timeFormat: Defaults.timeFormat,
     showTime: Defaults.showTime,
   };
 
-  private readonly paneView: LineDrawingPaneView;
-  private readonly timeAxisPaneView: CustomTimeAxisPaneView;
-  private readonly priceAxisPaneView: CustomPriceAxisPaneView;
-  private readonly startTimeAxisView: CustomTimeAxisView;
-  private readonly endTimeAxisView: CustomTimeAxisView;
-  private readonly startPriceAxisView: CustomPriceAxisView;
-  private readonly endPriceAxisView: CustomPriceAxisView;
-
   constructor({
     chart,
     series,
@@ -110,7 +66,12 @@ export class LineDrawing
     initialEvent,
     defaultMarkers = {},
   }: LineDrawingParams) {
-    super({ chart, series, container, interaction });
+    super({
+      chart,
+      series,
+      container,
+      interaction,
+    });
 
     this.removeSelf = removeSelf;
     this.openSettings = openSettings;
@@ -122,51 +83,16 @@ export class LineDrawing
     };
 
     this.settings = createDefaultSettings(this.defaultMarkers);
+    this.initializeViews(new LineDrawingPaneView(this), ['start', 'end'], ['start', 'end']);
 
-    this.paneView = new LineDrawingPaneView(this);
-
-    this.timeAxisPaneView = new CustomTimeAxisPaneView({
-      getAxisSegments: () => this.getTimeAxisSegments(),
-    });
-
-    this.priceAxisPaneView = new CustomPriceAxisPaneView({
-      getAxisSegments: () => this.getPriceAxisSegments(),
+    this.subscribeFormat(formatObservable, (format) => {
+      this.displayFormat = format;
     });
 
-    this.startTimeAxisView = new CustomTimeAxisView({
-      getAxisLabel: (kind) => this.getTimeAxisLabel(kind),
-      labelKind: 'start',
-    });
-
-    this.endTimeAxisView = new CustomTimeAxisView({
-      getAxisLabel: (kind) => this.getTimeAxisLabel(kind),
-      labelKind: 'end',
-    });
-
-    this.startPriceAxisView = new CustomPriceAxisView({
-      getAxisLabel: (kind) => this.getPriceAxisLabel(kind),
-      labelKind: 'start',
-    });
-
-    this.endPriceAxisView = new CustomPriceAxisView({
-      getAxisLabel: (kind) => this.getPriceAxisLabel(kind),
-      labelKind: 'end',
-    });
-
-    if (formatObservable) {
-      this.subscriptions.add(
-        formatObservable.subscribe((format) => {
-          this.displayFormat = format;
-          this.render();
-        }),
-      );
-    }
-
     this.series.attachPrimitive(this);
 
-    if (initialEvent && initialEvent.sourceEvent) {
-      const point = this.getEventPoint(initialEvent.sourceEvent);
-      this.startDrawing(point);
+    if (initialEvent?.sourceEvent) {
+      this.startDrawing(this.getEventPoint(initialEvent.sourceEvent));
     }
   }
 
@@ -180,7 +106,9 @@ export class LineDrawing
       mode: this.mode,
       startAnchor: this.startAnchor,
       endAnchor: this.endAnchor,
-      settings: { ...this.settings },
+      settings: {
+        ...this.settings,
+      },
     };
   }
 
@@ -221,38 +149,6 @@ export class LineDrawing
     return getLineDrawingSettingsTabs(this.settings);
   }
 
-  public updateAllViews(): void {
-    updateViews([
-      this.paneView,
-      this.timeAxisPaneView,
-      this.priceAxisPaneView,
-      this.startTimeAxisView,
-      this.endTimeAxisView,
-      this.startPriceAxisView,
-      this.endPriceAxisView,
-    ]);
-  }
-
-  public paneViews(): readonly IPrimitivePaneView[] {
-    return [this.paneView];
-  }
-
-  public timeAxisPaneViews(): readonly IPrimitivePaneView[] {
-    return [this.timeAxisPaneView];
-  }
-
-  public priceAxisPaneViews(): readonly IPrimitivePaneView[] {
-    return [this.priceAxisPaneView];
-  }
-
-  public timeAxisViews() {
-    return [this.startTimeAxisView, this.endTimeAxisView];
-  }
-
-  public priceAxisViews() {
-    return [this.startPriceAxisView, this.endPriceAxisView];
-  }
-
   public getRenderData(): LineDrawingRenderData | null {
     if (this.hidden) {
       return null;
@@ -270,25 +166,16 @@ export class LineDrawing
     };
   }
 
-  protected getDrawingHandles(): readonly DrawingHandle<LineDrawingHandleKey>[] {
-    const geometry = this.getGeometry();
+  protected getGeometry(): TwoPointGeometry | null {
+    return this.getTwoPointGeometry();
+  }
 
-    if (!geometry) {
-      return [];
-    }
+  protected getStartHandleId(): LineDrawingHandleKey {
+    return 'start';
+  }
 
-    return [
-      {
-        id: 'start',
-        ...geometry.startPoint,
-        shape: 'circle',
-      },
-      {
-        id: 'end',
-        ...geometry.endPoint,
-        shape: 'circle',
-      },
-    ];
+  protected getEndHandleId(): LineDrawingHandleKey {
+    return 'end';
   }
 
   protected getHoveredItem(x: number, y: number): PrimitiveHoveredItem | null {
@@ -296,7 +183,10 @@ export class LineDrawing
       return null;
     }
 
-    const point = { x, y };
+    const point = {
+      x,
+      y,
+    };
 
     if (this.getDrawingHandleAtPoint(point)) {
       return {
@@ -306,7 +196,7 @@ export class LineDrawing
       };
     }
 
-    if (!this.isPointNearLine(point)) {
+    if (!this.isPointNearLine(point, LINE_HIT_TOLERANCE)) {
       return null;
     }
 
@@ -317,92 +207,16 @@ export class LineDrawing
     };
   }
 
-  protected getTimeAxisSegments(): AxisSegment[] {
-    if (!this.isSelected() && !this.isCreationPending()) {
-      return [];
-    }
-
-    const geometry = this.getGeometry();
-
-    if (!geometry) {
-      return [];
-    }
-
-    const { colors } = getThemeStore();
-
-    return [
-      {
-        from: geometry.left,
-        to: geometry.right,
-        color: colors.axisMarkerAreaFill,
-      },
-    ];
-  }
-
-  protected getPriceAxisSegments(): AxisSegment[] {
-    if (!this.isSelected() && !this.isCreationPending()) {
-      return [];
-    }
-
-    const geometry = this.getGeometry();
-
-    if (!geometry) {
-      return [];
-    }
-
-    const { colors } = getThemeStore();
-
-    return [
-      {
-        from: geometry.top,
-        to: geometry.bottom,
-        color: colors.axisMarkerAreaFill,
-      },
-    ];
-  }
-
   protected getTimeAxisLabel(kind: string): AxisLabel | null {
-    if ((!this.isSelected() && !this.isCreationPending()) || (kind !== 'start' && kind !== 'end')) {
-      return null;
-    }
-
-    const coordinate = this.getTimeCoordinate(kind);
-    const text = this.getTimeText(kind);
-
-    if (coordinate === null || !text) {
-      return null;
-    }
-
-    const { colors } = getThemeStore();
-
-    return {
-      coordinate,
-      text,
-      textColor: colors.chartPriceLineText,
-      backgroundColor: colors.axisMarkerLabelFill,
-    };
+    return this.getInteractiveAxisLabel(kind, ['start', 'end'], (labelKind) => {
+      return this.createAxisLabel(this.getAnchorTimeCoordinate(labelKind), this.getTimeText(labelKind));
+    });
   }
 
   protected getPriceAxisLabel(kind: string): AxisLabel | null {
-    if ((!this.isSelected() && !this.isCreationPending()) || (kind !== 'start' && kind !== 'end')) {
-      return null;
-    }
-
-    const coordinate = this.getPriceCoordinate(kind);
-    const text = this.getPriceText(kind);
-
-    if (coordinate === null || !text) {
-      return null;
-    }
-
-    const { colors } = getThemeStore();
-
-    return {
-      coordinate,
-      text,
-      textColor: colors.chartPriceLineText,
-      backgroundColor: colors.axisMarkerLabelFill,
-    };
+    return this.getInteractiveAxisLabel(kind, ['start', 'end'], (labelKind) => {
+      return this.createAxisLabel(this.getAnchorPriceCoordinate(labelKind), this.getPriceText(labelKind));
+    });
   }
 
   protected handleDoubleClick = (event: MouseEvent): void => {
@@ -412,43 +226,18 @@ export class LineDrawing
 
     const point = this.getEventPoint(event as PointerEvent);
 
-    if (!this.getDrawingHandleAtPoint(point) && !this.isPointNearLine(point)) {
+    if (!this.getDrawingHandleAtPoint(point) && !this.isPointNearLine(point, LINE_HIT_TOLERANCE)) {
       return;
     }
 
-    event.preventDefault();
-    event.stopPropagation();
+    this.consumeEvent(event);
 
     this.openSettings?.();
   };
 
-  protected handlePointerDown = (event: PointerEvent): void => {
-    if (this.hidden || event.button !== 0) {
-      return;
-    }
-
-    const point = this.getEventPoint(event);
-
-    if (this.mode === 'idle' || this.mode === 'drawing') {
-      event.preventDefault();
-      event.stopPropagation();
-
-      if (this.mode === 'idle') {
-        this.startDrawing(point);
-      } else {
-        this.updateDrawing(point);
-        this.finishDrawing();
-      }
-
-      return;
-    }
-
-    if (this.mode !== 'ready') {
-      return;
-    }
-
+  protected handleReadyPointerDown(event: PointerEvent, point: Point): void {
     const pointTarget = this.getDrawingHandleAtPoint(point)?.id ?? null;
-    const isNearLine = this.isPointNearLine(point);
+    const isNearLine = this.isPointNearLine(point, LINE_HIT_TOLERANCE);
     const isDrawingHit = pointTarget !== null || isNearLine;
     const isSelected = this.isSelected();
 
@@ -460,8 +249,7 @@ export class LineDrawing
       return;
     }
 
-    event.preventDefault();
-    event.stopPropagation();
+    this.consumeEvent(event);
 
     if (!isSelected) {
       this.select();
@@ -470,7 +258,7 @@ export class LineDrawing
 
     const dragMode: LineDrawingMode = `dragging-${pointTarget ?? 'body'}`;
     this.startDragging(dragMode, point, event.pointerId);
-  };
+  }
 
   protected handlePointerMove = (event: PointerEvent): void => {
     const point = this.getEventPoint(event);
@@ -485,18 +273,15 @@ export class LineDrawing
     }
 
     if (this.mode === 'dragging-start' || this.mode === 'dragging-end') {
-      event.preventDefault();
-      event.stopPropagation();
-
+      this.consumeEvent(event);
       this.movePoint(point);
       this.render();
+
       return;
     }
 
     if (this.mode === 'dragging-body') {
-      event.preventDefault();
-      event.stopPropagation();
-
+      this.consumeEvent(event);
       this.moveBody(point);
       this.render();
     }
@@ -512,194 +297,34 @@ export class LineDrawing
     }
   };
 
-  private startDrawing(point: Point): void {
-    const anchor = this.createAnchor(point);
-
-    if (!anchor) {
-      return;
-    }
-
-    this.startAnchor = anchor;
-    this.endAnchor = anchor;
-    this.mode = 'drawing';
-
-    this.render();
-  }
-
-  private updateDrawing(point: Point): void {
-    const anchor = this.createAnchor(point);
-
-    if (!anchor) {
-      return;
-    }
-
-    this.endAnchor = anchor;
-    this.render();
-  }
-
-  private finishDrawing(): void {
-    const geometry = this.getGeometry();
-
-    if (!geometry) {
-      return;
-    }
-
-    const lineSize = Math.hypot(
-      geometry.endPoint.x - geometry.startPoint.x,
-      geometry.endPoint.y - geometry.startPoint.y,
-    );
-
-    if (lineSize < MIN_LINE_SIZE) {
-      this.removeSelf?.();
-      return;
-    }
-
-    this.mode = 'ready';
-    this.resolveReady?.();
-
-    this.render();
-  }
-
-  private startDragging(mode: LineDrawingMode, point: Point, pointerId: number): void {
-    this.mode = mode;
-    this.dragPointerId = pointerId;
-    this.dragStartPoint = point;
-    this.dragStateSnapshot = this.getState();
-
-    this.hideCrosshair();
-    this.render();
-  }
-
-  private finishDragging(): void {
-    this.mode = 'ready';
-    this.resolveReady?.();
-
-    this.dragPointerId = null;
-    this.dragStartPoint = null;
-    this.dragStateSnapshot = null;
-
-    this.showCrosshair();
-    this.render();
+  protected finishDrawing(): void {
+    this.finishLinearDrawing(this.getGeometry(), MIN_LINE_SIZE, () => this.removeSelf?.());
   }
 
   private movePoint(point: Point): void {
-    const anchor = this.createAnchor(point);
+    if (this.mode === 'dragging-start') {
+      this.setAnchorFromPoint('start', point);
 
-    if (!anchor) {
       return;
     }
 
-    if (this.mode === 'dragging-start') {
-      this.startAnchor = anchor;
-    }
-
     if (this.mode === 'dragging-end') {
-      this.endAnchor = anchor;
+      this.setAnchorFromPoint('end', point);
     }
   }
 
   private moveBody(point: Point): void {
     const snapshot = this.dragStateSnapshot;
 
-    if (!snapshot?.startAnchor || !snapshot.endAnchor || !this.dragStartPoint) {
-      return;
-    }
-
-    const offsetX = point.x - this.dragStartPoint.x;
-    const priceOffset = getPriceDeltaFromCoordinates(this.series, this.dragStartPoint.y, point.y);
-
-    const nextStartTime = shiftTimeByPixels(this.chart, snapshot.startAnchor.time, offsetX, this.series);
-    const nextEndTime = shiftTimeByPixels(this.chart, snapshot.endAnchor.time, offsetX, this.series);
-
-    if (nextStartTime === null || nextEndTime === null) {
+    if (!snapshot) {
       return;
     }
 
-    this.startAnchor = {
-      time: nextStartTime,
-      price: snapshot.startAnchor.price + priceOffset,
-    };
-
-    this.endAnchor = {
-      time: nextEndTime,
-      price: snapshot.endAnchor.price + priceOffset,
-    };
-  }
-
-  private createAnchor(point: Point): Anchor | null {
-    return getAnchorFromPoint(this.chart, this.series, point);
-  }
-
-  protected getGeometry(): LineDrawingGeometry | null {
-    if (!this.startAnchor || !this.endAnchor) {
-      return null;
-    }
-
-    const startX = getXCoordinateFromTime(this.chart, this.startAnchor.time, this.series);
-    const endX = getXCoordinateFromTime(this.chart, this.endAnchor.time, this.series);
-    const startY = getYCoordinateFromPrice(this.series, this.startAnchor.price);
-    const endY = getYCoordinateFromPrice(this.series, this.endAnchor.price);
-
-    if (startX === null || endX === null || startY === null || endY === null) {
-      return null;
-    }
-
-    const startPoint = {
-      x: Math.round(Number(startX)),
-      y: Math.round(Number(startY)),
-    };
-
-    const endPoint = {
-      x: Math.round(Number(endX)),
-      y: Math.round(Number(endY)),
-    };
-
-    return {
-      startPoint,
-      endPoint,
-      left: Math.min(startPoint.x, endPoint.x),
-      right: Math.max(startPoint.x, endPoint.x),
-      top: Math.min(startPoint.y, endPoint.y),
-      bottom: Math.max(startPoint.y, endPoint.y),
-    };
-  }
-
-  private isPointNearLine(point: Point): boolean {
-    const geometry = this.getGeometry();
-
-    if (!geometry) {
-      return false;
-    }
-
-    return getDistanceToSegment(point, geometry.startPoint, geometry.endPoint) <= LINE_HIT_TOLERANCE;
-  }
-
-  private getTimeCoordinate(kind: TimeLabelKind): number | null {
-    const anchor = kind === 'start' ? this.startAnchor : this.endAnchor;
-
-    if (!anchor) {
-      return null;
-    }
-
-    const coordinate = getXCoordinateFromTime(this.chart, anchor.time, this.series);
-
-    return coordinate === null ? null : Number(coordinate);
-  }
-
-  private getPriceCoordinate(kind: PriceLabelKind): number | null {
-    const anchor = kind === 'start' ? this.startAnchor : this.endAnchor;
-
-    if (!anchor) {
-      return null;
-    }
-
-    const coordinate = getYCoordinateFromPrice(this.series, anchor.price);
-
-    return coordinate === null ? null : Number(coordinate);
+    this.moveAnchorsByPixels(snapshot.startAnchor, snapshot.endAnchor, this.dragStartPoint, point);
   }
 
   private getTimeText(kind: TimeLabelKind): string {
-    const anchor = kind === 'start' ? this.startAnchor : this.endAnchor;
+    const anchor = this.getAnchor(kind);
 
     if (!anchor || typeof anchor.time !== 'number') {
       return '';
@@ -714,7 +339,7 @@ export class LineDrawing
   }
 
   private getPriceText(kind: PriceLabelKind): string {
-    const anchor = kind === 'start' ? this.startAnchor : this.endAnchor;
+    const anchor = this.getAnchor(kind);
 
     if (!anchor) {
       return '';
diff --git a/src/core/Drawings/parallelChannel/parallelChannel.ts b/src/core/Drawings/parallelChannel/parallelChannel.ts
index f86e4ad74ed095b5d73b85cf138a6bd9be898873..db64b42564a8f490b279578f959fb723fb88dced 100644
--- a/src/core/Drawings/parallelChannel/parallelChannel.ts
+++ b/src/core/Drawings/parallelChannel/parallelChannel.ts
@@ -1,26 +1,11 @@
-import { IPrimitivePaneView, PrimitiveHoveredItem, UTCTimestamp } from 'lightweight-charts';
+import { PrimitiveHoveredItem, UTCTimestamp } from 'lightweight-charts';
 
-import {
-  CustomPriceAxisPaneView,
-  CustomPriceAxisView,
-  CustomTimeAxisPaneView,
-  CustomTimeAxisView,
-} from '@core/Drawings/axis';
-import {
-  getAnchorFromPoint,
-  getPriceDelta as getPriceDeltaFromCoordinates,
-  getPriceFromYCoordinate,
-  getXCoordinateFromTime,
-  getYCoordinateFromPrice,
-  shiftTimeByPixels,
-} from '@core/Drawings/helpers';
-import { getDistanceToSegment, updateViews } from '@core/Drawings/utils';
-import { SeriesDrawingBase } from '@src/core/Drawings/SeriesDrawingBase';
-
-import { getThemeStore } from '@src/theme';
-import { Defaults } from '@src/types/defaults';
-import { formatPrice } from '@src/utils';
-import { formatDate } from '@src/utils/formatter';
+import { getPriceFromYCoordinate, getYCoordinateFromPrice } from '@core/Drawings/helpers';
+import { getDistanceToSegment } from '@core/Drawings/utils';
+
+import { BaseDrawingParams, ISeriesDrawing, TwoPointDrawingBase } from '@src/core/Drawings/base';
+import { type ChartOptionsModel, Defaults, type SettingsTab } from '@src/types';
+import { formatDate, formatPrice } from '@src/utils';
 
 import { ParallelChannelPaneView } from './paneView';
 import {
@@ -33,8 +18,6 @@ import {
 
 import type { DrawingHandle } from '@core/Drawings/handles';
 import type { Anchor, AxisLabel, AxisSegment, Point } from '@core/Drawings/types';
-import type { BaseDrawingParams, ISeriesDrawing } from '@src/core/Drawings/SeriesDrawingBase';
-import type { ChartOptionsModel, SettingsTab } from '@src/types';
 
 type ParallelChannelMode = 'idle' | 'drawing-line' | 'drawing-channel' | 'ready' | 'dragging';
 
@@ -49,6 +32,7 @@ type ParallelChannelDragTarget =
 
 type ParallelChannelHandleKey = Exclude<ParallelChannelDragTarget, 'body'>;
 type TimeLabelKind = 'start' | 'end';
+
 type PriceLabelKind = 'main-start' | 'main-end' | 'parallel-start' | 'parallel-end';
 
 type ParallelChannelParams = BaseDrawingParams;
@@ -88,7 +72,7 @@ const MIN_CHANNEL_WIDTH = 4;
 const VERTICAL_LINE_TOLERANCE = 0.001;
 
 export class ParallelChannel
-  extends SeriesDrawingBase<ParallelChannelSettings, ParallelChannelHandleKey>
+  extends TwoPointDrawingBase<ParallelChannelSettings, ParallelChannelHandleKey>
   implements ISeriesDrawing
 {
   private openSettings?: () => void;
@@ -96,10 +80,7 @@ export class ParallelChannel
   protected settings: ParallelChannelSettings = createDefaultSettings();
   protected mode: ParallelChannelMode = 'idle';
 
-  private startAnchor: Anchor | null = null;
-  private endAnchor: Anchor | null = null;
   private priceOffset: number | null = null;
-
   private activeDragTarget: ParallelChannelDragTarget | null = null;
   private dragPointerId: number | null = null;
   private dragStartPoint: Point | null = null;
@@ -111,18 +92,6 @@ export class ParallelChannel
     showTime: Defaults.showTime,
   };
 
-  private paneView: ParallelChannelPaneView;
-  private timeAxisPaneView: CustomTimeAxisPaneView;
-  private priceAxisPaneView: CustomPriceAxisPaneView;
-
-  private startTimeAxisView: CustomTimeAxisView;
-  private endTimeAxisView: CustomTimeAxisView;
-
-  private mainStartPriceAxisView: CustomPriceAxisView;
-  private mainEndPriceAxisView: CustomPriceAxisView;
-  private parallelStartPriceAxisView: CustomPriceAxisView;
-  private parallelEndPriceAxisView: CustomPriceAxisView;
-
   constructor({
     container,
     interaction,
@@ -132,69 +101,23 @@ export class ParallelChannel
     series,
     initialEvent,
   }: ParallelChannelParams) {
-    super({
-      chart,
-      series,
-      container,
-      interaction,
-    });
+    super({ chart, series, container, interaction });
 
     this.openSettings = openSettings;
-
-    this.paneView = new ParallelChannelPaneView(this);
-
-    this.timeAxisPaneView = new CustomTimeAxisPaneView({
-      getAxisSegments: () => this.getTimeAxisSegments(),
-    });
-
-    this.priceAxisPaneView = new CustomPriceAxisPaneView({
-      getAxisSegments: () => this.getPriceAxisSegments(),
-    });
-
-    this.startTimeAxisView = new CustomTimeAxisView({
-      getAxisLabel: (kind) => this.getTimeAxisLabel(kind),
-      labelKind: 'start',
+    this.initializeViews(
+      new ParallelChannelPaneView(this),
+      ['start', 'end'],
+      ['main-start', 'main-end', 'parallel-start', 'parallel-end'],
+    );
+
+    this.subscribeFormat(formatObservable, (format) => {
+      this.displayFormat = format;
     });
 
-    this.endTimeAxisView = new CustomTimeAxisView({
-      getAxisLabel: (kind) => this.getTimeAxisLabel(kind),
-      labelKind: 'end',
-    });
-
-    this.mainStartPriceAxisView = new CustomPriceAxisView({
-      getAxisLabel: (kind) => this.getPriceAxisLabel(kind),
-      labelKind: 'main-start',
-    });
-
-    this.mainEndPriceAxisView = new CustomPriceAxisView({
-      getAxisLabel: (kind) => this.getPriceAxisLabel(kind),
-      labelKind: 'main-end',
-    });
-
-    this.parallelStartPriceAxisView = new CustomPriceAxisView({
-      getAxisLabel: (kind) => this.getPriceAxisLabel(kind),
-      labelKind: 'parallel-start',
-    });
-
-    this.parallelEndPriceAxisView = new CustomPriceAxisView({
-      getAxisLabel: (kind) => this.getPriceAxisLabel(kind),
-      labelKind: 'parallel-end',
-    });
-
-    if (formatObservable) {
-      this.subscriptions.add(
-        formatObservable.subscribe((format) => {
-          this.displayFormat = format;
-          this.render();
-        }),
-      );
-    }
-
     this.series.attachPrimitive(this);
 
-    if (initialEvent && initialEvent.sourceEvent) {
-      const point = this.getEventPoint(initialEvent.sourceEvent);
-      this.startDrawing(point);
+    if (initialEvent?.sourceEvent) {
+      this.startDrawing(this.getEventPoint(initialEvent.sourceEvent));
     }
   }
 
@@ -254,45 +177,6 @@ export class ParallelChannel
     return getParallelChannelSettingsTabs(this.settings);
   }
 
-  public updateAllViews(): void {
-    updateViews([
-      this.paneView,
-      this.timeAxisPaneView,
-      this.priceAxisPaneView,
-      this.startTimeAxisView,
-      this.endTimeAxisView,
-      this.mainStartPriceAxisView,
-      this.mainEndPriceAxisView,
-      this.parallelStartPriceAxisView,
-      this.parallelEndPriceAxisView,
-    ]);
-  }
-
-  public paneViews(): readonly IPrimitivePaneView[] {
-    return [this.paneView];
-  }
-
-  public timeAxisPaneViews(): readonly IPrimitivePaneView[] {
-    return [this.timeAxisPaneView];
-  }
-
-  public priceAxisPaneViews(): readonly IPrimitivePaneView[] {
-    return [this.priceAxisPaneView];
-  }
-
-  public timeAxisViews() {
-    return [this.startTimeAxisView, this.endTimeAxisView];
-  }
-
-  public priceAxisViews() {
-    return [
-      this.mainStartPriceAxisView,
-      this.mainEndPriceAxisView,
-      this.parallelStartPriceAxisView,
-      this.parallelEndPriceAxisView,
-    ];
-  }
-
   public getRenderData(): ParallelChannelRenderData | null {
     if (this.hidden) {
       return null;
@@ -380,83 +264,49 @@ export class ParallelChannel
   }
 
   protected getTimeAxisSegments(): AxisSegment[] {
-    if (!this.isSelected() && !this.isCreationPending()) {
+    if (!this.shouldShowInteractiveAxis()) {
       return [];
     }
 
     const geometry = this.getGeometry();
 
-    if (!geometry) {
-      return [];
-    }
-
-    const { colors } = getThemeStore();
-
-    return [
-      {
-        from: geometry.left,
-        to: geometry.right,
-        color: colors.axisMarkerAreaFill,
-      },
-    ];
+    return geometry ? [this.createAxisSegment(geometry.left, geometry.right)] : [];
   }
 
   protected getPriceAxisSegments(): AxisSegment[] {
-    if (!this.isSelected() && !this.isCreationPending()) {
+    if (!this.shouldShowInteractiveAxis()) {
       return [];
     }
 
     const geometry = this.getGeometry();
 
-    if (!geometry) {
-      return [];
-    }
-
-    const { colors } = getThemeStore();
-
-    return [
-      {
-        from: geometry.top,
-        to: geometry.bottom,
-        color: colors.axisMarkerAreaFill,
-      },
-    ];
+    return geometry ? [this.createAxisSegment(geometry.top, geometry.bottom)] : [];
   }
 
   protected getTimeAxisLabel(kind: string): AxisLabel | null {
-    if ((!this.isSelected() && !this.isCreationPending()) || (kind !== 'start' && kind !== 'end')) {
+    if (!this.shouldShowInteractiveAxis() || (kind !== 'start' && kind !== 'end')) {
       return null;
     }
 
-    const anchor = kind === 'start' ? this.startAnchor : this.endAnchor;
+    const anchor = this.getAnchor(kind);
 
     if (!anchor || typeof anchor.time !== 'number') {
       return null;
     }
 
-    const coordinate = getXCoordinateFromTime(this.chart, anchor.time, this.series);
-
-    if (coordinate === null) {
-      return null;
-    }
-
-    const { colors } = getThemeStore();
-
-    return {
-      coordinate,
-      text: formatDate(
+    return this.createAxisLabel(
+      this.getAnchorTimeCoordinate(kind),
+      formatDate(
         anchor.time as UTCTimestamp,
         this.displayFormat.dateFormat,
         this.displayFormat.timeFormat,
         this.displayFormat.showTime,
       ),
-      textColor: colors.chartPriceLineText,
-      backgroundColor: colors.axisMarkerLabelFill,
-    };
+    );
   }
 
   protected getPriceAxisLabel(kind: string): AxisLabel | null {
-    if ((!this.isSelected() && !this.isCreationPending()) || !isPriceLabelKind(kind)) {
+    if (!this.shouldShowInteractiveAxis() || !isPriceLabelKind(kind)) {
       return null;
     }
 
@@ -468,35 +318,16 @@ export class ParallelChannel
 
     const coordinate = getYCoordinateFromPrice(this.series, price);
 
-    if (coordinate === null) {
-      return null;
-    }
-
-    const { colors } = getThemeStore();
-
-    return {
-      coordinate,
-      text: formatPrice(price) ?? '',
-      textColor: colors.chartPriceLineText,
-      backgroundColor: colors.axisMarkerLabelFill,
-    };
+    return this.createAxisLabel(coordinate === null ? null : Number(coordinate), formatPrice(price) ?? '');
   }
 
   protected handleDoubleClick = (event: MouseEvent): void => {
-    if (this.hidden || this.mode !== 'ready' || !this.isSelected()) {
-      return;
-    }
-
-    const point = this.getEventPoint(event as PointerEvent);
-
-    if (!this.isPointOnChannel(point) && !this.getDrawingHandleAtPoint(point)) {
-      return;
-    }
-
-    event.preventDefault();
-    event.stopPropagation();
-
-    this.openSettings?.();
+    this.openSettingsOnDoubleClick(
+      event,
+      (point) => Boolean(this.isPointOnChannel(point) || this.getDrawingHandleAtPoint(point)),
+      () => this.openSettings?.(),
+      true,
+    );
   };
 
   protected handlePointerDown = (event: PointerEvent): void => {
@@ -508,14 +339,12 @@ export class ParallelChannel
 
     switch (this.mode) {
       case 'idle':
-        event.preventDefault();
-        event.stopPropagation();
+        this.consumeEvent(event);
         this.startDrawing(point);
         return;
 
       case 'drawing-line':
-        event.preventDefault();
-        event.stopPropagation();
+        this.consumeEvent(event);
         this.setEndAnchor(point);
 
         if (!this.hasValidMainLine()) {
@@ -529,8 +358,7 @@ export class ParallelChannel
         return;
 
       case 'drawing-channel':
-        event.preventDefault();
-        event.stopPropagation();
+        this.consumeEvent(event);
         this.setPriceOffset(point);
 
         if (!this.hasValidChannelWidth()) {
@@ -557,15 +385,13 @@ export class ParallelChannel
     }
 
     if (!isSelected) {
-      event.preventDefault();
-      event.stopPropagation();
+      this.consumeEvent(event);
       this.select();
       return;
     }
 
     if (pointTarget || isChannelHit) {
-      event.preventDefault();
-      event.stopPropagation();
+      this.consumeEvent(event);
       this.startDragging(pointTarget ?? 'body', point, event.pointerId);
       return;
     }
@@ -592,9 +418,7 @@ export class ParallelChannel
       return;
     }
 
-    event.preventDefault();
-    event.stopPropagation();
-
+    this.consumeEvent(event);
     this.applyDrag(point);
     this.render();
   };
@@ -612,23 +436,28 @@ export class ParallelChannel
       return null;
     }
 
-    const startPoint = this.getAnchorPoint(this.startAnchor);
-    const endPoint = this.getAnchorPoint(this.endAnchor);
+    const mainGeometry = this.getTwoPointGeometry();
 
-    const parallelStartPoint = this.getAnchorPoint({
+    if (!mainGeometry) {
+      return null;
+    }
+
+    const parallelStartPoint = this.getPointFromAnchor({
       time: this.startAnchor.time,
       price: this.startAnchor.price + this.priceOffset,
     });
 
-    const parallelEndPoint = this.getAnchorPoint({
+    const parallelEndPoint = this.getPointFromAnchor({
       time: this.endAnchor.time,
       price: this.endAnchor.price + this.priceOffset,
     });
 
-    if (!startPoint || !endPoint || !parallelStartPoint || !parallelEndPoint) {
+    if (!parallelStartPoint || !parallelEndPoint) {
       return null;
     }
 
+    const { startPoint } = mainGeometry;
+    const { endPoint } = mainGeometry;
     const mainMiddlePoint = getMiddlePoint(startPoint, endPoint);
     const parallelMiddlePoint = getMiddlePoint(parallelStartPoint, parallelEndPoint);
     const middleStartPoint = getMiddlePoint(startPoint, parallelStartPoint);
@@ -658,18 +487,15 @@ export class ParallelChannel
       return;
     }
 
-    this.startAnchor = anchor;
-    this.endAnchor = anchor;
+    this.setAnchors(anchor, anchor);
     this.priceOffset = 0;
     this.mode = 'drawing-line';
-
     this.render();
   }
 
   private finishDrawing(): void {
     this.mode = 'ready';
     this.resolveReady?.();
-
     this.render();
   }
 
@@ -679,19 +505,16 @@ export class ParallelChannel
     this.dragPointerId = pointerId;
     this.dragStartPoint = point;
     this.dragStateSnapshot = this.getState();
-
     this.hideCrosshair();
     this.render();
   }
 
   private finishDragging(): void {
     this.mode = 'ready';
-
     this.activeDragTarget = null;
     this.dragPointerId = null;
     this.dragStartPoint = null;
     this.dragStateSnapshot = null;
-
     this.showCrosshair();
     this.render();
   }
@@ -701,31 +524,24 @@ export class ParallelChannel
       case 'main-start':
         this.moveMainEdge('start', point);
         break;
-
       case 'main-middle':
         this.moveMainMiddle(point);
         break;
-
       case 'main-end':
         this.moveMainEdge('end', point);
         break;
-
       case 'parallel-start':
         this.moveParallelEdge('start', point);
         break;
-
       case 'parallel-middle':
         this.moveParallelMiddle(point);
         break;
-
       case 'parallel-end':
         this.moveParallelEdge('end', point);
         break;
-
       case 'body':
         this.moveBody(point);
         break;
-
       default:
         break;
     }
@@ -738,22 +554,11 @@ export class ParallelChannel
       return;
     }
 
-    const previousAnchor = kind === 'start' ? this.startAnchor : this.endAnchor;
+    const previousAnchor = this.getAnchor(kind);
+    this.setAnchor(kind, anchor);
 
-    if (kind === 'start') {
-      this.startAnchor = anchor;
-    } else {
-      this.endAnchor = anchor;
-    }
-
-    if (this.hasValidMainLine()) {
-      return;
-    }
-
-    if (kind === 'start') {
-      this.startAnchor = previousAnchor;
-    } else {
-      this.endAnchor = previousAnchor;
+    if (!this.hasValidMainLine()) {
+      this.setAnchor(kind, previousAnchor);
     }
   }
 
@@ -765,27 +570,16 @@ export class ParallelChannel
       return;
     }
 
-    const previousAnchor = kind === 'start' ? this.startAnchor : this.endAnchor;
-
+    const previousAnchor = this.getAnchor(kind);
     const baseAnchor: Anchor = {
       time: anchor.time,
       price: anchor.price - snapshot.priceOffset,
     };
 
-    if (kind === 'start') {
-      this.startAnchor = baseAnchor;
-    } else {
-      this.endAnchor = baseAnchor;
-    }
-
-    if (this.hasValidMainLine()) {
-      return;
-    }
+    this.setAnchor(kind, baseAnchor);
 
-    if (kind === 'start') {
-      this.startAnchor = previousAnchor;
-    } else {
-      this.endAnchor = previousAnchor;
+    if (!this.hasValidMainLine()) {
+      this.setAnchor(kind, previousAnchor);
     }
   }
 
@@ -817,65 +611,35 @@ export class ParallelChannel
 
     this.priceOffset = snapshot.priceOffset - priceDelta;
 
-    if (this.hasValidChannelWidth()) {
-      return;
+    if (!this.hasValidChannelWidth()) {
+      this.startAnchor = snapshot.startAnchor;
+      this.endAnchor = snapshot.endAnchor;
+      this.priceOffset = snapshot.priceOffset;
     }
-
-    this.startAnchor = snapshot.startAnchor;
-    this.endAnchor = snapshot.endAnchor;
-    this.priceOffset = snapshot.priceOffset;
   }
 
   private moveParallelMiddle(point: Point): void {
     const previousOffset = this.priceOffset;
-
     this.setPriceOffset(point);
 
-    if (this.hasValidChannelWidth()) {
-      return;
+    if (!this.hasValidChannelWidth()) {
+      this.priceOffset = previousOffset;
     }
-
-    this.priceOffset = previousOffset;
   }
 
   private moveBody(point: Point): void {
     const snapshot = this.dragStateSnapshot;
 
-    if (!snapshot?.startAnchor || !snapshot.endAnchor || snapshot.priceOffset === null || !this.dragStartPoint) {
+    if (!snapshot || snapshot.priceOffset === null) {
       return;
     }
 
-    const offsetX = point.x - this.dragStartPoint.x;
-    const priceDelta = getPriceDeltaFromCoordinates(this.series, this.dragStartPoint.y, point.y);
-
-    const startTime = shiftTimeByPixels(this.chart, snapshot.startAnchor.time, offsetX, this.series);
-    const endTime = shiftTimeByPixels(this.chart, snapshot.endAnchor.time, offsetX, this.series);
-
-    if (startTime === null || endTime === null) {
-      return;
-    }
-
-    this.startAnchor = {
-      time: startTime,
-      price: snapshot.startAnchor.price + priceDelta,
-    };
-
-    this.endAnchor = {
-      time: endTime,
-      price: snapshot.endAnchor.price + priceDelta,
-    };
-
+    this.moveAnchorsByPixels(snapshot.startAnchor, snapshot.endAnchor, this.dragStartPoint, point);
     this.priceOffset = snapshot.priceOffset;
   }
 
   private setEndAnchor(point: Point): void {
-    const anchor = this.createAnchor(point);
-
-    if (!anchor) {
-      return;
-    }
-
-    this.endAnchor = anchor;
+    this.setAnchorFromPoint('end', point);
   }
 
   private setPriceOffset(point: Point): void {
@@ -894,8 +658,8 @@ export class ParallelChannel
   }
 
   private getLinePriceAtX(startAnchor: Anchor, endAnchor: Anchor, x: number): number | null {
-    const startPoint = this.getAnchorPoint(startAnchor);
-    const endPoint = this.getAnchorPoint(endAnchor);
+    const startPoint = this.getPointFromAnchor(startAnchor);
+    const endPoint = this.getPointFromAnchor(endAnchor);
 
     if (!startPoint || !endPoint) {
       return null;
@@ -916,21 +680,13 @@ export class ParallelChannel
   private hasValidMainLine(): boolean {
     const geometry = this.getGeometry();
 
-    if (!geometry) {
-      return false;
-    }
-
-    return getDistance(geometry.startPoint, geometry.endPoint) >= MIN_LINE_SIZE;
+    return geometry !== null && getDistance(geometry.startPoint, geometry.endPoint) >= MIN_LINE_SIZE;
   }
 
   private hasValidChannelWidth(): boolean {
     const geometry = this.getGeometry();
 
-    if (!geometry) {
-      return false;
-    }
-
-    return getDistance(geometry.startPoint, geometry.parallelStartPoint) >= MIN_CHANNEL_WIDTH;
+    return geometry !== null && getDistance(geometry.startPoint, geometry.parallelStartPoint) >= MIN_CHANNEL_WIDTH;
   }
 
   private isPointOnChannel(point: Point): boolean {
@@ -971,38 +727,16 @@ export class ParallelChannel
     switch (kind) {
       case 'main-start':
         return this.startAnchor.price;
-
       case 'main-end':
         return this.endAnchor.price;
-
       case 'parallel-start':
         return this.startAnchor.price + this.priceOffset;
-
       case 'parallel-end':
         return this.endAnchor.price + this.priceOffset;
-
       default:
         return null;
     }
   }
-
-  private getAnchorPoint(anchor: Anchor): Point | null {
-    const x = getXCoordinateFromTime(this.chart, anchor.time, this.series);
-    const y = getYCoordinateFromPrice(this.series, anchor.price);
-
-    if (x === null || y === null) {
-      return null;
-    }
-
-    return {
-      x: Number(x),
-      y: Number(y),
-    };
-  }
-
-  private createAnchor(point: Point): Anchor | null {
-    return getAnchorFromPoint(this.chart, this.series, point);
-  }
 }
 
 function isPriceLabelKind(kind: string): kind is PriceLabelKind {
diff --git a/src/core/Drawings/ray/ray.ts b/src/core/Drawings/ray/ray.ts
index 0bc1c878ae9556e24b133d92343005b0e8179874..46a152f0867d181894d1d1210a1698eb5ccf7261 100644
--- a/src/core/Drawings/ray/ray.ts
+++ b/src/core/Drawings/ray/ray.ts
@@ -1,38 +1,24 @@
-import { IPrimitivePaneView, PrimitiveHoveredItem, UTCTimestamp } from 'lightweight-charts';
-
-import {
-  CustomPriceAxisPaneView,
-  CustomPriceAxisView,
-  CustomTimeAxisPaneView,
-  CustomTimeAxisView,
-} from '@core/Drawings/axis';
-import {
-  getAnchorFromPoint,
-  getPriceDelta as getPriceDeltaFromCoordinates,
-  getXCoordinateFromTime,
-  getYCoordinateFromPrice,
-  shiftTimeByPixels,
-} from '@core/Drawings/helpers';
-import { getDistanceToSegment, updateViews } from '@core/Drawings/utils';
-import { SeriesDrawingBase } from '@src/core/Drawings/SeriesDrawingBase';
-
-import { getThemeStore } from '@src/theme';
-import { Defaults } from '@src/types/defaults';
-import { formatPrice } from '@src/utils';
-import { formatDate } from '@src/utils/formatter';
+import { PrimitiveHoveredItem, UTCTimestamp } from 'lightweight-charts';
+
+import { getDistanceToSegment } from '@core/Drawings/utils';
+
+import { BaseDrawingParams, ISeriesDrawing, LinearDrawingBase } from '@src/core/Drawings/base';
+import { type ChartOptionsModel, Defaults, type SettingsTab } from '@src/types';
+import { formatDate, formatPrice } from '@src/utils';
 
 import { RayPaneView } from './paneView';
+import { createDefaultSettings, getRaySettingTabs } from './settings';
 
-import { createDefaultSettings, getRaySettingTabs, RaySettings, RayStyle, RayTextStyle } from './settings';
+import type { RaySettings, RayStyle, RayTextStyle } from './settings';
 
-import type { DrawingHandle } from '@core/Drawings/handles';
-import type { Anchor, AxisLabel, AxisSegment, Point } from '@core/Drawings/types';
-import type { BaseDrawingParams, ISeriesDrawing } from '@src/core/Drawings/SeriesDrawingBase';
-import type { ChartOptionsModel, SettingsTab } from '@src/types';
+import type { Anchor, AxisLabel, Point } from '@core/Drawings/types';
 
 type RayMode = 'idle' | 'drawing' | 'ready' | 'dragging-start' | 'dragging-direction' | 'dragging-body';
+
 type RayHandleKey = 'start' | 'direction';
+
 type TimeLabelKind = 'start' | 'direction';
+
 type PriceLabelKind = 'start' | 'direction';
 
 type RayParams = BaseDrawingParams;
@@ -55,24 +41,23 @@ interface RayGeometry {
   bottom: number;
 }
 
+interface RayIntersection {
+  point: Point;
+  t: number;
+}
+
 export interface RayRenderData extends RayGeometry, RayStyle, RayTextStyle {}
 
 const LINE_HIT_TOLERANCE = 6;
 const MIN_LINE_SIZE = 4;
 
-export class Ray extends SeriesDrawingBase<RaySettings, RayHandleKey> implements ISeriesDrawing {
+export class Ray extends LinearDrawingBase<RaySettings, RayHandleKey, RayState> implements ISeriesDrawing {
   private removeSelf?: () => void;
   private openSettings?: () => void;
 
   protected settings: RaySettings = createDefaultSettings();
-  protected mode: RayMode = 'idle';
-
-  private startAnchor: Anchor | null = null;
-  private directionAnchor: Anchor | null = null;
 
-  private dragPointerId: number | null = null;
-  private dragStartPoint: Point | null = null;
-  private dragStateSnapshot: RayState | null = null;
+  protected mode: RayMode = 'idle';
 
   private displayFormat: ChartOptionsModel = {
     dateFormat: Defaults.dateFormat,
@@ -80,14 +65,6 @@ export class Ray extends SeriesDrawingBase<RaySettings, RayHandleKey> implements
     showTime: Defaults.showTime,
   };
 
-  private readonly paneView: RayPaneView;
-  private readonly timeAxisPaneView: CustomTimeAxisPaneView;
-  private readonly priceAxisPaneView: CustomPriceAxisPaneView;
-  private readonly startTimeAxisView: CustomTimeAxisView;
-  private readonly directionTimeAxisView: CustomTimeAxisView;
-  private readonly startPriceAxisView: CustomPriceAxisView;
-  private readonly directionPriceAxisView: CustomPriceAxisView;
-
   constructor({
     chart,
     series,
@@ -98,56 +75,34 @@ export class Ray extends SeriesDrawingBase<RaySettings, RayHandleKey> implements
     openSettings,
     initialEvent,
   }: RayParams) {
-    super({ chart, series, container, interaction });
+    super({
+      chart,
+      series,
+      container,
+      interaction,
+    });
 
     this.removeSelf = removeSelf;
     this.openSettings = openSettings;
+    this.initializeViews(new RayPaneView(this), ['start', 'direction'], ['start', 'direction']);
 
-    this.paneView = new RayPaneView(this);
-
-    this.timeAxisPaneView = new CustomTimeAxisPaneView({
-      getAxisSegments: () => this.getTimeAxisSegments(),
+    this.subscribeFormat(formatObservable, (format) => {
+      this.displayFormat = format;
     });
 
-    this.priceAxisPaneView = new CustomPriceAxisPaneView({
-      getAxisSegments: () => this.getPriceAxisSegments(),
-    });
-
-    this.startTimeAxisView = new CustomTimeAxisView({
-      getAxisLabel: (kind) => this.getTimeAxisLabel(kind),
-      labelKind: 'start',
-    });
-
-    this.directionTimeAxisView = new CustomTimeAxisView({
-      getAxisLabel: (kind) => this.getTimeAxisLabel(kind),
-      labelKind: 'direction',
-    });
-
-    this.startPriceAxisView = new CustomPriceAxisView({
-      getAxisLabel: (kind) => this.getPriceAxisLabel(kind),
-      labelKind: 'start',
-    });
-
-    this.directionPriceAxisView = new CustomPriceAxisView({
-      getAxisLabel: (kind) => this.getPriceAxisLabel(kind),
-      labelKind: 'direction',
-    });
+    this.series.attachPrimitive(this);
 
-    if (formatObservable) {
-      this.subscriptions.add(
-        formatObservable.subscribe((format) => {
-          this.displayFormat = format;
-          this.render();
-        }),
-      );
+    if (initialEvent?.sourceEvent) {
+      this.startDrawing(this.getEventPoint(initialEvent.sourceEvent));
     }
+  }
 
-    this.series.attachPrimitive(this);
+  private get directionAnchor(): Anchor | null {
+    return this.endAnchor;
+  }
 
-    if (initialEvent && initialEvent.sourceEvent) {
-      const point = this.getEventPoint(initialEvent.sourceEvent);
-      this.startDrawing(point);
-    }
+  private set directionAnchor(anchor: Anchor | null) {
+    this.endAnchor = anchor;
   }
 
   public isCreationPending(): boolean {
@@ -160,11 +115,17 @@ export class Ray extends SeriesDrawingBase<RaySettings, RayHandleKey> implements
       mode: this.mode,
       startAnchor: this.startAnchor,
       directionAnchor: this.directionAnchor,
-      settings: { ...this.settings },
+      settings: {
+        ...this.settings,
+      },
     };
   }
 
   public setState(state: unknown): void {
+    if (!state || typeof state !== 'object') {
+      return;
+    }
+
     const nextState = state as Partial<RayState>;
 
     if ('hidden' in nextState && typeof nextState.hidden === 'boolean') {
@@ -197,38 +158,6 @@ export class Ray extends SeriesDrawingBase<RaySettings, RayHandleKey> implements
     return getRaySettingTabs(this.settings);
   }
 
-  public updateAllViews(): void {
-    updateViews([
-      this.paneView,
-      this.timeAxisPaneView,
-      this.priceAxisPaneView,
-      this.startTimeAxisView,
-      this.directionTimeAxisView,
-      this.startPriceAxisView,
-      this.directionPriceAxisView,
-    ]);
-  }
-
-  public paneViews(): readonly IPrimitivePaneView[] {
-    return [this.paneView];
-  }
-
-  public timeAxisPaneViews(): readonly IPrimitivePaneView[] {
-    return [this.timeAxisPaneView];
-  }
-
-  public priceAxisPaneViews(): readonly IPrimitivePaneView[] {
-    return [this.priceAxisPaneView];
-  }
-
-  public timeAxisViews() {
-    return [this.startTimeAxisView, this.directionTimeAxisView];
-  }
-
-  public priceAxisViews() {
-    return [this.startPriceAxisView, this.directionPriceAxisView];
-  }
-
   public getRenderData(): RayRenderData | null {
     if (this.hidden) {
       return null;
@@ -246,27 +175,16 @@ export class Ray extends SeriesDrawingBase<RaySettings, RayHandleKey> implements
     };
   }
 
-  protected getDrawingHandles(): readonly DrawingHandle<RayHandleKey>[] {
-    const geometry = this.getGeometry();
+  protected getStartHandleId(): RayHandleKey {
+    return 'start';
+  }
 
-    if (!geometry) {
-      return [];
-    }
+  protected getEndHandleId(): RayHandleKey {
+    return 'direction';
+  }
 
-    return [
-      {
-        id: 'start',
-        ...geometry.startPoint,
-        shape: 'circle',
-        strokeColor: this.settings.lineColor,
-      },
-      {
-        id: 'direction',
-        ...geometry.directionPoint,
-        shape: 'circle',
-        strokeColor: this.settings.lineColor,
-      },
-    ];
+  protected getLineHandleStrokeColor(): string {
+    return this.settings.lineColor;
   }
 
   protected getHoveredItem(x: number, y: number): PrimitiveHoveredItem | null {
@@ -274,7 +192,10 @@ export class Ray extends SeriesDrawingBase<RaySettings, RayHandleKey> implements
       return null;
     }
 
-    const point = { x, y };
+    const point = {
+      x,
+      y,
+    };
 
     if (this.getDrawingHandleAtPoint(point)) {
       return {
@@ -295,139 +216,57 @@ export class Ray extends SeriesDrawingBase<RaySettings, RayHandleKey> implements
     };
   }
 
-  protected getTimeAxisSegments(): AxisSegment[] {
-    if (!this.isSelected() && !this.isCreationPending()) {
-      return [];
-    }
-
-    const geometry = this.getGeometry();
-
-    if (!geometry) {
-      return [];
-    }
-
-    const { colors } = getThemeStore();
+  protected getTimeAxisLabel(kind: string): AxisLabel | null {
+    return this.getInteractiveAxisLabel(kind, ['start', 'direction'], (labelKind) => {
+      const anchorKind = labelKind === 'start' ? 'start' : 'end';
 
-    return [
-      {
-        from: Math.min(geometry.startPoint.x, geometry.directionPoint.x),
-        to: Math.max(geometry.startPoint.x, geometry.directionPoint.x),
-        color: colors.axisMarkerAreaFill,
-      },
-    ];
+      return this.createAxisLabel(this.getAnchorTimeCoordinate(anchorKind), this.getTimeText(labelKind));
+    });
   }
 
-  protected getPriceAxisSegments(): AxisSegment[] {
-    if (!this.isSelected() && !this.isCreationPending()) {
-      return [];
-    }
-
-    const geometry = this.getGeometry();
-
-    if (!geometry) {
-      return [];
-    }
-
-    const { colors } = getThemeStore();
+  protected getPriceAxisLabel(kind: string): AxisLabel | null {
+    return this.getInteractiveAxisLabel(kind, ['start', 'direction'], (labelKind) => {
+      const anchorKind = labelKind === 'start' ? 'start' : 'end';
 
-    return [
-      {
-        from: Math.min(geometry.startPoint.y, geometry.directionPoint.y),
-        to: Math.max(geometry.startPoint.y, geometry.directionPoint.y),
-        color: colors.axisMarkerAreaFill,
-      },
-    ];
+      return this.createAxisLabel(this.getAnchorPriceCoordinate(anchorKind), this.getPriceText(labelKind));
+    });
   }
 
-  protected getTimeAxisLabel(kind: string): AxisLabel | null {
-    if ((!this.isSelected() && !this.isCreationPending()) || (kind !== 'start' && kind !== 'direction')) {
-      return null;
-    }
-
-    const coordinate = this.getTimeCoordinate(kind);
-    const text = this.getTimeText(kind);
-
-    if (coordinate === null || !text) {
-      return null;
-    }
-
-    const { colors } = getThemeStore();
-
-    return {
-      coordinate,
-      text,
-      textColor: colors.chartPriceLineText,
-      backgroundColor: colors.axisMarkerLabelFill,
-    };
-  }
+  protected getGeometry(): RayGeometry | null {
+    const geometry = this.getTwoPointGeometry();
 
-  protected getPriceAxisLabel(kind: string): AxisLabel | null {
-    if ((!this.isSelected() && !this.isCreationPending()) || (kind !== 'start' && kind !== 'direction')) {
+    if (!geometry) {
       return null;
     }
 
-    const coordinate = this.getPriceCoordinate(kind);
-    const text = this.getPriceText(kind);
+    const { startPoint } = geometry;
+    const directionPoint = geometry.endPoint;
+    const rayEndPoint = this.getRayEndPoint(startPoint, directionPoint);
 
-    if (coordinate === null || !text) {
+    if (!rayEndPoint) {
       return null;
     }
 
-    const { colors } = getThemeStore();
-
     return {
-      coordinate,
-      text,
-      textColor: colors.chartPriceLineText,
-      backgroundColor: colors.axisMarkerLabelFill,
+      startPoint,
+      directionPoint,
+      rayEndPoint,
+      left: Math.min(startPoint.x, rayEndPoint.x),
+      right: Math.max(startPoint.x, rayEndPoint.x),
+      top: Math.min(startPoint.y, rayEndPoint.y),
+      bottom: Math.max(startPoint.y, rayEndPoint.y),
     };
   }
 
   protected handleDoubleClick = (event: MouseEvent): void => {
-    if (this.hidden || this.mode !== 'ready') {
-      return;
-    }
-
-    const point = this.getEventPoint(event as PointerEvent);
-
-    if (!this.getDrawingHandleAtPoint(point) && !this.isPointNearRay(point)) {
-      return;
-    }
-
-    event.preventDefault();
-    event.stopPropagation();
-
-    this.openSettings?.();
+    this.openSettingsOnDoubleClick(
+      event,
+      (point) => this.getDrawingHandleAtPoint(point) !== null || this.isPointNearRay(point),
+      () => this.openSettings?.(),
+    );
   };
 
-  protected handlePointerDown = (event: PointerEvent): void => {
-    if (this.hidden || event.button !== 0) {
-      return;
-    }
-
-    const point = this.getEventPoint(event);
-
-    if (this.mode === 'idle') {
-      event.preventDefault();
-      event.stopPropagation();
-
-      this.startDrawing(point);
-      return;
-    }
-
-    if (this.mode === 'drawing') {
-      event.preventDefault();
-      event.stopPropagation();
-
-      this.updateDrawing(point);
-      this.finishDrawing();
-      return;
-    }
-
-    if (this.mode !== 'ready') {
-      return;
-    }
-
+  protected handleReadyPointerDown(event: PointerEvent, point: Point): void {
     const pointTarget = this.getDrawingHandleAtPoint(point)?.id ?? null;
     const isNearRay = this.isPointNearRay(point);
     const isDrawingHit = pointTarget !== null || isNearRay;
@@ -437,39 +276,34 @@ export class Ray extends SeriesDrawingBase<RaySettings, RayHandleKey> implements
         return;
       }
 
-      event.preventDefault();
-      event.stopPropagation();
-
+      this.consumeEvent(event);
       this.select();
       return;
     }
 
     if (pointTarget === 'start') {
-      event.preventDefault();
-      event.stopPropagation();
-
+      this.consumeEvent(event);
       this.startDragging('dragging-start', point, event.pointerId);
+
       return;
     }
 
     if (pointTarget === 'direction') {
-      event.preventDefault();
-      event.stopPropagation();
-
+      this.consumeEvent(event);
       this.startDragging('dragging-direction', point, event.pointerId);
+
       return;
     }
 
     if (isNearRay) {
-      event.preventDefault();
-      event.stopPropagation();
-
+      this.consumeEvent(event);
       this.startDragging('dragging-body', point, event.pointerId);
+
       return;
     }
 
     this.deselect();
-  };
+  }
 
   protected handlePointerMove = (event: PointerEvent): void => {
     const point = this.getEventPoint(event);
@@ -484,18 +318,15 @@ export class Ray extends SeriesDrawingBase<RaySettings, RayHandleKey> implements
     }
 
     if (this.mode === 'dragging-start' || this.mode === 'dragging-direction') {
-      event.preventDefault();
-      event.stopPropagation();
-
+      this.consumeEvent(event);
       this.movePoint(point);
       this.render();
+
       return;
     }
 
     if (this.mode === 'dragging-body') {
-      event.preventDefault();
-      event.stopPropagation();
-
+      this.consumeEvent(event);
       this.moveBody(point);
       this.render();
     }
@@ -511,163 +342,30 @@ export class Ray extends SeriesDrawingBase<RaySettings, RayHandleKey> implements
     }
   };
 
-  private startDrawing(point: Point): void {
-    const anchor = this.createAnchor(point);
-
-    if (!anchor) {
-      return;
-    }
-
-    this.startAnchor = anchor;
-    this.directionAnchor = anchor;
-    this.mode = 'drawing';
-
-    this.render();
-  }
-
-  private updateDrawing(point: Point): void {
-    const anchor = this.createAnchor(point);
-
-    if (!anchor) {
-      return;
-    }
-
-    this.directionAnchor = anchor;
-    this.render();
-  }
-
-  private finishDrawing(): void {
-    const geometry = this.getGeometry();
-
-    if (!geometry) {
-      return;
-    }
-
-    const lineSize = Math.hypot(
-      geometry.directionPoint.x - geometry.startPoint.x,
-      geometry.directionPoint.y - geometry.startPoint.y,
-    );
-
-    if (lineSize < MIN_LINE_SIZE) {
-      this.removeSelf?.();
-      return;
-    }
-
-    this.mode = 'ready';
-    this.resolveReady?.();
-
-    this.render();
-  }
-
-  private startDragging(mode: RayMode, point: Point, pointerId: number): void {
-    this.mode = mode;
-    this.dragPointerId = pointerId;
-    this.dragStartPoint = point;
-    this.dragStateSnapshot = this.getState();
-
-    this.hideCrosshair();
-    this.render();
-  }
-
-  private finishDragging(): void {
-    this.mode = 'ready';
-    this.resolveReady?.();
-
-    this.dragPointerId = null;
-    this.dragStartPoint = null;
-    this.dragStateSnapshot = null;
-
-    this.showCrosshair();
-    this.render();
+  protected finishDrawing(): void {
+    this.finishLinearDrawing(this.getTwoPointGeometry(), MIN_LINE_SIZE, () => this.removeSelf?.());
   }
 
   private movePoint(point: Point): void {
-    const anchor = this.createAnchor(point);
+    if (this.mode === 'dragging-start') {
+      this.setAnchorFromPoint('start', point);
 
-    if (!anchor) {
       return;
     }
 
-    if (this.mode === 'dragging-start') {
-      this.startAnchor = anchor;
-    }
-
     if (this.mode === 'dragging-direction') {
-      this.directionAnchor = anchor;
+      this.setAnchorFromPoint('end', point);
     }
   }
 
   private moveBody(point: Point): void {
     const snapshot = this.dragStateSnapshot;
 
-    if (!snapshot?.startAnchor || !snapshot.directionAnchor || !this.dragStartPoint) {
+    if (!snapshot) {
       return;
     }
 
-    const offsetX = point.x - this.dragStartPoint.x;
-    const priceOffset = getPriceDeltaFromCoordinates(this.series, this.dragStartPoint.y, point.y);
-
-    const nextStartTime = shiftTimeByPixels(this.chart, snapshot.startAnchor.time, offsetX, this.series);
-    const nextDirectionTime = shiftTimeByPixels(this.chart, snapshot.directionAnchor.time, offsetX, this.series);
-
-    if (nextStartTime === null || nextDirectionTime === null) {
-      return;
-    }
-
-    this.startAnchor = {
-      time: nextStartTime,
-      price: snapshot.startAnchor.price + priceOffset,
-    };
-
-    this.directionAnchor = {
-      time: nextDirectionTime,
-      price: snapshot.directionAnchor.price + priceOffset,
-    };
-  }
-
-  private createAnchor(point: Point): Anchor | null {
-    return getAnchorFromPoint(this.chart, this.series, point);
-  }
-
-  protected getGeometry(): RayGeometry | null {
-    if (!this.startAnchor || !this.directionAnchor) {
-      return null;
-    }
-
-    const startX = getXCoordinateFromTime(this.chart, this.startAnchor.time, this.series);
-    const directionX = getXCoordinateFromTime(this.chart, this.directionAnchor.time, this.series);
-    const startY = getYCoordinateFromPrice(this.series, this.startAnchor.price);
-    const directionY = getYCoordinateFromPrice(this.series, this.directionAnchor.price);
-
-    if (startX === null || directionX === null || startY === null || directionY === null) {
-      return null;
-    }
-
-    const startPoint = {
-      x: Math.round(Number(startX)),
-      y: Math.round(Number(startY)),
-    };
-
-    const directionPoint = {
-      x: Math.round(Number(directionX)),
-      y: Math.round(Number(directionY)),
-    };
-
-    const rayEndPoint = this.getRayEndPoint(startPoint, directionPoint);
-
-    if (!rayEndPoint) {
-      return null;
-    }
-
-    return {
-      startPoint,
-      directionPoint,
-      rayEndPoint,
-      left: Math.min(startPoint.x, rayEndPoint.x),
-      right: Math.max(startPoint.x, rayEndPoint.x),
-      top: Math.min(startPoint.y, rayEndPoint.y),
-      bottom: Math.max(startPoint.y, rayEndPoint.y),
-    };
+    this.moveAnchorsByPixels(snapshot.startAnchor, snapshot.directionAnchor, this.dragStartPoint, point);
   }
 
   private getRayEndPoint(startPoint: Point, directionPoint: Point): Point | null {
@@ -678,42 +376,24 @@ export class Ray extends SeriesDrawingBase<RaySettings, RayHandleKey> implements
       return null;
     }
 
-    const { width, height } = this.container.getBoundingClientRect();
-    const candidates: Point[] = [];
-
-    if (dx !== 0) {
-      const leftT = (0 - startPoint.x) / dx;
-      const rightT = (width - startPoint.x) / dx;
-
-      const leftY = startPoint.y + leftT * dy;
-      const rightY = startPoint.y + rightT * dy;
-
-      if (leftT >= 1 && leftY >= 0 && leftY <= height) {
-        candidates.push({ x: 0, y: leftY });
-      }
+    const { width } = this.container.getBoundingClientRect();
+    const height = this.series.getPane().getHeight();
 
-      if (rightT >= 1 && rightY >= 0 && rightY <= height) {
-        candidates.push({ x: width, y: rightY });
-      }
+    if (width <= 0 || height <= 0) {
+      return null;
     }
 
-    if (dy !== 0) {
-      const topT = (0 - startPoint.y) / dy;
-      const bottomT = (height - startPoint.y) / dy;
-
-      const topX = startPoint.x + topT * dx;
-      const bottomX = startPoint.x + bottomT * dx;
-
-      if (topT >= 1 && topX >= 0 && topX <= width) {
-        candidates.push({ x: topX, y: 0 });
-      }
+    const candidates: RayIntersection[] = [];
+    this.addVerticalRayIntersections(candidates, startPoint, dx, dy, width, height);
+    this.addHorizontalRayIntersections(candidates, startPoint, dx, dy, width, height);
 
-      if (bottomT >= 1 && bottomX >= 0 && bottomX <= width) {
-        candidates.push({ x: bottomX, y: height });
-      }
+    if (!candidates.length) {
+      return null;
     }
 
-    return candidates[0] ?? directionPoint;
+    return candidates.reduce((farthest, candidate) => {
+      return candidate.t > farthest.t ? candidate : farthest;
+    }).point;
   }
 
   private isPointNearRay(point: Point): boolean {
@@ -726,30 +406,6 @@ export class Ray extends SeriesDrawingBase<RaySettings, RayHandleKey> implements
     return getDistanceToSegment(point, geometry.startPoint, geometry.rayEndPoint) <= LINE_HIT_TOLERANCE;
   }
 
-  private getTimeCoordinate(kind: TimeLabelKind): number | null {
-    const anchor = kind === 'start' ? this.startAnchor : this.directionAnchor;
-
-    if (!anchor) {
-      return null;
-    }
-
-    const coordinate = getXCoordinateFromTime(this.chart, anchor.time, this.series);
-
-    return coordinate === null ? null : Number(coordinate);
-  }
-
-  private getPriceCoordinate(kind: PriceLabelKind): number | null {
-    const anchor = kind === 'start' ? this.startAnchor : this.directionAnchor;
-
-    if (!anchor) {
-      return null;
-    }
-
-    const coordinate = getYCoordinateFromPrice(this.series, anchor.price);
-
-    return coordinate === null ? null : Number(coordinate);
-  }
-
   private getTimeText(kind: TimeLabelKind): string {
     const anchor = kind === 'start' ? this.startAnchor : this.directionAnchor;
 
@@ -774,4 +430,76 @@ export class Ray extends SeriesDrawingBase<RaySettings, RayHandleKey> implements
 
     return formatPrice(anchor.price) ?? '';
   }
+
+  private addVerticalRayIntersections(
+    candidates: RayIntersection[],
+    startPoint: Point,
+    dx: number,
+    dy: number,
+    width: number,
+    height: number,
+  ): void {
+    if (dx !== 0) {
+      const leftT = -startPoint.x / dx;
+      const rightT = (width - startPoint.x) / dx;
+      const leftY = startPoint.y + leftT * dy;
+      const rightY = startPoint.y + rightT * dy;
+
+      if (leftT >= 0 && leftY >= 0 && leftY <= height) {
+        candidates.push({
+          point: {
+            x: 0,
+            y: leftY,
+          },
+          t: leftT,
+        });
+      }
+
+      if (rightT >= 0 && rightY >= 0 && rightY <= height) {
+        candidates.push({
+          point: {
+            x: width,
+            y: rightY,
+          },
+          t: rightT,
+        });
+      }
+    }
+  }
+
+  private addHorizontalRayIntersections(
+    candidates: RayIntersection[],
+    startPoint: Point,
+    dx: number,
+    dy: number,
+    width: number,
+    height: number,
+  ): void {
+    if (dy !== 0) {
+      const topT = -startPoint.y / dy;
+      const bottomT = (height - startPoint.y) / dy;
+      const topX = startPoint.x + topT * dx;
+      const bottomX = startPoint.x + bottomT * dx;
+
+      if (topT >= 0 && topX >= 0 && topX <= width) {
+        candidates.push({
+          point: {
+            x: topX,
+            y: 0,
+          },
+          t: topT,
+        });
+      }
+
+      if (bottomT >= 0 && bottomX >= 0 && bottomX <= width) {
+        candidates.push({
+          point: {
+            x: bottomX,
+            y: height,
+          },
+          t: bottomT,
+        });
+      }
+    }
+  }
 }
diff --git a/src/core/Drawings/rectangle/rectangle.ts b/src/core/Drawings/rectangle/rectangle.ts
index 82bb27a9708986410b73683dbb7bd6ea1c2a86bb..9c80af45d796d4baad9c0700cf85b0ee775f98fb 100644
--- a/src/core/Drawings/rectangle/rectangle.ts
+++ b/src/core/Drawings/rectangle/rectangle.ts
@@ -1,31 +1,10 @@
-import { clamp } from 'lodash-es';
+import { PrimitiveHoveredItem, Time, UTCTimestamp } from 'lightweight-charts';
 
-import {
-  CustomPriceAxisPaneView,
-  CustomPriceAxisView,
-  CustomTimeAxisPaneView,
-  CustomTimeAxisView,
-} from '@core/Drawings/axis';
-import {
-  clampPointToContainer as clampPointToContainerInElement,
-  getAnchorFromPoint,
-  getContainerSize as getElementContainerSize,
-  getPriceDelta as getPriceDeltaFromCoordinates,
-  getPriceFromYCoordinate,
-  getTimeFromXCoordinate,
-  getXCoordinateFromTime,
-  getYCoordinateFromPrice,
-  isPointInBounds,
-  normalizeBounds,
-  shiftTimeByPixels,
-} from '@core/Drawings/helpers';
-import { updateViews } from '@core/Drawings/utils';
-import { SeriesDrawingBase } from '@src/core/Drawings/SeriesDrawingBase';
-
-import { getThemeStore } from '@src/theme';
-import { Defaults } from '@src/types/defaults';
-import { formatPrice } from '@src/utils';
-import { formatDate } from '@src/utils/formatter';
+import { isPointInBounds, normalizeBounds } from '@core/Drawings/helpers';
+
+import { AreaDrawingBase, BaseDrawingParams, ISeriesDrawing } from '@src/core/Drawings/base';
+import { type ChartOptionsModel, Defaults, type SettingsTab } from '@src/types';
+import { formatDate, formatPrice } from '@src/utils';
 
 import { RectanglePaneView } from './paneView';
 import {
@@ -37,10 +16,7 @@ import {
 } from './settings';
 
 import type { DrawingHandle } from '@core/Drawings/handles';
-import type { AxisLabel, AxisSegment, Point } from '@core/Drawings/types';
-import type { BaseDrawingParams, ISeriesDrawing } from '@src/core/Drawings/SeriesDrawingBase';
-import type { ChartOptionsModel, SettingsTab } from '@src/types';
-import type { IPrimitivePaneView, PrimitiveHoveredItem, Time, UTCTimestamp } from 'lightweight-charts';
+import type { Anchor, AxisLabel, Bounds, Point } from '@core/Drawings/types';
 
 type RectangleMode = 'idle' | 'drawing' | 'ready' | 'dragging';
 type RectangleHandle = 'body' | 'nw' | 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w' | null;
@@ -60,11 +36,12 @@ interface RectangleState {
   settings: RectangleSettings;
 }
 
-interface RectangleGeometry {
-  left: number;
-  right: number;
-  top: number;
-  bottom: number;
+interface RectangleDragSnapshot {
+  startAnchor: Anchor | null;
+  endAnchor: Anchor | null;
+}
+
+interface RectangleGeometry extends Bounds {
   width: number;
   height: number;
 }
@@ -74,22 +51,17 @@ export type RectangleRenderData = RectangleGeometry & RectangleStyle & Rectangle
 const BODY_HIT_TOLERANCE = 6;
 const MIN_RECTANGLE_SIZE = 6;
 
-export class Rectangle extends SeriesDrawingBase<RectangleSettings, RectangleHandleKey> implements ISeriesDrawing {
+export class Rectangle extends AreaDrawingBase<RectangleSettings, RectangleHandleKey> implements ISeriesDrawing {
   private removeSelf?: () => void;
   private openSettings?: () => void;
 
   protected settings: RectangleSettings = createDefaultSettings();
   protected mode: RectangleMode = 'idle';
 
-  private startTime: Time | null = null;
-  private endTime: Time | null = null;
-  private startPrice: number | null = null;
-  private endPrice: number | null = null;
-
-  private activeDragTarget: RectangleHandle = null;
-  private dragPointerId: number | null = null;
+  protected activeDragTarget: RectangleHandle = null;
+  protected dragPointerId: number | null = null;
   private dragStartPoint: Point | null = null;
-  private dragStateSnapshot: RectangleState | null = null;
+  private dragAnchorSnapshot: RectangleDragSnapshot | null = null;
   private dragGeometrySnapshot: RectangleGeometry | null = null;
 
   private displayFormat: ChartOptionsModel = {
@@ -98,14 +70,6 @@ export class Rectangle extends SeriesDrawingBase<RectangleSettings, RectangleHan
     showTime: Defaults.showTime,
   };
 
-  private readonly paneView: RectanglePaneView;
-  private readonly timeAxisPaneView: CustomTimeAxisPaneView;
-  private readonly priceAxisPaneView: CustomPriceAxisPaneView;
-  private readonly leftTimeAxisView: CustomTimeAxisView;
-  private readonly rightTimeAxisView: CustomTimeAxisView;
-  private readonly topPriceAxisView: CustomPriceAxisView;
-  private readonly bottomPriceAxisView: CustomPriceAxisView;
-
   constructor({
     chart,
     series,
@@ -120,51 +84,16 @@ export class Rectangle extends SeriesDrawingBase<RectangleSettings, RectangleHan
 
     this.removeSelf = removeSelf;
     this.openSettings = openSettings;
+    this.initializeViews(new RectanglePaneView(this), ['left', 'right'], ['top', 'bottom']);
 
-    this.paneView = new RectanglePaneView(this);
-
-    this.timeAxisPaneView = new CustomTimeAxisPaneView({
-      getAxisSegments: () => this.getTimeAxisSegments(),
-    });
-
-    this.priceAxisPaneView = new CustomPriceAxisPaneView({
-      getAxisSegments: () => this.getPriceAxisSegments(),
-    });
-
-    this.leftTimeAxisView = new CustomTimeAxisView({
-      getAxisLabel: (kind) => this.getTimeAxisLabel(kind),
-      labelKind: 'left',
-    });
-
-    this.rightTimeAxisView = new CustomTimeAxisView({
-      getAxisLabel: (kind) => this.getTimeAxisLabel(kind),
-      labelKind: 'right',
-    });
-
-    this.topPriceAxisView = new CustomPriceAxisView({
-      getAxisLabel: (kind) => this.getPriceAxisLabel(kind),
-      labelKind: 'top',
+    this.subscribeFormat(formatObservable, (format) => {
+      this.displayFormat = format;
     });
 
-    this.bottomPriceAxisView = new CustomPriceAxisView({
-      getAxisLabel: (kind) => this.getPriceAxisLabel(kind),
-      labelKind: 'bottom',
-    });
-
-    if (formatObservable) {
-      this.subscriptions.add(
-        formatObservable.subscribe((format) => {
-          this.displayFormat = format;
-          this.render();
-        }),
-      );
-    }
-
     this.series.attachPrimitive(this);
 
-    if (initialEvent && initialEvent.sourceEvent) {
-      const point = this.getEventPoint(initialEvent.sourceEvent);
-      this.startDrawing(point);
+    if (initialEvent?.sourceEvent) {
+      this.startDrawing(this.getEventPoint(initialEvent.sourceEvent));
     }
   }
 
@@ -176,15 +105,19 @@ export class Rectangle extends SeriesDrawingBase<RectangleSettings, RectangleHan
     return {
       hidden: this.hidden,
       mode: this.mode,
-      startTime: this.startTime,
-      endTime: this.endTime,
-      startPrice: this.startPrice,
-      endPrice: this.endPrice,
+      startTime: this.startAnchor?.time ?? null,
+      endTime: this.endAnchor?.time ?? null,
+      startPrice: this.startAnchor?.price ?? null,
+      endPrice: this.endAnchor?.price ?? null,
       settings: { ...this.settings },
     };
   }
 
   public setState(state: unknown): void {
+    if (!state || typeof state !== 'object') {
+      return;
+    }
+
     const nextState = state as Partial<RectangleState>;
 
     if ('hidden' in nextState && typeof nextState.hidden === 'boolean') {
@@ -195,21 +128,8 @@ export class Rectangle extends SeriesDrawingBase<RectangleSettings, RectangleHan
       this.mode = nextState.mode;
     }
 
-    if ('startTime' in nextState) {
-      this.startTime = nextState.startTime ?? null;
-    }
-
-    if ('endTime' in nextState) {
-      this.endTime = nextState.endTime ?? null;
-    }
-
-    if ('startPrice' in nextState) {
-      this.startPrice = nextState.startPrice ?? null;
-    }
-
-    if ('endPrice' in nextState) {
-      this.endPrice = nextState.endPrice ?? null;
-    }
+    this.restoreAnchor('start', nextState);
+    this.restoreAnchor('end', nextState);
 
     if ('settings' in nextState && nextState.settings) {
       this.settings = {
@@ -225,38 +145,6 @@ export class Rectangle extends SeriesDrawingBase<RectangleSettings, RectangleHan
     return getRectangleSettingsTabs(this.settings);
   }
 
-  public updateAllViews(): void {
-    updateViews([
-      this.paneView,
-      this.timeAxisPaneView,
-      this.priceAxisPaneView,
-      this.leftTimeAxisView,
-      this.rightTimeAxisView,
-      this.topPriceAxisView,
-      this.bottomPriceAxisView,
-    ]);
-  }
-
-  public paneViews(): readonly IPrimitivePaneView[] {
-    return [this.paneView];
-  }
-
-  public timeAxisPaneViews(): readonly IPrimitivePaneView[] {
-    return [this.timeAxisPaneView];
-  }
-
-  public priceAxisPaneViews(): readonly IPrimitivePaneView[] {
-    return [this.priceAxisPaneView];
-  }
-
-  public timeAxisViews() {
-    return [this.leftTimeAxisView, this.rightTimeAxisView];
-  }
-
-  public priceAxisViews() {
-    return [this.topPriceAxisView, this.bottomPriceAxisView];
-  }
-
   public getRenderData(): RectangleRenderData | null {
     if (this.hidden) {
       return null;
@@ -337,188 +225,37 @@ export class Rectangle extends SeriesDrawingBase<RectangleSettings, RectangleHan
     };
   }
 
-  protected getTimeAxisSegments(): AxisSegment[] {
-    if (!this.isSelected() && !this.isCreationPending()) {
-      return [];
-    }
-
-    const bounds = this.getTimeBounds();
-
-    if (!bounds) {
-      return [];
-    }
-
-    const { colors } = getThemeStore();
-
-    return [
-      {
-        from: bounds.left,
-        to: bounds.right,
-        color: colors.axisMarkerAreaFill,
-      },
-    ];
-  }
-
-  protected getPriceAxisSegments(): AxisSegment[] {
-    if (!this.isSelected() && !this.isCreationPending()) {
-      return [];
-    }
-
-    const bounds = this.getPriceBounds();
-
-    if (!bounds) {
-      return [];
-    }
-
-    const { colors } = getThemeStore();
-
-    return [
-      {
-        from: bounds.top,
-        to: bounds.bottom,
-        color: colors.axisMarkerAreaFill,
-      },
-    ];
-  }
-
   protected getTimeAxisLabel(kind: string): AxisLabel | null {
-    if ((!this.isSelected() && !this.isCreationPending()) || (kind !== 'left' && kind !== 'right')) {
-      return null;
-    }
-
-    const labelKind = kind as TimeLabelKind;
-    const coordinate = this.getTimeCoordinate(labelKind);
-    const text = this.getTimeText(labelKind);
-
-    if (coordinate === null || !text) {
-      return null;
-    }
-
-    const { colors } = getThemeStore();
-
-    return {
-      coordinate,
-      text,
-      textColor: colors.chartPriceLineText,
-      backgroundColor: colors.axisMarkerLabelFill,
-    };
+    return this.getBoundsAxisLabel(kind, ['left', 'right'], (labelKind) => this.getTimeText(labelKind));
   }
 
   protected getPriceAxisLabel(kind: string): AxisLabel | null {
-    if ((!this.isSelected() && !this.isCreationPending()) || (kind !== 'top' && kind !== 'bottom')) {
-      return null;
-    }
+    return this.getBoundsAxisLabel(kind, ['top', 'bottom'], (labelKind) => this.getPriceText(labelKind));
+  }
 
-    const labelKind = kind as PriceLabelKind;
-    const coordinate = this.getPriceCoordinate(labelKind);
-    const text = this.getPriceText(labelKind);
+  protected getGeometry(): RectangleGeometry | null {
+    const geometry = this.getTwoPointGeometry();
 
-    if (coordinate === null || !text) {
+    if (!geometry) {
       return null;
     }
 
-    const { colors } = getThemeStore();
-
     return {
-      coordinate,
-      text,
-      textColor: colors.chartPriceLineText,
-      backgroundColor: colors.axisMarkerLabelFill,
+      left: geometry.left,
+      right: geometry.right,
+      top: geometry.top,
+      bottom: geometry.bottom,
+      width: geometry.right - geometry.left,
+      height: geometry.bottom - geometry.top,
     };
   }
 
   protected handleDoubleClick = (event: MouseEvent): void => {
-    if (this.hidden || this.mode !== 'ready') {
-      return;
-    }
-
-    const point = this.getEventPoint(event as PointerEvent);
-
-    if (!this.containsPoint(point) && !this.getDrawingHandleAtPoint(point)) {
-      return;
-    }
-
-    event.preventDefault();
-    event.stopPropagation();
-
-    this.openSettings?.();
-  };
-
-  protected handlePointerDown = (event: PointerEvent): void => {
-    if (this.hidden || event.button !== 0) {
-      return;
-    }
-
-    const point = this.getEventPoint(event);
-
-    if (this.mode === 'idle') {
-      event.preventDefault();
-      event.stopPropagation();
-
-      this.startDrawing(point);
-      return;
-    }
-
-    if (this.mode === 'drawing') {
-      event.preventDefault();
-      event.stopPropagation();
-
-      this.updateDrawing(point);
-      this.finishDrawing();
-      return;
-    }
-
-    if (this.mode !== 'ready') {
-      return;
-    }
-
-    if (!this.isSelected()) {
-      if (!this.containsPoint(point)) {
-        return;
-      }
-
-      event.preventDefault();
-      event.stopPropagation();
-
-      this.select();
-      return;
-    }
-
-    const dragTarget = this.getDragTarget(point);
-
-    if (!dragTarget) {
-      this.deselect();
-      return;
-    }
-
-    event.preventDefault();
-    event.stopPropagation();
-
-    this.startDragging(point, event.pointerId, dragTarget);
-  };
-
-  protected handlePointerMove = (event: PointerEvent): void => {
-    const point = this.getEventPoint(event);
-
-    if (this.mode === 'drawing') {
-      this.updateDrawing(point);
-      return;
-    }
-
-    if (this.mode !== 'dragging' || this.dragPointerId !== event.pointerId) {
-      return;
-    }
-
-    event.preventDefault();
-
-    if (this.activeDragTarget === 'body') {
-      this.moveWhole(point);
-      this.render();
-      return;
-    }
-
-    this.resizeRectangle(point);
-    this.render();
+    this.openSettingsOnDoubleClick(
+      event,
+      (point) => Boolean(this.containsPoint(point) || this.getDrawingHandleAtPoint(point)),
+      () => this.openSettings?.(),
+    );
   };
 
   protected handlePointerUp = (event: PointerEvent): void => {
@@ -529,37 +266,7 @@ export class Rectangle extends SeriesDrawingBase<RectangleSettings, RectangleHan
     this.finishDragging();
   };
 
-  private startDrawing(point: Point): void {
-    const anchor = this.createAnchor(point);
-
-    if (!anchor) {
-      return;
-    }
-
-    this.startTime = anchor.time;
-    this.endTime = anchor.time;
-    this.startPrice = anchor.price;
-    this.endPrice = anchor.price;
-    this.mode = 'drawing';
-
-    this.render();
-  }
-
-  private updateDrawing(point: Point): void {
-    const clampedPoint = this.clampPointToContainer(point);
-    const anchor = this.createAnchor(clampedPoint);
-
-    if (!anchor) {
-      return;
-    }
-
-    this.endTime = anchor.time;
-    this.endPrice = anchor.price;
-
-    this.render();
-  }
-
-  private finishDrawing(): void {
+  protected finishDrawing(): void {
     const geometry = this.getGeometry();
 
     if (!geometry || geometry.width < MIN_RECTANGLE_SIZE || geometry.height < MIN_RECTANGLE_SIZE) {
@@ -574,25 +281,25 @@ export class Rectangle extends SeriesDrawingBase<RectangleSettings, RectangleHan
 
     this.mode = 'ready';
     this.resolveReady?.();
-
     this.render();
   }
 
-  private startDragging(point: Point, pointerId: number, dragTarget: Exclude<RectangleHandle, null>): void {
+  protected startDragging(point: Point, pointerId: number, dragTarget: Exclude<RectangleHandle, null>): void {
     this.mode = 'dragging';
     this.activeDragTarget = dragTarget;
     this.dragPointerId = pointerId;
     this.dragStartPoint = point;
-    this.dragStateSnapshot = this.getState();
+    this.dragAnchorSnapshot = {
+      startAnchor: this.startAnchor,
+      endAnchor: this.endAnchor,
+    };
     this.dragGeometrySnapshot = this.getGeometry();
-
     this.render();
   }
 
   private finishDragging(): void {
     this.mode = 'ready';
     this.resolveReady?.();
-
     this.clearInteractionState();
     this.render();
   }
@@ -601,82 +308,40 @@ export class Rectangle extends SeriesDrawingBase<RectangleSettings, RectangleHan
     this.activeDragTarget = null;
     this.dragPointerId = null;
     this.dragStartPoint = null;
-    this.dragStateSnapshot = null;
+    this.dragAnchorSnapshot = null;
     this.dragGeometrySnapshot = null;
   }
 
   private resetToIdle(): void {
     this.hidden = false;
     this.mode = 'idle';
-
-    this.startTime = null;
-    this.endTime = null;
-    this.startPrice = null;
-    this.endPrice = null;
-
+    this.setAnchors(null, null);
     this.clearInteractionState();
     this.render();
   }
 
-  private getDragTarget(point: Point): Exclude<RectangleHandle, null> | null {
+  protected getDragTarget(point: Point): Exclude<RectangleHandle, null> | null {
     const handleTarget = this.getDrawingHandleAtPoint(point)?.id;
 
     if (handleTarget) {
       return handleTarget;
     }
 
-    if (this.containsPoint(point)) {
-      return 'body';
-    }
-
-    return null;
+    return this.containsPoint(point) ? 'body' : null;
   }
 
-  private moveWhole(point: Point): void {
-    const snapshot = this.dragStateSnapshot;
+  protected moveWhole(point: Point): void {
+    const snapshot = this.dragAnchorSnapshot;
     const geometry = this.dragGeometrySnapshot;
 
-    if (!snapshot || !geometry || !this.dragStartPoint) {
-      return;
-    }
-
-    if (
-      snapshot.startTime === null ||
-      snapshot.endTime === null ||
-      snapshot.startPrice === null ||
-      snapshot.endPrice === null
-    ) {
+    if (!snapshot || !geometry) {
       return;
     }
 
-    const containerSize = this.getContainerSize();
-    const rawOffsetX = point.x - this.dragStartPoint.x;
-    const rawOffsetY = point.y - this.dragStartPoint.y;
-
-    const minOffsetX = -geometry.left;
-    const maxOffsetX = containerSize.width - geometry.right;
-    const clampedOffsetX = clamp(rawOffsetX, minOffsetX, maxOffsetX);
-
-    const minOffsetY = -geometry.top;
-    const maxOffsetY = containerSize.height - geometry.bottom;
-    const clampedOffsetY = clamp(rawOffsetY, minOffsetY, maxOffsetY);
-
-    const nextStartTime = this.shiftTime(snapshot.startTime, clampedOffsetX);
-    const nextEndTime = this.shiftTime(snapshot.endTime, clampedOffsetX);
-
-    if (nextStartTime === null || nextEndTime === null) {
-      return;
-    }
-
-    const priceOffset = this.getPriceDelta(this.dragStartPoint.y, this.dragStartPoint.y + clampedOffsetY);
-
-    this.startTime = nextStartTime;
-    this.endTime = nextEndTime;
-    this.startPrice = snapshot.startPrice + priceOffset;
-    this.endPrice = snapshot.endPrice + priceOffset;
+    this.moveAnchorsWithinContainer(snapshot.startAnchor, snapshot.endAnchor, this.dragStartPoint, point, geometry);
   }
 
-  private resizeRectangle(point: Point): void {
+  protected resize(point: Point): void {
     const geometry = this.dragGeometrySnapshot;
 
     if (!geometry || !this.activeDragTarget || this.activeDragTarget === 'body') {
@@ -684,11 +349,7 @@ export class Rectangle extends SeriesDrawingBase<RectangleSettings, RectangleHan
     }
 
     const clampedPoint = this.clampPointToContainer(point);
-
-    let { left } = geometry;
-    let { right } = geometry;
-    let { top } = geometry;
-    let { bottom } = geometry;
+    let { left, right, top, bottom } = geometry;
 
     switch (this.activeDragTarget) {
       case 'nw':
@@ -729,100 +390,7 @@ export class Rectangle extends SeriesDrawingBase<RectangleSettings, RectangleHan
   private setRectangleBounds(left: number, right: number, top: number, bottom: number): boolean {
     const bounds = normalizeBounds(left, right, top, bottom, this.container);
 
-    const startTime = getTimeFromXCoordinate(this.chart, bounds.left);
-    const endTime = getTimeFromXCoordinate(this.chart, bounds.right);
-    const startPrice = getPriceFromYCoordinate(this.series, bounds.top);
-    const endPrice = getPriceFromYCoordinate(this.series, bounds.bottom);
-
-    if (startTime === null || endTime === null || startPrice === null || endPrice === null) {
-      return false;
-    }
-
-    this.startTime = startTime;
-    this.endTime = endTime;
-    this.startPrice = startPrice;
-    this.endPrice = endPrice;
-
-    return true;
-  }
-
-  private createAnchor(point: Point): { time: Time; price: number } | null {
-    return getAnchorFromPoint(this.chart, this.series, point);
-  }
-
-  protected getGeometry(): RectangleGeometry | null {
-    if (this.startTime === null || this.endTime === null || this.startPrice === null || this.endPrice === null) {
-      return null;
-    }
-
-    const startX = getXCoordinateFromTime(this.chart, this.startTime, this.series);
-    const endX = getXCoordinateFromTime(this.chart, this.endTime, this.series);
-    const startY = getYCoordinateFromPrice(this.series, this.startPrice);
-    const endY = getYCoordinateFromPrice(this.series, this.endPrice);
-
-    if (startX === null || endX === null || startY === null || endY === null) {
-      return null;
-    }
-
-    const left = Math.round(Math.min(Number(startX), Number(endX)));
-    const right = Math.round(Math.max(Number(startX), Number(endX)));
-    const top = Math.round(Math.min(Number(startY), Number(endY)));
-    const bottom = Math.round(Math.max(Number(startY), Number(endY)));
-
-    return {
-      left,
-      right,
-      top,
-      bottom,
-      width: right - left,
-      height: bottom - top,
-    };
-  }
-
-  private getTimeBounds(): { left: number; right: number } | null {
-    const geometry = this.getGeometry();
-
-    if (!geometry) {
-      return null;
-    }
-
-    return {
-      left: geometry.left,
-      right: geometry.right,
-    };
-  }
-
-  private getPriceBounds(): { top: number; bottom: number } | null {
-    const geometry = this.getGeometry();
-
-    if (!geometry) {
-      return null;
-    }
-
-    return {
-      top: geometry.top,
-      bottom: geometry.bottom,
-    };
-  }
-
-  private getTimeCoordinate(kind: TimeLabelKind): number | null {
-    const geometry = this.getGeometry();
-
-    if (!geometry) {
-      return null;
-    }
-
-    return kind === 'left' ? geometry.left : geometry.right;
-  }
-
-  private getPriceCoordinate(kind: PriceLabelKind): number | null {
-    const geometry = this.getGeometry();
-
-    if (!geometry) {
-      return null;
-    }
-
-    return kind === 'top' ? geometry.top : geometry.bottom;
+    return this.setAnchorsFromPoints({ x: bounds.left, y: bounds.top }, { x: bounds.right, y: bounds.bottom });
   }
 
   private getTimeText(kind: TimeLabelKind): string {
@@ -843,63 +411,55 @@ export class Rectangle extends SeriesDrawingBase<RectangleSettings, RectangleHan
   private getPriceText(kind: PriceLabelKind): string {
     const price = this.getPriceValueForLabel(kind);
 
-    if (price === null) {
-      return '';
-    }
-
-    return formatPrice(price) ?? '';
+    return price === null ? '' : (formatPrice(price) ?? '');
   }
 
   private getTimeValueForLabel(kind: TimeLabelKind): Time | null {
-    if (this.startTime === null || this.endTime === null) {
+    if (!this.startAnchor || !this.endAnchor) {
       return null;
     }
 
-    const startX = getXCoordinateFromTime(this.chart, this.startTime, this.series);
-    const endX = getXCoordinateFromTime(this.chart, this.endTime, this.series);
+    const geometry = this.getTwoPointGeometry();
 
-    if (startX === null || endX === null) {
-      return kind === 'left' ? this.startTime : this.endTime;
+    if (!geometry) {
+      return kind === 'left' ? this.startAnchor.time : this.endAnchor.time;
     }
 
-    const startIsLeft = Number(startX) <= Number(endX);
+    const startIsLeft = geometry.startPoint.x <= geometry.endPoint.x;
 
     if (kind === 'left') {
-      return startIsLeft ? this.startTime : this.endTime;
+      return startIsLeft ? this.startAnchor.time : this.endAnchor.time;
     }
 
-    return startIsLeft ? this.endTime : this.startTime;
+    return startIsLeft ? this.endAnchor.time : this.startAnchor.time;
   }
 
   private getPriceValueForLabel(kind: PriceLabelKind): number | null {
-    if (this.startPrice === null || this.endPrice === null) {
+    if (!this.startAnchor || !this.endAnchor) {
       return null;
     }
 
-    const startY = getYCoordinateFromPrice(this.series, this.startPrice);
-    const endY = getYCoordinateFromPrice(this.series, this.endPrice);
+    const geometry = this.getTwoPointGeometry();
 
-    if (startY === null || endY === null) {
-      return kind === 'top' ? Math.max(this.startPrice, this.endPrice) : Math.min(this.startPrice, this.endPrice);
+    if (!geometry) {
+      return kind === 'top'
+        ? Math.max(this.startAnchor.price, this.endAnchor.price)
+        : Math.min(this.startAnchor.price, this.endAnchor.price);
     }
 
-    const startIsTop = Number(startY) <= Number(endY);
+    const startIsTop = geometry.startPoint.y <= geometry.endPoint.y;
 
     if (kind === 'top') {
-      return startIsTop ? this.startPrice : this.endPrice;
+      return startIsTop ? this.startAnchor.price : this.endAnchor.price;
     }
 
-    return startIsTop ? this.endPrice : this.startPrice;
+    return startIsTop ? this.endAnchor.price : this.startAnchor.price;
   }
 
-  private containsPoint(point: Point): boolean {
+  protected containsPoint(point: Point): boolean {
     const geometry = this.getGeometry();
 
-    if (!geometry) {
-      return false;
-    }
-
-    return isPointInBounds(point, geometry, BODY_HIT_TOLERANCE);
+    return geometry !== null && isPointInBounds(point, geometry, BODY_HIT_TOLERANCE);
   }
 
   private getCursorStyle(handle: Exclude<RectangleHandle, null>): PrimitiveHoveredItem['cursorStyle'] {
@@ -922,20 +482,4 @@ export class Rectangle extends SeriesDrawingBase<RectangleSettings, RectangleHan
         return 'default';
     }
   }
-
-  private shiftTime(time: Time, offsetX: number): Time | null {
-    return shiftTimeByPixels(this.chart, time, offsetX, this.series);
-  }
-
-  private getPriceDelta(fromY: number, toY: number): number {
-    return getPriceDeltaFromCoordinates(this.series, fromY, toY);
-  }
-
-  private getContainerSize(): { width: number; height: number } {
-    return getElementContainerSize(this.container);
-  }
-
-  private clampPointToContainer(point: Point): Point {
-    return clampPointToContainerInElement(point, this.container);
-  }
 }
diff --git a/src/core/Drawings/regressionTrend/regressionTrend.ts b/src/core/Drawings/regressionTrend/regressionTrend.ts
index a99e71508d607e0e9569dd118c8913680c911201..6ed24bfd367a9c32664674a6d64490e755e6eb9a 100644
--- a/src/core/Drawings/regressionTrend/regressionTrend.ts
+++ b/src/core/Drawings/regressionTrend/regressionTrend.ts
@@ -1,17 +1,9 @@
-import {
-  CustomPriceAxisPaneView,
-  CustomPriceAxisView,
-  CustomTimeAxisPaneView,
-  CustomTimeAxisView,
-} from '@core/Drawings/axis';
 import { getTimeFromXCoordinate, getXCoordinateFromTime, getYCoordinateFromPrice } from '@core/Drawings/helpers';
-import { getDistanceToSegment, updateViews } from '@core/Drawings/utils';
-import { SeriesDrawingBase } from '@src/core/Drawings/SeriesDrawingBase';
+import { getDistanceToSegment } from '@core/Drawings/utils';
 
-import { getThemeStore } from '@src/theme';
-import { Defaults } from '@src/types/defaults';
-import { formatPrice } from '@src/utils';
-import { formatDate } from '@src/utils/formatter';
+import { BaseDrawingParams, DrawingBase, ISeriesDrawing } from '@src/core/Drawings/base';
+import { type ChartOptionsModel, Defaults, type SettingsTab } from '@src/types';
+import { formatDate, formatPrice } from '@src/utils';
 
 import { RegressionTrendPaneView } from './paneView';
 import {
@@ -23,9 +15,7 @@ import {
 
 import type { DrawingHandle } from '@core/Drawings/handles';
 import type { AxisLabel, AxisSegment, Point } from '@core/Drawings/types';
-import type { BaseDrawingParams, ISeriesDrawing } from '@src/core/Drawings/SeriesDrawingBase';
-import type { ChartOptionsModel, SettingsTab } from '@src/types';
-import type { IPrimitivePaneView, PrimitiveHoveredItem, Time, UTCTimestamp } from 'lightweight-charts';
+import type { PrimitiveHoveredItem, Time, UTCTimestamp } from 'lightweight-charts';
 
 type RegressionTrendMode = 'idle' | 'drawing' | 'ready' | 'dragging';
 type DragTarget = 'start' | 'end' | 'body' | null;
@@ -84,7 +74,7 @@ const MIN_BAR_DISTANCE = 1;
 const REGRESSION_DEVIATION = 2;
 
 export class RegressionTrend
-  extends SeriesDrawingBase<RegressionTrendSettings, RegressionTrendHandleKey>
+  extends DrawingBase<RegressionTrendSettings, RegressionTrendHandleKey>
   implements ISeriesDrawing
 {
   private openSettings?: () => void;
@@ -106,14 +96,6 @@ export class RegressionTrend
     showTime: Defaults.showTime,
   };
 
-  private paneView: RegressionTrendPaneView;
-  private timeAxisPaneView: CustomTimeAxisPaneView;
-  private priceAxisPaneView: CustomPriceAxisPaneView;
-  private startTimeAxisView: CustomTimeAxisView;
-  private endTimeAxisView: CustomTimeAxisView;
-  private startPriceAxisView: CustomPriceAxisView;
-  private endPriceAxisView: CustomPriceAxisView;
-
   constructor({
     chart,
     series,
@@ -126,51 +108,16 @@ export class RegressionTrend
     super({ chart, series, container, interaction });
 
     this.openSettings = openSettings;
+    this.initializeViews(new RegressionTrendPaneView(this), ['start', 'end'], ['start', 'end']);
 
-    this.paneView = new RegressionTrendPaneView(this);
-
-    this.timeAxisPaneView = new CustomTimeAxisPaneView({
-      getAxisSegments: () => this.getTimeAxisSegments(),
-    });
-
-    this.priceAxisPaneView = new CustomPriceAxisPaneView({
-      getAxisSegments: () => this.getPriceAxisSegments(),
-    });
-
-    this.startTimeAxisView = new CustomTimeAxisView({
-      getAxisLabel: (kind) => this.getTimeAxisLabel(kind),
-      labelKind: 'start',
-    });
-
-    this.endTimeAxisView = new CustomTimeAxisView({
-      getAxisLabel: (kind) => this.getTimeAxisLabel(kind),
-      labelKind: 'end',
-    });
-
-    this.startPriceAxisView = new CustomPriceAxisView({
-      getAxisLabel: (kind) => this.getPriceAxisLabel(kind),
-      labelKind: 'start',
-    });
-
-    this.endPriceAxisView = new CustomPriceAxisView({
-      getAxisLabel: (kind) => this.getPriceAxisLabel(kind),
-      labelKind: 'end',
+    this.subscribeFormat(formatObservable, (format) => {
+      this.displayFormat = format;
     });
 
-    if (formatObservable) {
-      this.subscriptions.add(
-        formatObservable.subscribe((format) => {
-          this.displayFormat = format;
-          this.render();
-        }),
-      );
-    }
-
     this.series.attachPrimitive(this);
 
-    if (initialEvent && initialEvent.sourceEvent) {
-      const point = this.getEventPoint(initialEvent.sourceEvent);
-      this.startDrawing(point);
+    if (initialEvent?.sourceEvent) {
+      this.startDrawing(this.getEventPoint(initialEvent.sourceEvent));
     }
   }
 
@@ -193,26 +140,25 @@ export class RegressionTrend
       return;
     }
 
-    const next = state as Partial<RegressionTrendState>;
-
-    this.hidden = next.hidden ?? this.hidden;
+    const nextState = state as Partial<RegressionTrendState>;
+    this.hidden = nextState.hidden ?? this.hidden;
 
-    if (next.mode) {
-      this.mode = next.mode === 'dragging' ? 'ready' : next.mode;
+    if (nextState.mode) {
+      this.mode = nextState.mode === 'dragging' ? 'ready' : nextState.mode;
     }
 
-    if ('startTime' in next) {
-      this.startTime = next.startTime ?? null;
+    if ('startTime' in nextState) {
+      this.startTime = nextState.startTime ?? null;
     }
 
-    if ('endTime' in next) {
-      this.endTime = next.endTime ?? null;
+    if ('endTime' in nextState) {
+      this.endTime = nextState.endTime ?? null;
     }
 
-    if (next.settings) {
+    if (nextState.settings) {
       this.settings = {
         ...createDefaultSettings(),
-        ...next.settings,
+        ...nextState.settings,
       };
     }
 
@@ -223,36 +169,6 @@ export class RegressionTrend
     return getRegressionTrendSettingsTabs(this.settings);
   }
 
-  public updateAllViews(): void {
-    updateViews([
-      this.paneView,
-      this.timeAxisPaneView,
-      this.priceAxisPaneView,
-      ...this.timeAxisViews(),
-      ...this.priceAxisViews(),
-    ]);
-  }
-
-  public paneViews(): readonly IPrimitivePaneView[] {
-    return [this.paneView];
-  }
-
-  public timeAxisPaneViews(): readonly IPrimitivePaneView[] {
-    return [this.timeAxisPaneView];
-  }
-
-  public priceAxisPaneViews(): readonly IPrimitivePaneView[] {
-    return [this.priceAxisPaneView];
-  }
-
-  public timeAxisViews() {
-    return [this.startTimeAxisView, this.endTimeAxisView];
-  }
-
-  public priceAxisViews() {
-    return [this.startPriceAxisView, this.endPriceAxisView];
-  }
-
   public getRenderData(): RegressionTrendRenderData | null {
     if (this.hidden) {
       return null;
@@ -325,29 +241,17 @@ export class RegressionTrend
   }
 
   protected getTimeAxisSegments(): AxisSegment[] {
-    if (!this.isSelected() && !this.isCreationPending()) {
+    if (!this.shouldShowInteractiveAxis()) {
       return [];
     }
 
     const geometry = this.getGeometry();
 
-    if (!geometry) {
-      return [];
-    }
-
-    const { colors } = getThemeStore();
-
-    return [
-      {
-        from: geometry.left,
-        to: geometry.right,
-        color: colors.axisMarkerAreaFill,
-      },
-    ];
+    return geometry ? [this.createAxisSegment(geometry.left, geometry.right)] : [];
   }
 
   protected getPriceAxisSegments(): AxisSegment[] {
-    if (!this.isSelected() && !this.isCreationPending()) {
+    if (!this.shouldShowInteractiveAxis()) {
       return [];
     }
 
@@ -357,19 +261,16 @@ export class RegressionTrend
       return [];
     }
 
-    const { colors } = getThemeStore();
-
     return [
-      {
-        from: Math.min(geometry.baseStartPoint.y, geometry.baseEndPoint.y),
-        to: Math.max(geometry.baseStartPoint.y, geometry.baseEndPoint.y),
-        color: colors.axisMarkerAreaFill,
-      },
+      this.createAxisSegment(
+        Math.min(geometry.baseStartPoint.y, geometry.baseEndPoint.y),
+        Math.max(geometry.baseStartPoint.y, geometry.baseEndPoint.y),
+      ),
     ];
   }
 
   protected getTimeAxisLabel(kind: string): AxisLabel | null {
-    if ((!this.isSelected() && !this.isCreationPending()) || (kind !== 'start' && kind !== 'end')) {
+    if (!this.shouldShowInteractiveAxis() || (kind !== 'start' && kind !== 'end')) {
       return null;
     }
 
@@ -382,69 +283,33 @@ export class RegressionTrend
 
     const coordinate = getXCoordinateFromTime(this.chart, time, this.series);
 
-    if (coordinate === null) {
-      return null;
-    }
-
-    const { colors } = getThemeStore();
-
-    return {
-      coordinate,
-      text: formatDate(
-        time as UTCTimestamp,
-        this.displayFormat.dateFormat,
-        this.displayFormat.timeFormat,
-        this.displayFormat.showTime,
-      ),
-      textColor: colors.chartPriceLineText,
-      backgroundColor: colors.axisMarkerLabelFill,
-    };
+    return this.createAxisLabel(coordinate === null ? null : Number(coordinate), this.formatTime(time));
   }
 
   protected getPriceAxisLabel(kind: string): AxisLabel | null {
-    if ((!this.isSelected() && !this.isCreationPending()) || (kind !== 'start' && kind !== 'end')) {
+    if (!this.shouldShowInteractiveAxis() || (kind !== 'start' && kind !== 'end')) {
       return null;
     }
 
-    const labelKind = kind as PriceLabelKind;
     const geometry = this.getGeometry();
 
     if (!geometry) {
       return null;
     }
 
-    const price = labelKind === 'start' ? geometry.baseStartPrice : geometry.baseEndPrice;
+    const price = kind === 'start' ? geometry.baseStartPrice : geometry.baseEndPrice;
     const coordinate = getYCoordinateFromPrice(this.series, price);
 
-    if (coordinate === null) {
-      return null;
-    }
-
-    const { colors } = getThemeStore();
-
-    return {
-      coordinate,
-      text: formatPrice(price) ?? '',
-      textColor: colors.chartPriceLineText,
-      backgroundColor: colors.axisMarkerLabelFill,
-    };
+    return this.createAxisLabel(coordinate === null ? null : Number(coordinate), formatPrice(price) ?? '');
   }
 
   protected handleDoubleClick = (event: MouseEvent): void => {
-    if (this.hidden || this.mode !== 'ready' || !this.isSelected()) {
-      return;
-    }
-
-    const point = this.getEventPoint(event as PointerEvent);
-
-    if (!this.containsPoint(point) && !this.getDrawingHandleAtPoint(point)) {
-      return;
-    }
-
-    event.preventDefault();
-    event.stopPropagation();
-
-    this.openSettings?.();
+    this.openSettingsOnDoubleClick(
+      event,
+      (point) => Boolean(this.containsPoint(point) || this.getDrawingHandleAtPoint(point)),
+      () => this.openSettings?.(),
+      true,
+    );
   };
 
   protected handlePointerDown = (event: PointerEvent): void => {
@@ -455,9 +320,7 @@ export class RegressionTrend
     const point = this.getEventPoint(event);
 
     if (this.mode === 'idle') {
-      event.preventDefault();
-      event.stopPropagation();
-
+      this.consumeEvent(event);
       this.startDrawing(point);
       return;
     }
@@ -469,9 +332,7 @@ export class RegressionTrend
         return;
       }
 
-      event.preventDefault();
-      event.stopPropagation();
-
+      this.consumeEvent(event);
       this.endTime = time;
 
       if (!this.hasValidRange()) {
@@ -481,7 +342,6 @@ export class RegressionTrend
 
       this.mode = 'ready';
       this.resolveReady?.();
-
       this.render();
       return;
     }
@@ -490,15 +350,7 @@ export class RegressionTrend
       return;
     }
 
-    if (!this.isSelected()) {
-      if (!this.containsPoint(point)) {
-        return;
-      }
-
-      event.preventDefault();
-      event.stopPropagation();
-
-      this.select();
+    if (this.selectOnPointerDown(event, () => this.containsPoint(point))) {
       return;
     }
 
@@ -509,16 +361,12 @@ export class RegressionTrend
       return;
     }
 
-    event.preventDefault();
-    event.stopPropagation();
-
+    this.consumeEvent(event);
     this.activeDragTarget = dragTarget;
     this.dragPointerId = event.pointerId;
     this.dragStartPoint = point;
     this.dragStateSnapshot = this.getState();
-
     this.mode = 'dragging';
-
     this.hideCrosshair();
     this.render();
   };
@@ -556,9 +404,7 @@ export class RegressionTrend
     this.dragPointerId = null;
     this.dragStartPoint = null;
     this.dragStateSnapshot = null;
-
     this.mode = 'ready';
-
     this.showCrosshair();
     this.render();
   };
@@ -597,10 +443,10 @@ export class RegressionTrend
       baseStartPrice: regression.baseStartPrice,
       baseEndPrice: regression.baseEndPrice,
       correlation: regression.correlation,
-      left: Math.min(...points.map((item) => item.x)),
-      right: Math.max(...points.map((item) => item.x)),
-      top: Math.min(...points.map((item) => item.y)),
-      bottom: Math.max(...points.map((item) => item.y)),
+      left: Math.min(...points.map((point) => point.x)),
+      right: Math.max(...points.map((point) => point.x)),
+      top: Math.min(...points.map((point) => point.y)),
+      bottom: Math.max(...points.map((point) => point.y)),
     };
   }
 
@@ -614,7 +460,6 @@ export class RegressionTrend
     this.startTime = time;
     this.endTime = time;
     this.mode = 'drawing';
-
     this.render();
   }
 
@@ -687,7 +532,6 @@ export class RegressionTrend
     const minOffset = -minIndex;
     const maxOffset = data.length - 1 - maxIndex;
     const offset = Math.max(minOffset, Math.min(rawOffset, maxOffset));
-
     this.startTime = data[startIndex + offset].time;
     this.endTime = data[endIndex + offset].time;
   }
@@ -756,11 +600,7 @@ export class RegressionTrend
   private hasValidRange(): boolean {
     const range = this.getBarRange();
 
-    if (!range) {
-      return false;
-    }
-
-    return range.right - range.left >= MIN_BAR_DISTANCE;
+    return range !== null && range.right - range.left >= MIN_BAR_DISTANCE;
   }
 
   private getBarTime(x: number): Time | null {
@@ -773,29 +613,17 @@ export class RegressionTrend
     const data = this.getSeriesData();
     const index = findNearestBarIndex(data, time);
 
-    if (index === null) {
-      return null;
-    }
-
-    return data[index].time;
+    return index === null ? null : data[index].time;
   }
 
   private getBarIndexByX(x: number, data: readonly RegressionSeriesData[]): number | null {
     const time = getTimeFromXCoordinate(this.chart, x);
 
-    if (typeof time !== 'number') {
-      return null;
-    }
-
-    return findNearestBarIndex(data, time);
+    return typeof time === 'number' ? findNearestBarIndex(data, time) : null;
   }
 
   private getBarIndex(time: Time, data: readonly RegressionSeriesData[]): number | null {
-    if (typeof time !== 'number') {
-      return null;
-    }
-
-    return findNearestBarIndex(data, time);
+    return typeof time === 'number' ? findNearestBarIndex(data, time) : null;
   }
 
   private getSeriesData(): readonly RegressionSeriesData[] {
@@ -823,11 +651,7 @@ export class RegressionTrend
       return handle.id;
     }
 
-    if (this.containsPoint(point)) {
-      return 'body';
-    }
-
-    return null;
+    return this.containsPoint(point) ? 'body' : null;
   }
 
   private containsPoint(point: Point): boolean {
@@ -856,6 +680,15 @@ export class RegressionTrend
       geometry.lowerStartPoint,
     ]);
   }
+
+  private formatTime(time: number): string {
+    return formatDate(
+      time as UTCTimestamp,
+      this.displayFormat.dateFormat,
+      this.displayFormat.timeFormat,
+      this.displayFormat.showTime,
+    );
+  }
 }
 
 function calculateRegression(values: readonly number[]): {
@@ -944,7 +777,6 @@ function findNearestBarIndex(data: readonly RegressionSeriesData[], targetTime:
 
   const nextIndex = Math.min(left, data.length - 1);
   const previousIndex = Math.max(0, nextIndex - 1);
-
   const nextTime = data[nextIndex].time;
   const previousTime = data[previousIndex].time;
 
diff --git a/src/core/Drawings/ruler/ruler.ts b/src/core/Drawings/ruler/ruler.ts
index 81f152cc338fce0c28f03678b19c0cbacb390cdc..34fd37b9b7ae953953947829660fa1ee2d286a38 100644
--- a/src/core/Drawings/ruler/ruler.ts
+++ b/src/core/Drawings/ruler/ruler.ts
@@ -1,49 +1,29 @@
 import { PrimitiveHoveredItem } from 'lightweight-charts';
 import { Observable, skip } from 'rxjs';
 
-import {
-  CustomPriceAxisPaneView,
-  CustomPriceAxisView,
-  CustomTimeAxisPaneView,
-  CustomTimeAxisView,
-} from '@core/Drawings/axis';
-import {
-  getAnchorFromPoint,
-  getPriceFromYCoordinate,
-  getXCoordinateFromTime,
-  getYCoordinateFromPrice,
-} from '@core/Drawings/helpers';
-import { updateViews } from '@core/Drawings/utils';
-import { SeriesDrawingBase } from '@src/core/Drawings/SeriesDrawingBase';
+import { getPriceFromYCoordinate } from '@core/Drawings/helpers';
 
+import { BaseDrawingParams, ISeriesDrawing, TwoPointDrawingBase, TwoPointGeometry } from '@src/core/Drawings/base';
 import { getThemeStore } from '@src/theme';
 import { t } from '@src/translations';
-import { ChartOptionsModel, Direction } from '@src/types';
-import { Defaults } from '@src/types/defaults';
-import { SettingsTab, SettingsValues } from '@src/types/settings';
-import { formatPrice, formatVolume } from '@src/utils';
-import { formatDate } from '@src/utils/formatter';
+import { ChartOptionsModel, Defaults, Direction, SettingsTab, SettingsValues } from '@src/types';
+import { formatDate, formatPrice, formatVolume } from '@src/utils';
 
 import { RulerPaneView } from './paneView';
 
 import type { Anchor, AxisLabel, AxisSegment, Point } from '@core/Drawings/types';
-import type { BaseDrawingParams, ISeriesDrawing } from '@src/core/Drawings/SeriesDrawingBase';
 import type {
   AutoscaleInfo,
   Coordinate,
-  IPrimitivePaneView,
-  ISeriesApi,
-  ISeriesPrimitiveAxisView,
   Logical,
   MouseEventHandler,
   MouseEventParams,
-  SeriesOptionsMap,
   Time,
   UTCTimestamp,
 } from 'lightweight-charts';
 
-type SeriesApi = ISeriesApi<keyof SeriesOptionsMap, Time>;
 type RulerMode = 'idle' | 'placingEnd' | 'ready';
+type RulerAnchorKind = 'start' | 'end';
 
 interface RulerState {
   hidden: boolean;
@@ -68,7 +48,7 @@ export interface RulerRenderData {
   verticalArrowSide: Direction.Top | Direction.Bottom | null;
 }
 
-export class Ruler extends SeriesDrawingBase implements ISeriesDrawing {
+export class Ruler extends TwoPointDrawingBase<SettingsValues> implements ISeriesDrawing {
   private removeSelf?: () => void;
 
   protected settings: SettingsValues = {};
@@ -80,20 +60,9 @@ export class Ruler extends SeriesDrawingBase implements ISeriesDrawing {
     showTime: Defaults.showTime,
   };
 
-  private startAnchor: Anchor | null = null;
-  private endAnchor: Anchor | null = null;
-
   private readonly clickHandler: MouseEventHandler<Time>;
   private readonly moveHandler: MouseEventHandler<Time>;
 
-  private readonly paneView: RulerPaneView;
-  private readonly timeAxisPaneView: CustomTimeAxisPaneView;
-  private readonly priceAxisPaneView: CustomPriceAxisPaneView;
-  private readonly startTimeAxisView: CustomTimeAxisView;
-  private readonly endTimeAxisView: CustomTimeAxisView;
-  private readonly startPriceAxisView: CustomPriceAxisView;
-  private readonly endPriceAxisView: CustomPriceAxisView;
-
   constructor({
     chart,
     series,
@@ -104,57 +73,17 @@ export class Ruler extends SeriesDrawingBase implements ISeriesDrawing {
     interaction,
     initialEvent,
   }: RulerParams) {
-    super({
-      chart,
-      series,
-      container,
-      interaction,
-    });
+    super({ chart, series, container, interaction });
 
     this.removeSelf = removeSelf;
-
     this.clickHandler = (params) => this.handleChartClick(params);
     this.moveHandler = (params) => this.handleMove(params);
+    this.initializeViews(new RulerPaneView(this), ['start', 'end'], ['start', 'end']);
 
-    this.paneView = new RulerPaneView(this);
-
-    this.timeAxisPaneView = new CustomTimeAxisPaneView({
-      getAxisSegments: () => this.getTimeAxisSegments(),
-    });
-
-    this.priceAxisPaneView = new CustomPriceAxisPaneView({
-      getAxisSegments: () => this.getPriceAxisSegments(),
-    });
-
-    this.startTimeAxisView = new CustomTimeAxisView({
-      getAxisLabel: (labelKind) => this.getTimeAxisLabel(labelKind),
-      labelKind: 'start',
+    this.subscribeFormat(formatObservable, (format) => {
+      this.displayFormat = format;
     });
 
-    this.endTimeAxisView = new CustomTimeAxisView({
-      getAxisLabel: (labelKind) => this.getTimeAxisLabel(labelKind),
-      labelKind: 'end',
-    });
-
-    this.startPriceAxisView = new CustomPriceAxisView({
-      getAxisLabel: (labelKind) => this.getPriceAxisLabel(labelKind),
-      labelKind: 'start',
-    });
-
-    this.endPriceAxisView = new CustomPriceAxisView({
-      getAxisLabel: (labelKind) => this.getPriceAxisLabel(labelKind),
-      labelKind: 'end',
-    });
-
-    if (formatObservable) {
-      this.subscriptions.add(
-        formatObservable.subscribe((format) => {
-          this.displayFormat = format;
-          this.render();
-        }),
-      );
-    }
-
     resetTriggers.forEach((trigger) => {
       this.subscriptions.add(
         trigger.pipe(skip(1)).subscribe(() => {
@@ -165,9 +94,8 @@ export class Ruler extends SeriesDrawingBase implements ISeriesDrawing {
 
     this.series.attachPrimitive(this);
 
-    if (initialEvent && initialEvent.sourceEvent) {
-      const point = this.getEventPoint(initialEvent.sourceEvent);
-      this.startDrawing(point);
+    if (initialEvent?.sourceEvent) {
+      this.startDrawing(this.getEventPoint(initialEvent.sourceEvent));
     }
   }
 
@@ -189,59 +117,32 @@ export class Ruler extends SeriesDrawingBase implements ISeriesDrawing {
   }
 
   public setState(state: unknown): void {
-    const nextState = state as Partial<RulerState>;
+    if (!state || typeof state !== 'object') {
+      return;
+    }
 
+    const nextState = state as Partial<RulerState>;
     this.hidden = nextState.hidden ?? this.hidden;
     this.mode = nextState.mode ?? this.mode;
-    this.startAnchor = nextState.startAnchor ?? this.startAnchor;
-    this.endAnchor = nextState.endAnchor ?? this.endAnchor;
 
-    this.render();
-  }
-
-  public updateAllViews(): void {
-    updateViews([
-      this.paneView,
-      this.timeAxisPaneView,
-      this.priceAxisPaneView,
-      this.startTimeAxisView,
-      this.endTimeAxisView,
-      this.startPriceAxisView,
-      this.endPriceAxisView,
-    ]);
-  }
-
-  public paneViews(): readonly IPrimitivePaneView[] {
-    return [this.paneView];
-  }
-
-  public timeAxisPaneViews(): readonly IPrimitivePaneView[] {
-    return [this.timeAxisPaneView];
-  }
-
-  public priceAxisPaneViews(): readonly IPrimitivePaneView[] {
-    return [this.priceAxisPaneView];
-  }
+    if ('startAnchor' in nextState) {
+      this.startAnchor = nextState.startAnchor ?? null;
+    }
 
-  public timeAxisViews(): readonly ISeriesPrimitiveAxisView[] {
-    return [this.startTimeAxisView, this.endTimeAxisView];
-  }
+    if ('endAnchor' in nextState) {
+      this.endAnchor = nextState.endAnchor ?? null;
+    }
 
-  public priceAxisViews(): readonly ISeriesPrimitiveAxisView[] {
-    return [this.startPriceAxisView, this.endPriceAxisView];
+    this.render();
   }
 
   public autoscaleInfo(startTimePoint: Logical, endTimePoint: Logical): AutoscaleInfo | null {
-    if (this.hidden || this.mode === 'placingEnd') {
+    if (this.hidden || this.mode === 'placingEnd' || !this.startAnchor || !this.endAnchor) {
       return null;
     }
 
-    if (!this.startAnchor || !this.endAnchor) {
-      return null;
-    }
-
-    const startCoordinate = getXCoordinateFromTime(this.chart, this.startAnchor.time, this.series);
-    const endCoordinate = getXCoordinateFromTime(this.chart, this.endAnchor.time, this.series);
+    const startCoordinate = this.getAnchorTimeCoordinate('start');
+    const endCoordinate = this.getAnchorTimeCoordinate('end');
 
     if (startCoordinate === null || endCoordinate === null) {
       return null;
@@ -263,42 +164,24 @@ export class Ruler extends SeriesDrawingBase implements ISeriesDrawing {
 
     return {
       priceRange: {
-        minValue: Math.min(Number(this.startAnchor.price), Number(this.endAnchor.price)),
-        maxValue: Math.max(Number(this.startAnchor.price), Number(this.endAnchor.price)),
+        minValue: Math.min(this.startAnchor.price, this.endAnchor.price),
+        maxValue: Math.max(this.startAnchor.price, this.endAnchor.price),
       },
     };
   }
 
   public getRenderData(): RulerRenderData {
-    const startPoint = this.startAnchor ? this.getPoint(this.startAnchor) : null;
-    const endPoint = this.endAnchor ? this.getPoint(this.endAnchor) : null;
-
-    const startPrice = this.startAnchor ? Number(this.startAnchor.price) : 0;
-    const endPrice = this.endAnchor ? Number(this.endAnchor.price) : 0;
+    const startPoint = this.getPointFromAnchor(this.startAnchor);
+    const endPoint = this.getPointFromAnchor(this.endAnchor);
+    const startPrice = this.startAnchor?.price ?? 0;
+    const endPrice = this.endAnchor?.price ?? 0;
     const priceDiff = endPrice - startPrice;
     const percentDiff = startPrice !== 0 ? (priceDiff / startPrice) * 100 : null;
-
-    const startIndex = this.startAnchor ? this.findIndexByTime(this.startAnchor.time) : -1;
-    const endIndex = this.endAnchor ? this.findIndexByTime(this.endAnchor.time) : -1;
-
-    const barsCount = startIndex >= 0 && endIndex >= 0 ? Math.abs(endIndex - startIndex) : 0;
+    const barsCount = this.getBarsCount();
     const volume = this.getVolumeInRange();
     const isLong = priceDiff >= 0;
-
-    const horizontalArrowSide =
-      startPoint && endPoint && startPoint.x !== endPoint.x
-        ? endPoint.x > startPoint.x
-          ? Direction.Right
-          : Direction.Left
-        : null;
-
-    const verticalArrowSide =
-      startPoint && endPoint && startPoint.y !== endPoint.y
-        ? endPoint.y > startPoint.y
-          ? Direction.Bottom
-          : Direction.Top
-        : null;
-
+    const horizontalArrowSide = this.getHorizontalArrowSide(startPoint, endPoint);
+    const verticalArrowSide = this.getVerticalArrowSide(startPoint, endPoint);
     const { colors } = getThemeStore();
 
     return {
@@ -309,7 +192,9 @@ export class Ruler extends SeriesDrawingBase implements ISeriesDrawing {
       fillColor: isLong ? colors.rulerPositiveFill : colors.rulerNegativeFill,
       textColor: colors.chartPriceLineText,
       infoLines: [
-        `${formatPrice(Math.abs(priceDiff))} (${percentDiff === null ? '-' : `${formatPrice(Math.abs(percentDiff))}%`})`,
+        `${formatPrice(Math.abs(priceDiff))} (${
+          percentDiff === null ? '-' : `${formatPrice(Math.abs(percentDiff))}%`
+        })`,
         `${barsCount} ${t('bars')},`,
         `${t('Vol')} ${formatVolume(volume)}`,
       ],
@@ -318,56 +203,46 @@ export class Ruler extends SeriesDrawingBase implements ISeriesDrawing {
     };
   }
 
-  public getTimeCoordinate(kind: 'start' | 'end'): Coordinate | null {
-    const anchor = kind === 'start' ? this.startAnchor : this.endAnchor;
-
-    if (!anchor) {
-      return null;
-    }
+  public getTimeCoordinate(kind: RulerAnchorKind): Coordinate | null {
+    const coordinate = this.getAnchorTimeCoordinate(kind);
 
-    return getXCoordinateFromTime(this.chart, anchor.time, this.series);
+    return coordinate === null ? null : (coordinate as Coordinate);
   }
 
-  public getPriceCoordinate(kind: 'start' | 'end'): Coordinate | null {
-    const anchor = kind === 'start' ? this.startAnchor : this.endAnchor;
+  public getPriceCoordinate(kind: RulerAnchorKind): Coordinate | null {
+    const coordinate = this.getAnchorPriceCoordinate(kind);
 
-    if (!anchor) {
-      return null;
-    }
-
-    return getYCoordinateFromPrice(this.series, anchor.price);
+    return coordinate === null ? null : (coordinate as Coordinate);
   }
 
   public getTimeBounds(): { left: number; right: number } | null {
-    const startX = this.getTimeCoordinate('start');
-    const endX = this.getTimeCoordinate('end');
+    const geometry = this.getTwoPointGeometry();
 
-    if (startX === null || endX === null) {
+    if (!geometry) {
       return null;
     }
 
     return {
-      left: Math.min(startX, endX),
-      right: Math.max(startX, endX),
+      left: geometry.left,
+      right: geometry.right,
     };
   }
 
   public getPriceBounds(): { top: number; bottom: number } | null {
-    const startY = this.getPriceCoordinate('start');
-    const endY = this.getPriceCoordinate('end');
+    const geometry = this.getTwoPointGeometry();
 
-    if (startY === null || endY === null) {
+    if (!geometry) {
       return null;
     }
 
     return {
-      top: Math.min(startY, endY),
-      bottom: Math.max(startY, endY),
+      top: geometry.top,
+      bottom: geometry.bottom,
     };
   }
 
-  public getTimeText(kind: 'start' | 'end'): string {
-    const anchor = kind === 'start' ? this.startAnchor : this.endAnchor;
+  public getTimeText(kind: RulerAnchorKind): string {
+    const anchor = this.getAnchor(kind);
 
     if (!anchor || typeof anchor.time !== 'number') {
       return '';
@@ -381,58 +256,34 @@ export class Ruler extends SeriesDrawingBase implements ISeriesDrawing {
     );
   }
 
-  public getPriceText(kind: 'start' | 'end'): string {
-    const anchor = kind === 'start' ? this.startAnchor : this.endAnchor;
+  public getPriceText(kind: RulerAnchorKind): string {
+    const anchor = this.getAnchor(kind);
 
-    if (!anchor) {
-      return '';
-    }
-
-    return formatPrice(Number(anchor.price)) ?? '';
+    return anchor ? (formatPrice(anchor.price) ?? '') : '';
   }
 
   public shouldShowInObjectTree(): boolean {
     return false;
   }
 
-  protected getHoveredItem(x: number, y: number): PrimitiveHoveredItem | null {
+  protected getHoveredItem(_x: number, _y: number): PrimitiveHoveredItem | null {
     return null;
   }
 
+  protected getGeometry(): TwoPointGeometry | null {
+    return this.getTwoPointGeometry();
+  }
+
   protected getTimeAxisSegments(): AxisSegment[] {
     const bounds = this.getTimeBounds();
 
-    if (!bounds) {
-      return [];
-    }
-
-    const { colors } = getThemeStore();
-
-    return [
-      {
-        from: bounds.left,
-        to: bounds.right,
-        color: colors.axisMarkerAreaFill,
-      },
-    ];
+    return bounds ? [this.createAxisSegment(bounds.left, bounds.right)] : [];
   }
 
   protected getPriceAxisSegments(): AxisSegment[] {
     const bounds = this.getPriceBounds();
 
-    if (!bounds) {
-      return [];
-    }
-
-    const { colors } = getThemeStore();
-
-    return [
-      {
-        from: bounds.top,
-        to: bounds.bottom,
-        color: colors.axisMarkerAreaFill,
-      },
-    ];
+    return bounds ? [this.createAxisSegment(bounds.top, bounds.bottom)] : [];
   }
 
   protected getTimeAxisLabel(kind: string): AxisLabel | null {
@@ -440,21 +291,7 @@ export class Ruler extends SeriesDrawingBase implements ISeriesDrawing {
       return null;
     }
 
-    const coordinate = this.getTimeCoordinate(kind);
-    const text = this.getTimeText(kind);
-
-    if (coordinate === null || !text) {
-      return null;
-    }
-
-    const { colors } = getThemeStore();
-
-    return {
-      coordinate,
-      text,
-      textColor: colors.chartPriceLineText,
-      backgroundColor: colors.axisMarkerLabelFill,
-    };
+    return this.createAxisLabel(this.getAnchorTimeCoordinate(kind), this.getTimeText(kind));
   }
 
   protected getPriceAxisLabel(kind: string): AxisLabel | null {
@@ -462,37 +299,12 @@ export class Ruler extends SeriesDrawingBase implements ISeriesDrawing {
       return null;
     }
 
-    const coordinate = this.getPriceCoordinate(kind);
-    const text = this.getPriceText(kind);
-
-    if (coordinate === null || !text) {
-      return null;
-    }
-
-    const { colors } = getThemeStore();
-
-    return {
-      coordinate,
-      text,
-      textColor: colors.chartPriceLineText,
-      backgroundColor: colors.axisMarkerLabelFill,
-    };
-  }
-
-  private findIndexByTime(time: Time): number {
-    const data = this.series.data() ?? [];
-
-    return data.findIndex((item) => {
-      if (typeof item.time === 'number' && typeof time === 'number') {
-        return item.time === time;
-      }
-
-      return false;
-    });
+    return this.createAxisLabel(this.getAnchorPriceCoordinate(kind), this.getPriceText(kind));
   }
 
   protected bindEvents(): void {
     // todo: попробовать привести к виду базового класса
+
     if (this.isBound) {
       return;
     }
@@ -502,12 +314,9 @@ export class Ruler extends SeriesDrawingBase implements ISeriesDrawing {
     this.chart.subscribeCrosshairMove(this.moveHandler);
   }
 
-  protected getGeometry(): void {
-    console.log('stub');
-  }
-
   protected unbindEvents(): void {
     // todo: попробовать привести к виду базового класса
+
     if (!this.isBound) {
       return;
     }
@@ -527,21 +336,14 @@ export class Ruler extends SeriesDrawingBase implements ISeriesDrawing {
       return;
     }
 
-    if (!params.sourceEvent) {
-      return;
-    }
-
-    const anchor = this.createAnchor(params);
+    const anchor = this.createAnchorFromParams(params);
 
     if (!anchor) {
       return;
     }
 
-    const point = this.getEventPoint(params.sourceEvent);
-
     if (this.mode === 'idle') {
-      this.startDrawing(point);
-
+      this.startDrawing(this.getEventPoint(params.sourceEvent));
       return;
     }
 
@@ -549,32 +351,25 @@ export class Ruler extends SeriesDrawingBase implements ISeriesDrawing {
       this.endAnchor = anchor;
       this.mode = 'ready';
       this.resolveReady?.();
-
       this.showCrosshair();
       this.render();
     }
   }
 
   private startDrawing(point: Point): void {
-    const anchor = getAnchorFromPoint(this.chart, this.series, point);
-    this.startAnchor = anchor;
-    this.endAnchor = anchor;
+    const anchor = this.createAnchor(point);
+    this.setAnchors(anchor, anchor);
     this.mode = 'placingEnd';
-
     this.hideCrosshair();
     this.render();
   }
 
   private handleMove(params: MouseEventParams<Time>): void {
-    if (this.hidden || !params.point) {
-      return;
-    }
-
-    if (this.mode !== 'placingEnd') {
+    if (this.hidden || !params.point || this.mode !== 'placingEnd') {
       return;
     }
 
-    const anchor = this.createAnchor(params);
+    const anchor = this.createAnchorFromParams(params);
 
     if (!anchor) {
       return;
@@ -584,79 +379,35 @@ export class Ruler extends SeriesDrawingBase implements ISeriesDrawing {
     this.render();
   }
 
-  private createAnchor({ time, point }: MouseEventParams<Time>): Anchor | null {
+  private createAnchorFromParams({ time, point }: MouseEventParams<Time>): Anchor | null {
     if (!point || time === undefined) {
       return null;
     }
 
     const price = getPriceFromYCoordinate(this.series, point.y);
 
-    if (price === null) {
-      return null;
-    }
-
-    return {
-      price,
-      time,
-    };
+    return price === null ? null : { time, price };
   }
 
-  private getPoint(anchor: Anchor): Point | null {
-    const x = getXCoordinateFromTime(this.chart, anchor.time, this.series);
-    const y = getYCoordinateFromPrice(this.series, anchor.price);
+  private getBarsCount(): number {
+    const startIndex = this.startAnchor ? this.findIndexByTime(this.startAnchor.time) : -1;
+    const endIndex = this.endAnchor ? this.findIndexByTime(this.endAnchor.time) : -1;
+    return startIndex >= 0 && endIndex >= 0 ? Math.abs(endIndex - startIndex) : 0;
+  }
 
-    if (x === null || y === null) {
+  private getHorizontalArrowSide(start: Point | null, end: Point | null): RulerRenderData['horizontalArrowSide'] {
+    if (!start || !end || start.x === end.x) {
       return null;
     }
 
-    return {
-      x,
-      y,
-    };
+    return end.x > start.x ? Direction.Right : Direction.Left;
   }
 
-  private getVolumeInRange(): number {
-    if (!this.startAnchor || !this.endAnchor) {
-      return 0;
-    }
-
-    const data = this.series.data() ?? [];
-
-    if (!data.length) {
-      return 0;
-    }
-
-    const startIndex = this.findIndexByTime(this.startAnchor.time);
-    const endIndex = this.findIndexByTime(this.endAnchor.time);
-
-    if (startIndex < 0 || endIndex < 0) {
-      return 0;
-    }
-
-    const from = Math.min(startIndex, endIndex);
-    const to = Math.max(startIndex, endIndex);
-
-    let volume = 0;
-
-    for (let index = from; index <= to; index += 1) {
-      const item = data[index] as unknown as Record<string, unknown> | undefined;
-
-      if (!item) {
-        continue;
-      }
-
-      if (typeof item.volume === 'number') {
-        volume += item.volume;
-        continue;
-      }
-
-      const customValues = item.customValues as Record<string, unknown> | undefined;
-
-      if (customValues && typeof customValues.volume === 'number') {
-        volume += customValues.volume;
-      }
+  private getVerticalArrowSide(start: Point | null, end: Point | null): RulerRenderData['verticalArrowSide'] {
+    if (!start || !end || start.y === end.y) {
+      return null;
     }
 
-    return volume;
+    return end.y > start.y ? Direction.Bottom : Direction.Top;
   }
 }
diff --git a/src/core/Drawings/sliderPosition/sliderPosition.ts b/src/core/Drawings/sliderPosition/sliderPosition.ts
index 38267e9eedf8fe1c01b55de7c4cd762dfb6c378f..c9abaa9ac3918de4726334d0c6b7d8ef2983537b 100644
--- a/src/core/Drawings/sliderPosition/sliderPosition.ts
+++ b/src/core/Drawings/sliderPosition/sliderPosition.ts
@@ -1,13 +1,6 @@
 import { round } from 'lodash-es';
 import { skip } from 'rxjs';
 
-import {
-  CustomPriceAxisPaneView,
-  CustomPriceAxisView,
-  CustomTimeAxisPaneView,
-  CustomTimeAxisView,
-} from '@core/Drawings/axis';
-
 import {
   getPriceDelta as getPriceDeltaFromCoordinates,
   getPriceFromYCoordinate,
@@ -18,13 +11,19 @@ import {
   isPointInBounds,
   shiftTimeByPixels,
 } from '@core/Drawings/helpers';
-import { updateViews } from '@core/Drawings/utils';
-import { SeriesDrawingBase } from '@src/core/Drawings/SeriesDrawingBase';
+
+import { BaseDrawingParams, DrawingBase, ISeriesDrawing } from '@src/core/Drawings/base';
 import { getThemeStore } from '@src/theme';
 import { t } from '@src/translations';
-import { Defaults } from '@src/types/defaults';
-import { formatPercent, formatPrice, formatSignedNumber, getPercentPrecision, getPricePrecisionStep } from '@src/utils';
-import { formatDate } from '@src/utils/formatter';
+import { type ChartOptionsModel, Defaults, type SettingsTab, type SettingsValues } from '@src/types';
+import {
+  formatDate,
+  formatPercent,
+  formatPrice,
+  formatSignedNumber,
+  getPercentPrecision,
+  getPricePrecisionStep,
+} from '@src/utils';
 
 import { SliderPaneView } from './paneView';
 import { createDefaultSettings, getSliderPositionSettingsTabs } from './settings';
@@ -33,23 +32,18 @@ import type { SliderPositionArguments, SliderPositionSettings, SliderPositionSty
 
 import type { DrawingHandle } from '@core/Drawings/handles';
 import type { AxisLabel, AxisSegment, Bounds, Point } from '@core/Drawings/types';
-import type { BaseDrawingParams, ISeriesDrawing } from '@src/core/Drawings/SeriesDrawingBase';
-import type { ChartOptionsModel, SettingsTab, SettingsValues } from '@src/types';
-import type {
-  IPrimitivePaneView,
-  MouseEventHandler,
-  MouseEventParams,
-  PrimitiveHoveredItem,
-  Time,
-  UTCTimestamp,
-} from 'lightweight-charts';
+import type { MouseEventHandler, MouseEventParams, PrimitiveHoveredItem, Time, UTCTimestamp } from 'lightweight-charts';
 import type { Observable } from 'rxjs';
 
 type SliderSide = 'long' | 'short';
 type SliderMode = 'idle' | 'ready' | 'dragging';
+
 type DragTarget = 'body' | 'entry' | 'target' | 'stop' | 'end' | null;
+
 type SliderHandleId = Exclude<DragTarget, 'body' | null>;
+
 type TimeLabelKind = 'start' | 'end';
+
 type PriceLabelKind = 'target' | 'entry' | 'stop';
 
 interface SliderPositionParams extends BaseDrawingParams {
@@ -110,25 +104,15 @@ const INITIAL_WIDTH_PX = 160;
 const INITIAL_ZONE_OFFSET_PX = 60;
 const DEFAULT_RISK_REWARD_RATIO = 1;
 
-export class SliderPosition
-  extends SeriesDrawingBase<SliderPositionSettings, SliderHandleId>
-  implements ISeriesDrawing
-{
+export class SliderPosition extends DrawingBase<SliderPositionSettings, SliderHandleId> implements ISeriesDrawing {
   protected settings: SliderPositionSettings = createDefaultSettings();
+
   protected mode: SliderMode = 'idle';
 
   private readonly side: SliderSide;
   private readonly removeSelf?: () => void;
   private readonly openSettings?: () => void;
   private readonly clickHandler: MouseEventHandler<Time>;
-  private readonly paneView: SliderPaneView;
-  private readonly timeAxisPaneView: CustomTimeAxisPaneView;
-  private readonly priceAxisPaneView: CustomPriceAxisPaneView;
-  private readonly startTimeAxisView: CustomTimeAxisView;
-  private readonly endTimeAxisView: CustomTimeAxisView;
-  private readonly targetPriceAxisView: CustomPriceAxisView;
-  private readonly entryPriceAxisView: CustomPriceAxisView;
-  private readonly stopPriceAxisView: CustomPriceAxisView;
 
   private displayFormat: ChartOptionsModel = {
     dateFormat: Defaults.dateFormat,
@@ -138,9 +122,11 @@ export class SliderPosition
 
   private startTime: Time | null = null;
   private endTime: Time | null = null;
+
   private entryPrice: number | null = null;
   private stopPrice: number | null = null;
   private targetPrice: number | null = null;
+
   private activeDragTarget: DragTarget = null;
   private dragPointerId: number | null = null;
   private dragStartPoint: Point | null = null;
@@ -158,58 +144,23 @@ export class SliderPosition
     openSettings,
     initialEvent,
   }: SliderPositionParams) {
-    super({ chart, series, container, interaction });
+    super({
+      chart,
+      series,
+      container,
+      interaction,
+    });
 
     this.side = side;
     this.removeSelf = removeSelf;
     this.openSettings = openSettings;
     this.clickHandler = (params) => this.handleChartClick(params);
-    this.paneView = new SliderPaneView(this);
+    this.initializeViews(new SliderPaneView(this), ['start', 'end'], ['target', 'entry', 'stop'], 'normal');
 
-    this.timeAxisPaneView = new CustomTimeAxisPaneView({
-      getAxisSegments: () => this.getTimeAxisSegments(),
-      zOrder: 'normal',
+    this.subscribeFormat(formatObservable, (format) => {
+      this.displayFormat = format;
     });
 
-    this.priceAxisPaneView = new CustomPriceAxisPaneView({
-      getAxisSegments: () => this.getPriceAxisSegments(),
-      zOrder: 'normal',
-    });
-
-    this.startTimeAxisView = new CustomTimeAxisView({
-      getAxisLabel: (kind) => this.getTimeAxisLabel(kind),
-      labelKind: 'start',
-    });
-
-    this.endTimeAxisView = new CustomTimeAxisView({
-      getAxisLabel: (kind) => this.getTimeAxisLabel(kind),
-      labelKind: 'end',
-    });
-
-    this.targetPriceAxisView = new CustomPriceAxisView({
-      getAxisLabel: (kind) => this.getPriceAxisLabel(kind),
-      labelKind: 'target',
-    });
-
-    this.entryPriceAxisView = new CustomPriceAxisView({
-      getAxisLabel: (kind) => this.getPriceAxisLabel(kind),
-      labelKind: 'entry',
-    });
-
-    this.stopPriceAxisView = new CustomPriceAxisView({
-      getAxisLabel: (kind) => this.getPriceAxisLabel(kind),
-      labelKind: 'stop',
-    });
-
-    if (formatObservable) {
-      this.subscriptions.add(
-        formatObservable.subscribe((format) => {
-          this.displayFormat = format;
-          this.render();
-        }),
-      );
-    }
-
     resetTriggers.forEach((trigger) => {
       this.subscriptions.add(
         trigger.pipe(skip(1)).subscribe(() => {
@@ -240,7 +191,9 @@ export class SliderPosition
       targetPrice: this.targetPrice,
       riskRewardRatio: this.getCurrentRiskRewardRatio(),
       amount: this.settings.accountSize,
-      settings: { ...this.settings },
+      settings: {
+        ...this.settings,
+      },
     };
   }
 
@@ -250,6 +203,7 @@ export class SliderPosition
     }
 
     const next = state as Partial<SliderPositionState>;
+
     const {
       hidden = this.hidden,
       mode = this.mode,
@@ -303,7 +257,12 @@ export class SliderPosition
       SliderPositionSettings & SliderPositionArguments
     >;
 
-    this.applyPositionArguments({ entryPrice, targetPrice, stopPrice });
+    this.applyPositionArguments({
+      entryPrice,
+      targetPrice,
+      stopPrice,
+    });
+
     super.updateSettings(settings as SettingsValues);
   }
 
@@ -311,39 +270,6 @@ export class SliderPosition
     return getSliderPositionSettingsTabs(this.settings, this.getPositionArguments());
   }
 
-  public updateAllViews(): void {
-    updateViews([
-      this.paneView,
-      this.timeAxisPaneView,
-      this.priceAxisPaneView,
-      this.startTimeAxisView,
-      this.endTimeAxisView,
-      this.targetPriceAxisView,
-      this.entryPriceAxisView,
-      this.stopPriceAxisView,
-    ]);
-  }
-
-  public paneViews(): readonly IPrimitivePaneView[] {
-    return [this.paneView];
-  }
-
-  public timeAxisPaneViews(): readonly IPrimitivePaneView[] {
-    return [this.timeAxisPaneView];
-  }
-
-  public priceAxisPaneViews(): readonly IPrimitivePaneView[] {
-    return [this.priceAxisPaneView];
-  }
-
-  public timeAxisViews() {
-    return [this.startTimeAxisView, this.endTimeAxisView];
-  }
-
-  public priceAxisViews() {
-    return [this.targetPriceAxisView, this.entryPriceAxisView, this.stopPriceAxisView];
-  }
-
   public getRenderData(): SliderRenderData | null {
     if (this.hidden) {
       return null;
@@ -371,7 +297,10 @@ export class SliderPosition
     };
   }
 
-  public getTimeBounds(): { left: number; right: number } | null {
+  public getTimeBounds(): {
+    left: number;
+    right: number;
+  } | null {
     const start = this.getTimeCoordinate('start');
     const end = this.getTimeCoordinate('end');
 
@@ -422,10 +351,13 @@ export class SliderPosition
     switch (kind) {
       case 'target':
         return geometry.targetY;
+
       case 'entry':
         return geometry.entryY;
+
       case 'stop':
         return geometry.stopY;
+
       default:
         return null;
     }
@@ -441,17 +373,23 @@ export class SliderPosition
     switch (kind) {
       case 'target':
         return formatPrice(geometry.targetPrice) ?? '';
+
       case 'entry':
         return formatPrice(geometry.entryPrice) ?? '';
+
       case 'stop':
         return formatPrice(geometry.stopPrice) ?? '';
+
       default:
         return '';
     }
   }
 
   protected getHoveredItem(x: number, y: number): PrimitiveHoveredItem | null {
-    const point = { x, y };
+    const point = {
+      x,
+      y,
+    };
 
     if (!this.isSelected()) {
       if (!this.containsPoint(point)) {
@@ -497,15 +435,7 @@ export class SliderPosition
       return [];
     }
 
-    const { colors } = getThemeStore();
-
-    return [
-      {
-        from: bounds.left,
-        to: bounds.right,
-        color: colors.axisMarkerAreaFill,
-      },
-    ];
+    return [this.createAxisSegment(bounds.left, bounds.right)];
   }
 
   protected getPriceAxisSegments(): AxisSegment[] {
@@ -519,59 +449,30 @@ export class SliderPosition
       return [];
     }
 
-    const { colors } = getThemeStore();
-
     return [
-      {
-        from: geometry.profitTop,
-        to: geometry.profitBottom,
-        color: colors.axisMarkerAreaFill,
-      },
-      {
-        from: geometry.lossTop,
-        to: geometry.lossBottom,
-        color: colors.axisMarkerAreaFill,
-      },
+      this.createAxisSegment(geometry.profitTop, geometry.profitBottom),
+      this.createAxisSegment(geometry.lossTop, geometry.lossBottom),
     ];
   }
 
   protected getTimeAxisLabel(kind: string): AxisLabel | null {
-    if (!this.isSelected() || (kind !== 'start' && kind !== 'end')) {
+    if (!this.isAxisLabelAvailable() || !this.isSelected() || (kind !== 'start' && kind !== 'end')) {
       return null;
     }
 
     const labelKind = kind as TimeLabelKind;
-    const coordinate = this.getTimeCoordinate(labelKind);
-    const text = this.getTimeText(labelKind);
-
-    if (coordinate === null || !text) {
-      return null;
-    }
 
-    const { colors } = getThemeStore();
-
-    return {
-      coordinate,
-      text,
-      textColor: colors.chartPriceLineText,
-      backgroundColor: colors.axisMarkerLabelFill,
-    };
+    return this.createAxisLabel(this.getTimeCoordinate(labelKind), this.getTimeText(labelKind));
   }
 
   protected getPriceAxisLabel(kind: string): AxisLabel | null {
-    if (kind !== 'target' && kind !== 'entry' && kind !== 'stop') {
+    if (!this.isAxisLabelAvailable() || (kind !== 'target' && kind !== 'entry' && kind !== 'stop')) {
       return null;
     }
 
     const labelKind = kind as PriceLabelKind;
-    const coordinate = this.getPriceCoordinate(labelKind);
-    const text = this.getPriceText(labelKind);
-
-    if (coordinate === null || !text) {
-      return null;
-    }
-
     const { colors } = getThemeStore();
+
     let backgroundColor = colors.axisMarkerLabelDefaultFill;
 
     if (labelKind === 'target') {
@@ -580,12 +481,9 @@ export class SliderPosition
       backgroundColor = colors.axisMarkerLabelNegativeFill;
     }
 
-    return {
-      coordinate,
-      text,
-      textColor: colors.chartPriceLineText,
+    return this.createAxisLabel(this.getPriceCoordinate(labelKind), this.getPriceText(labelKind), {
       backgroundColor,
-    };
+    });
   }
 
   protected bindEvents(): void {
@@ -594,6 +492,7 @@ export class SliderPosition
     }
 
     super.bindEvents();
+
     this.chart.subscribeClick(this.clickHandler);
   }
 
@@ -603,6 +502,7 @@ export class SliderPosition
     }
 
     this.chart.unsubscribeClick(this.clickHandler);
+
     super.unbindEvents();
   }
 
@@ -617,8 +517,7 @@ export class SliderPosition
       return;
     }
 
-    event.preventDefault();
-    event.stopPropagation();
+    this.consumeEvent(event);
     this.openSettings?.();
   };
 
@@ -629,14 +528,7 @@ export class SliderPosition
 
     const point = this.getEventPoint(event);
 
-    if (!this.isSelected()) {
-      if (!this.containsPoint(point)) {
-        return;
-      }
-
-      event.preventDefault();
-      event.stopPropagation();
-      this.select();
+    if (this.selectOnPointerDown(event, () => this.containsPoint(point))) {
       return;
     }
 
@@ -647,9 +539,7 @@ export class SliderPosition
       return;
     }
 
-    event.preventDefault();
-    event.stopPropagation();
-
+    this.consumeEvent(event);
     this.activeDragTarget = dragTarget;
     this.dragPointerId = event.pointerId;
     this.dragStartPoint = point;
@@ -663,6 +553,7 @@ export class SliderPosition
     }
 
     event.preventDefault();
+
     this.applyDrag(this.getEventPoint(event));
     this.render();
   };
@@ -677,7 +568,6 @@ export class SliderPosition
     this.dragStartPoint = null;
     this.dragStateSnapshot = null;
     this.mode = 'ready';
-
     this.resolveReady?.();
     this.render();
   };
@@ -740,6 +630,7 @@ export class SliderPosition
 
   private handleChartClick(params: MouseEventParams<Time>): void {
     // todo: привести к общему виду дровингов как handleChartClick => startDrawing
+
     if (this.hidden || !params.point || this.mode !== 'idle') {
       return;
     }
@@ -761,11 +652,11 @@ export class SliderPosition
 
     const stopDirection = this.side === 'long' ? -1 : 1;
     const targetDirection = -stopDirection;
-
     this.startTime = anchor.time;
     this.endTime = this.shiftTime(anchor.time, INITIAL_WIDTH_PX) ?? anchor.time;
     this.entryPrice = anchor.price;
     this.stopPrice = this.normalizeStop(anchor.price, anchor.price + distance * stopDirection);
+
     this.targetPrice = this.normalizeTarget(
       anchor.price,
       anchor.price + distance * targetDirection * DEFAULT_RISK_REWARD_RATIO,
@@ -787,18 +678,23 @@ export class SliderPosition
       case 'body':
         this.moveWhole(snapshot, point);
         break;
+
       case 'entry':
         this.moveEntry(snapshot, point);
         break;
+
       case 'target':
         this.moveTarget(snapshot, point);
         break;
+
       case 'stop':
         this.moveStop(snapshot, point);
         break;
+
       case 'end':
         this.resizeEnd(snapshot, point);
         break;
+
       default:
         break;
     }
@@ -816,9 +712,7 @@ export class SliderPosition
     }
 
     const minDistance = getPricePrecisionStep();
-
     const minPrice = this.side === 'long' ? snapshot.stopPrice + minDistance : snapshot.targetPrice;
-
     const maxPrice = this.side === 'long' ? snapshot.targetPrice : snapshot.stopPrice - minDistance;
 
     if (minPrice > maxPrice) {
@@ -844,7 +738,6 @@ export class SliderPosition
 
     const timeOffset = point.x - this.dragStartPoint.x;
     const priceOffset = getPriceDeltaFromCoordinates(this.series, this.dragStartPoint.y, point.y);
-
     const nextStartTime = this.shiftTime(snapshot.startTime, timeOffset);
     const nextEndTime = this.shiftTime(snapshot.endTime, timeOffset);
 
@@ -853,7 +746,9 @@ export class SliderPosition
     }
 
     let nextEntryPrice = snapshot.entryPrice + priceOffset;
+
     let nextStopPrice = snapshot.stopPrice + priceOffset;
+
     let nextTargetPrice = snapshot.targetPrice + priceOffset;
 
     const range = getPriceRangeInContainer(this.series, this.container);
@@ -866,7 +761,9 @@ export class SliderPosition
         const shift = range.min - minValue;
 
         nextEntryPrice += shift;
+
         nextStopPrice += shift;
+
         nextTargetPrice += shift;
       }
 
@@ -874,7 +771,9 @@ export class SliderPosition
         const shift = maxValue - range.max;
 
         nextEntryPrice -= shift;
+
         nextStopPrice -= shift;
+
         nextTargetPrice -= shift;
       }
     }
@@ -925,7 +824,10 @@ export class SliderPosition
     this.endTime = nextEndTime;
   }
 
-  private createAnchor(params: MouseEventParams<Time>): { time: Time; price: number } | null {
+  private createAnchor(params: MouseEventParams<Time>): {
+    time: Time;
+    price: number;
+  } | null {
     if (!params.point || params.time === undefined) {
       return null;
     }
@@ -980,9 +882,7 @@ export class SliderPosition
 
     const minPrice = Math.min(upperPrice, lowerPrice);
     const maxPrice = Math.max(upperPrice, lowerPrice);
-
     const stopDistance = this.side === 'long' ? entryPrice - minPrice : maxPrice - entryPrice;
-
     const targetDistance = this.side === 'long' ? maxPrice - entryPrice : entryPrice - minPrice;
 
     if (stopDistance <= 0 || targetDistance <= 0) {
@@ -1018,6 +918,7 @@ export class SliderPosition
     }
 
     const data = this.series.data() ?? [];
+
     let lastPrice: number | null = null;
 
     for (const item of data) {
@@ -1157,7 +1058,6 @@ export class SliderPosition
   private getTargetText(geometry: SliderGeometry, amount: number): string {
     const diff = Math.abs(geometry.targetPrice - geometry.entryPrice);
     const percent = geometry.entryPrice !== 0 ? (diff / Math.abs(geometry.entryPrice)) * 100 : 0;
-
     const formattedDiff = formatPrice(diff) ?? '0';
     const formattedAmount = formatPrice(amount, 2) ?? '0';
 
@@ -1177,7 +1077,6 @@ export class SliderPosition
   private getStopText(geometry: SliderGeometry, amount: number): string {
     const diff = Math.abs(geometry.stopPrice - geometry.entryPrice);
     const percent = geometry.entryPrice !== 0 ? (diff / Math.abs(geometry.entryPrice)) * 100 : 0;
-
     const formattedDiff = formatPrice(diff) ?? '0';
     const formattedAmount = formatPrice(amount, 2) ?? '0';
 
diff --git a/src/core/Drawings/text/text.ts b/src/core/Drawings/text/text.ts
index af23c0f8a0a4d804466da1a844b3ba79401dd14d..03c59c46ef8dfb2eb95b2ab6be10cfd3ddff0b65 100644
--- a/src/core/Drawings/text/text.ts
+++ b/src/core/Drawings/text/text.ts
@@ -1,29 +1,24 @@
-import { IPrimitivePaneView, PrimitiveHoveredItem, TouchMouseEventData, UTCTimestamp } from 'lightweight-charts';
+import { IPrimitivePaneView, PrimitiveHoveredItem, UTCTimestamp } from 'lightweight-charts';
 import { clamp } from 'lodash-es';
 
 import { CustomPriceAxisView, CustomTimeAxisView } from '@core/Drawings/axis';
 import {
-  clampPointToContainer as clampPointToContainerInElement,
-  getAnchorFromPoint,
-  getContainerSize as getElementContainerSize,
+  clampPointToContainer,
+  getContainerSize,
   getXCoordinateFromTime,
   getYCoordinateFromPrice,
   isPointInBounds,
 } from '@core/Drawings/helpers';
-import { AxisSegment } from '@core/Drawings/types';
 import { updateViews } from '@core/Drawings/utils';
-import { SeriesDrawingBase } from '@src/core/Drawings/SeriesDrawingBase';
 
-import { getThemeStore } from '@src/theme';
-import { SettingsTab } from '@src/types';
-import { Defaults } from '@src/types/defaults';
+import { BaseDrawingParams, DrawingBase, ISeriesDrawing } from '@src/core/Drawings/base';
+import { Defaults, SettingsTab } from '@src/types';
 import { formatDate, formatPrice } from '@src/utils';
 
 import { TextPaneView } from './paneView';
 import { createDefaultSettings, getTextSettingsTabs, TextContentStyle, TextSettings, TextStyle } from './settings';
 
-import type { Anchor, AxisLabel, Point } from '@core/Drawings/types';
-import type { BaseDrawingParams, ISeriesDrawing } from '@src/core/Drawings/SeriesDrawingBase';
+import type { Anchor, AxisLabel, AxisSegment, Point } from '@core/Drawings/types';
 import type { ChartOptionsModel } from '@src/types';
 
 type TextMode = 'idle' | 'dragging' | 'ready';
@@ -60,11 +55,16 @@ const UI = {
 
 let measureCanvas: HTMLCanvasElement | null = null;
 
-export class Text extends SeriesDrawingBase<TextSettings> implements ISeriesDrawing {
-  private readonly removeSelf?: () => void;
+export class Text extends DrawingBase<TextSettings> implements ISeriesDrawing {
   private readonly openSettings?: () => void;
 
   protected mode: TextMode = 'idle';
+  protected settings: TextSettings = createDefaultSettings();
+
+  private point: Anchor | null = null;
+  private dragPointerId: number | null = null;
+  private dragStartPoint: Point | null = null;
+  private dragGeometrySnapshot: TextGeometry | null = null;
 
   private displayFormat: ChartOptionsModel = {
     dateFormat: Defaults.dateFormat,
@@ -72,27 +72,11 @@ export class Text extends SeriesDrawingBase<TextSettings> implements ISeriesDraw
     showTime: Defaults.showTime,
   };
 
-  private point: Anchor | null = null;
-  protected settings: TextSettings = createDefaultSettings();
-
-  private dragPointerId: number | null = null;
-  private dragStartPoint: Point | null = null;
-  private dragGeometrySnapshot: TextGeometry | null = null;
-
   private readonly paneView: TextPaneView;
   private readonly timeAxisView: CustomTimeAxisView;
   private readonly priceAxisView: CustomPriceAxisView;
 
-  constructor({
-    chart,
-    series,
-    formatObservable,
-    removeSelf,
-    openSettings,
-    container,
-    interaction,
-    initialEvent,
-  }: TextParams) {
+  constructor({ chart, series, formatObservable, openSettings, container, interaction, initialEvent }: TextParams) {
     super({
       chart,
       series,
@@ -100,35 +84,19 @@ export class Text extends SeriesDrawingBase<TextSettings> implements ISeriesDraw
       interaction,
     });
 
-    this.removeSelf = removeSelf;
     this.openSettings = openSettings;
-
     this.paneView = new TextPaneView(this);
+    this.timeAxisView = this.createTimeAxisView('main');
+    this.priceAxisView = this.createPriceAxisView('main');
 
-    this.timeAxisView = new CustomTimeAxisView({
-      getAxisLabel: (kind) => this.getTimeAxisLabel(kind),
-      labelKind: 'main',
-    });
-
-    this.priceAxisView = new CustomPriceAxisView({
-      getAxisLabel: (kind) => this.getPriceAxisLabel(kind),
-      labelKind: 'main',
+    this.subscribeFormat(formatObservable, (format) => {
+      this.displayFormat = format;
     });
 
-    if (formatObservable) {
-      this.subscriptions.add(
-        formatObservable.subscribe((format) => {
-          this.displayFormat = format;
-          this.render();
-        }),
-      );
-    }
-
     this.series.attachPrimitive(this);
 
-    if (initialEvent && initialEvent.sourceEvent) {
-      const point = this.getEventPoint(initialEvent.sourceEvent);
-      this.startDrawing(point);
+    if (initialEvent?.sourceEvent) {
+      this.startDrawing(this.getEventPoint(initialEvent.sourceEvent));
     }
   }
 
@@ -155,10 +123,12 @@ export class Text extends SeriesDrawingBase<TextSettings> implements ISeriesDraw
     }
 
     const nextState = state as Partial<TextState>;
-
     this.hidden = typeof nextState.hidden === 'boolean' ? nextState.hidden : this.hidden;
     this.mode = nextState.mode ?? this.mode;
-    this.point = nextState.point ?? this.point;
+
+    if ('point' in nextState) {
+      this.point = nextState.point ?? null;
+    }
 
     if (nextState.settings) {
       this.settings = {
@@ -213,11 +183,7 @@ export class Text extends SeriesDrawingBase<TextSettings> implements ISeriesDraw
   }
 
   protected getHoveredItem(x: number, y: number): PrimitiveHoveredItem | null {
-    if (this.hidden || this.mode === 'idle') {
-      return null;
-    }
-
-    if (!this.containsPoint({ x, y })) {
+    if (this.hidden || this.mode === 'idle' || !this.containsPoint({ x, y })) {
       return null;
     }
 
@@ -229,50 +195,37 @@ export class Text extends SeriesDrawingBase<TextSettings> implements ISeriesDraw
   }
 
   protected getTimeAxisLabel(kind: string): AxisLabel | null {
-    if (kind !== 'main' || !this.isSelected() || !this.point || typeof this.point.time !== 'number') {
+    if (
+      !this.isAxisLabelAvailable() ||
+      kind !== 'main' ||
+      !this.isSelected() ||
+      !this.point ||
+      typeof this.point.time !== 'number'
+    ) {
       return null;
     }
 
     const coordinate = getXCoordinateFromTime(this.chart, this.point.time, this.series);
 
-    if (coordinate === null) {
-      return null;
-    }
-
-    const { colors } = getThemeStore();
-
-    return {
-      coordinate,
-      text: formatDate(
+    return this.createAxisLabel(
+      coordinate === null ? null : Number(coordinate),
+      formatDate(
         this.point.time as UTCTimestamp,
         this.displayFormat.dateFormat,
         this.displayFormat.timeFormat,
         this.displayFormat.showTime,
       ),
-      textColor: colors.chartPriceLineText,
-      backgroundColor: colors.axisMarkerLabelFill,
-    };
+    );
   }
 
   protected getPriceAxisLabel(kind: string): AxisLabel | null {
-    if (kind !== 'main' || !this.isSelected() || !this.point) {
+    if (!this.isAxisLabelAvailable() || kind !== 'main' || !this.isSelected() || !this.point) {
       return null;
     }
 
     const coordinate = getYCoordinateFromPrice(this.series, this.point.price);
 
-    if (coordinate === null) {
-      return null;
-    }
-
-    const { colors } = getThemeStore();
-
-    return {
-      coordinate,
-      text: formatPrice(this.point.price) ?? '',
-      textColor: colors.chartPriceLineText,
-      backgroundColor: colors.axisMarkerLabelFill,
-    };
+    return this.createAxisLabel(coordinate === null ? null : Number(coordinate), formatPrice(this.point.price) ?? '');
   }
 
   protected getPriceAxisSegments(): AxisSegment[] {
@@ -291,9 +244,7 @@ export class Text extends SeriesDrawingBase<TextSettings> implements ISeriesDraw
     const point = this.getEventPoint(event);
 
     if (this.mode === 'idle') {
-      event.preventDefault();
-      event.stopPropagation();
-
+      this.consumeEvent(event);
       this.startDrawing(point);
       return;
     }
@@ -309,17 +260,13 @@ export class Text extends SeriesDrawingBase<TextSettings> implements ISeriesDraw
         return;
       }
 
-      event.preventDefault();
-      event.stopPropagation();
-
+      this.consumeEvent(event);
       this.select();
       return;
     }
 
     if (containsPoint) {
-      event.preventDefault();
-      event.stopPropagation();
-
+      this.consumeEvent(event);
       this.startDragging(point, event.pointerId);
       return;
     }
@@ -338,9 +285,7 @@ export class Text extends SeriesDrawingBase<TextSettings> implements ISeriesDraw
       return;
     }
 
-    event.preventDefault();
-    event.stopPropagation();
-
+    this.consumeEvent(event);
     this.openSettings?.();
   };
 
@@ -356,18 +301,16 @@ export class Text extends SeriesDrawingBase<TextSettings> implements ISeriesDraw
   };
 
   protected handlePointerUp = (event: PointerEvent): void => {
-    if (this.dragPointerId !== event.pointerId) {
+    if (this.dragPointerId !== event.pointerId || this.mode !== 'dragging') {
       return;
     }
 
-    if (this.mode === 'dragging') {
-      this.finishDragging();
-    }
+    this.finishDragging();
   };
 
   private startDrawing(point: Point): void {
     // todo: вынести в абстрактный класс абстрактным методом (и в соседних классах)
-    const anchor = this.createAnchor(this.clampPointToContainer(point));
+    const anchor = this.createAnchorFromPoint(clampPointToContainer(point, this.container));
 
     if (!anchor) {
       return;
@@ -376,7 +319,6 @@ export class Text extends SeriesDrawingBase<TextSettings> implements ISeriesDraw
     this.point = anchor;
     this.mode = 'ready';
     this.resolveReady?.();
-
     this.render();
   }
 
@@ -385,7 +327,6 @@ export class Text extends SeriesDrawingBase<TextSettings> implements ISeriesDraw
     this.dragPointerId = pointerId;
     this.dragStartPoint = point;
     this.dragGeometrySnapshot = this.getGeometry();
-
     this.hideCrosshair();
     this.render();
   }
@@ -393,11 +334,9 @@ export class Text extends SeriesDrawingBase<TextSettings> implements ISeriesDraw
   private finishDragging(): void {
     this.mode = 'ready';
     this.resolveReady?.();
-
     this.dragPointerId = null;
     this.dragStartPoint = null;
     this.dragGeometrySnapshot = null;
-
     this.showCrosshair();
     this.render();
   }
@@ -410,15 +349,13 @@ export class Text extends SeriesDrawingBase<TextSettings> implements ISeriesDraw
       return;
     }
 
-    const { width, height } = this.getContainerSize();
-
+    const { width, height } = getContainerSize(this.container);
     const offsetX = eventPoint.x - dragStartPoint.x;
     const offsetY = eventPoint.y - dragStartPoint.y;
-
     const nextLeft = clamp(geometry.left + offsetX, 0, Math.max(0, width - geometry.width));
     const nextTop = clamp(geometry.top + offsetY, 0, Math.max(0, height - geometry.height));
 
-    const anchor = this.createAnchor({
+    const anchor = this.createAnchorFromPoint({
       x: nextLeft,
       y: nextTop,
     });
@@ -430,48 +367,38 @@ export class Text extends SeriesDrawingBase<TextSettings> implements ISeriesDraw
     this.point = anchor;
   }
 
-  private createAnchor(point: Point): Anchor | null {
-    return getAnchorFromPoint(this.chart, this.series, point);
-  }
-
   protected getGeometry(): TextGeometry | null {
-    if (!this.point) {
-      return null;
-    }
-
-    const { width: containerWidth, height: containerHeight } = this.getContainerSize();
-
-    const x = getXCoordinateFromTime(this.chart, this.point.time, this.series);
-    const y = getYCoordinateFromPrice(this.series, this.point.price);
+    const projectedPoint = this.getPointFromAnchor(this.point);
 
-    if (x === null || y === null) {
+    if (!projectedPoint) {
       return null;
     }
 
+    const { width: containerWidth, height: containerHeight } = getContainerSize(this.container);
+
     const anchorPoint: Point = {
-      x: clamp(Math.round(Number(x)), 0, containerWidth),
-      y: clamp(Math.round(Number(y)), 0, containerHeight),
+      x: clamp(Math.round(projectedPoint.x), 0, containerWidth),
+      y: clamp(Math.round(projectedPoint.y), 0, containerHeight),
     };
 
     const lines = getTextLines(this.settings.text);
     const font = getFont(this.settings);
     const lineHeight = this.settings.fontSize;
     const measured = measureTextBlock(lines, font, lineHeight);
-
     const width = Math.min(containerWidth, measured.width + UI.padding * 2);
     const height = Math.min(containerHeight, measured.height + UI.padding * 2);
-
     const left = clamp(anchorPoint.x, 0, Math.max(0, containerWidth - width));
     const top = clamp(anchorPoint.y, 0, Math.max(0, containerHeight - height));
-    const right = left + width;
-    const bottom = top + height;
 
     return {
-      point: { x: left, y: top },
+      point: {
+        x: left,
+        y: top,
+      },
       left,
-      right,
+      right: left + width,
       top,
-      bottom,
+      bottom: top + height,
       width,
       height,
       lines,
@@ -483,19 +410,7 @@ export class Text extends SeriesDrawingBase<TextSettings> implements ISeriesDraw
   private containsPoint(point: Point): boolean {
     const geometry = this.getGeometry();
 
-    if (!geometry) {
-      return false;
-    }
-
-    return isPointInBounds(point, geometry, 2);
-  }
-
-  private getContainerSize(): { width: number; height: number } {
-    return getElementContainerSize(this.container);
-  }
-
-  private clampPointToContainer(point: Point): Point {
-    return clampPointToContainerInElement(point, this.container);
+    return geometry !== null && isPointInBounds(point, geometry, 2);
   }
 }
 
@@ -512,7 +427,14 @@ function getFont(settings: TextSettings): string {
   return `${italic}${bold}${settings.fontSize}px Inter, sans-serif`;
 }
 
-function measureTextBlock(lines: string[], font: string, lineHeight: number): { width: number; height: number } {
+function measureTextBlock(
+  lines: string[],
+  font: string,
+  lineHeight: number,
+): {
+  width: number;
+  height: number;
+} {
   const context = getMeasureContext();
 
   if (!context) {
diff --git a/src/core/Drawings/traectory/traectory.ts b/src/core/Drawings/traectory/traectory.ts
index 9ad386fdb6a14c7c2e036f78d747caec62bb4129..8d2fb230572b8d8ba90ae0309c7ca6a990f4c88d 100644
--- a/src/core/Drawings/traectory/traectory.ts
+++ b/src/core/Drawings/traectory/traectory.ts
@@ -1,33 +1,21 @@
-import { IPrimitivePaneView, PrimitiveHoveredItem } from 'lightweight-charts';
+import { PrimitiveHoveredItem } from 'lightweight-charts';
 import { clamp } from 'lodash-es';
 
-import { CustomPriceAxisPaneView, CustomTimeAxisPaneView } from '@core/Drawings/axis';
-import {
-  clampPointToContainer as clampPointToContainerInElement,
-  getAnchorFromPoint,
-  getContainerSize as getElementContainerSize,
-  getXCoordinateFromTime,
-  getYCoordinateFromPrice,
-  isNearPoint,
-} from '@core/Drawings/helpers';
-import { AxisLabel } from '@core/Drawings/types';
-import { getDistanceToSegment, updateViews } from '@core/Drawings/utils';
-import { SeriesDrawingBase } from '@src/core/Drawings/SeriesDrawingBase';
-
-import { getThemeStore } from '@src/theme';
+import { clampPointToContainer, getContainerSize, isNearPoint } from '@core/Drawings/helpers';
+import { getDistanceToSegment } from '@core/Drawings/utils';
 
-import { TraectoryPaneView } from './paneView';
+import { BaseDrawingParams, DrawingBase, ISeriesDrawing } from '@src/core/Drawings/base';
 
+import { TraectoryPaneView } from './paneView';
 import { createDefaultSettings, getTraectorySettingsTabs, TraectorySettings, TraectoryStyle } from './settings';
 
 import type { DrawingHandle } from '@core/Drawings/handles';
-import type { Anchor, AxisSegment, Point } from '@core/Drawings/types';
-import type { BaseDrawingParams, ISeriesDrawing } from '@src/core/Drawings/SeriesDrawingBase';
+import type { Anchor, AxisLabel, AxisSegment, Point } from '@core/Drawings/types';
 import type { SettingsTab } from '@src/types';
 
 type TraectoryMode = 'idle' | 'drawing' | 'ready' | 'dragging-point' | 'dragging-body';
-type TraectoryHandleId = `${number}`;
 
+type TraectoryHandleId = `${number}`;
 type TraectoryParams = BaseDrawingParams;
 
 export interface TraectoryState {
@@ -54,11 +42,12 @@ const POINT_HIT_TOLERANCE = 8;
 const SEGMENT_HIT_TOLERANCE = 6;
 const MIN_POINTS_COUNT = 2;
 
-export class Traectory extends SeriesDrawingBase<TraectorySettings, TraectoryHandleId> implements ISeriesDrawing {
+export class Traectory extends DrawingBase<TraectorySettings, TraectoryHandleId> implements ISeriesDrawing {
   private removeSelf?: () => void;
   private openSettings?: () => void;
 
   protected settings: TraectorySettings = createDefaultSettings();
+
   protected mode: TraectoryMode = 'idle';
 
   private points: Anchor[] = [];
@@ -69,10 +58,6 @@ export class Traectory extends SeriesDrawingBase<TraectorySettings, TraectoryHan
   private dragPointIndex: number | null = null;
   private dragGeometrySnapshot: TraectoryGeometry | null = null;
 
-  private readonly paneView: TraectoryPaneView;
-  private readonly timeAxisPaneView: CustomTimeAxisPaneView;
-  private readonly priceAxisPaneView: CustomPriceAxisPaneView;
-
   constructor({ chart, series, container, interaction, removeSelf, openSettings, initialEvent }: TraectoryParams) {
     super({
       chart,
@@ -83,22 +68,11 @@ export class Traectory extends SeriesDrawingBase<TraectorySettings, TraectoryHan
 
     this.removeSelf = removeSelf;
     this.openSettings = openSettings;
-
-    this.paneView = new TraectoryPaneView(this);
-
-    this.timeAxisPaneView = new CustomTimeAxisPaneView({
-      getAxisSegments: () => this.getTimeAxisSegments(),
-    });
-
-    this.priceAxisPaneView = new CustomPriceAxisPaneView({
-      getAxisSegments: () => this.getPriceAxisSegments(),
-    });
-
+    this.initializeViews(new TraectoryPaneView(this), [], []);
     this.series.attachPrimitive(this);
 
-    if (initialEvent && initialEvent.sourceEvent) {
-      const point = this.getEventPoint(initialEvent.sourceEvent);
-      this.startDrawing(point);
+    if (initialEvent?.sourceEvent) {
+      this.startDrawing(this.getEventPoint(initialEvent.sourceEvent));
     }
   }
 
@@ -111,7 +85,9 @@ export class Traectory extends SeriesDrawingBase<TraectorySettings, TraectoryHan
       hidden: this.hidden,
       mode: this.mode,
       points: this.points,
-      settings: { ...this.settings },
+      settings: {
+        ...this.settings,
+      },
     };
   }
 
@@ -121,7 +97,6 @@ export class Traectory extends SeriesDrawingBase<TraectorySettings, TraectoryHan
     }
 
     const nextState = state as Partial<TraectoryState>;
-
     this.hidden = typeof nextState.hidden === 'boolean' ? nextState.hidden : this.hidden;
     this.mode = nextState.mode ?? this.mode;
     this.points = Array.isArray(nextState.points) ? nextState.points : this.points;
@@ -140,30 +115,6 @@ export class Traectory extends SeriesDrawingBase<TraectorySettings, TraectoryHan
     return getTraectorySettingsTabs(this.settings);
   }
 
-  public updateAllViews(): void {
-    updateViews([this.paneView, this.timeAxisPaneView, this.priceAxisPaneView]);
-  }
-
-  public paneViews(): readonly IPrimitivePaneView[] {
-    return [this.paneView];
-  }
-
-  public timeAxisPaneViews(): readonly IPrimitivePaneView[] {
-    return [this.timeAxisPaneView];
-  }
-
-  public priceAxisPaneViews(): readonly IPrimitivePaneView[] {
-    return [this.priceAxisPaneView];
-  }
-
-  public timeAxisViews() {
-    return [];
-  }
-
-  public priceAxisViews() {
-    return [];
-  }
-
   public getRenderData(): TraectoryRenderData | null {
     if (this.hidden) {
       return null;
@@ -204,7 +155,10 @@ export class Traectory extends SeriesDrawingBase<TraectorySettings, TraectoryHan
       return null;
     }
 
-    const point = { x, y };
+    const point = {
+      x,
+      y,
+    };
 
     if (!this.isSelected()) {
       if (!this.isPointNearTraectory(point)) {
@@ -238,54 +192,18 @@ export class Traectory extends SeriesDrawingBase<TraectorySettings, TraectoryHan
   }
 
   protected getTimeAxisSegments(): AxisSegment[] {
-    if (!this.isSelected() && !this.isCreationPending()) {
-      return [];
-    }
-
-    const geometry = this.getGeometry();
-
-    if (!geometry) {
-      return [];
-    }
-
-    const { colors } = getThemeStore();
-
-    return [
-      {
-        from: geometry.left,
-        to: geometry.right,
-        color: colors.axisMarkerAreaFill,
-      },
-    ];
+    return this.getBoundsAxisSegments('time', () => this.getGeometry());
   }
 
   protected getPriceAxisSegments(): AxisSegment[] {
-    if (!this.isSelected() && !this.isCreationPending()) {
-      return [];
-    }
-
-    const geometry = this.getGeometry();
-
-    if (!geometry) {
-      return [];
-    }
-
-    const { colors } = getThemeStore();
-
-    return [
-      {
-        from: geometry.top,
-        to: geometry.bottom,
-        color: colors.axisMarkerAreaFill,
-      },
-    ];
+    return this.getBoundsAxisSegments('price', () => this.getGeometry());
   }
 
-  protected getPriceAxisLabel(kind: string): AxisLabel | null {
+  protected getPriceAxisLabel(_kind: string): AxisLabel | null {
     return null;
   }
 
-  protected getTimeAxisLabel(kind: string): AxisLabel | null {
+  protected getTimeAxisLabel(_kind: string): AxisLabel | null {
     return null;
   }
 
@@ -294,12 +212,8 @@ export class Traectory extends SeriesDrawingBase<TraectorySettings, TraectoryHan
       return;
     }
 
-    event.preventDefault();
-    event.stopPropagation();
-
-    const point = this.getEventPoint(event as PointerEvent);
-
-    this.appendPoint(point);
+    this.consumeEvent(event);
+    this.appendPoint(this.getEventPoint(event as PointerEvent));
   };
 
   protected handleDoubleClick = (event: MouseEvent): void => {
@@ -308,9 +222,7 @@ export class Traectory extends SeriesDrawingBase<TraectorySettings, TraectoryHan
     }
 
     if (this.mode === 'drawing') {
-      event.preventDefault();
-      event.stopPropagation();
-
+      this.consumeEvent(event);
       this.finishDrawing();
       return;
     }
@@ -325,9 +237,7 @@ export class Traectory extends SeriesDrawingBase<TraectorySettings, TraectoryHan
       return;
     }
 
-    event.preventDefault();
-    event.stopPropagation();
-
+    this.consumeEvent(event);
     this.openSettings?.();
   };
 
@@ -336,9 +246,7 @@ export class Traectory extends SeriesDrawingBase<TraectorySettings, TraectoryHan
       return;
     }
 
-    event.preventDefault();
-    event.stopPropagation();
-
+    this.consumeEvent(event);
     this.finishDrawing();
   };
 
@@ -350,9 +258,7 @@ export class Traectory extends SeriesDrawingBase<TraectorySettings, TraectoryHan
     const point = this.getEventPoint(event);
 
     if (this.mode === 'idle') {
-      event.preventDefault();
-      event.stopPropagation();
-
+      this.consumeEvent(event);
       this.startDrawing(point);
       return;
     }
@@ -365,24 +271,14 @@ export class Traectory extends SeriesDrawingBase<TraectorySettings, TraectoryHan
       return;
     }
 
-    if (!this.isSelected()) {
-      if (!this.isPointNearTraectory(point)) {
-        return;
-      }
-
-      event.preventDefault();
-      event.stopPropagation();
-
-      this.select();
+    if (this.selectOnPointerDown(event, () => this.isPointNearTraectory(point))) {
       return;
     }
 
     const pointIndex = this.getPointIndexAt(point);
 
     if (pointIndex !== null) {
-      event.preventDefault();
-      event.stopPropagation();
-
+      this.consumeEvent(event);
       this.startDraggingPoint(point, event.pointerId, pointIndex);
       return;
     }
@@ -392,9 +288,7 @@ export class Traectory extends SeriesDrawingBase<TraectorySettings, TraectoryHan
       return;
     }
 
-    event.preventDefault();
-    event.stopPropagation();
-
+    this.consumeEvent(event);
     this.startDraggingBody(point, event.pointerId);
   };
 
@@ -412,6 +306,7 @@ export class Traectory extends SeriesDrawingBase<TraectorySettings, TraectoryHan
 
     if (this.mode === 'dragging-point') {
       event.preventDefault();
+
       this.movePoint(point);
       this.render();
       return;
@@ -419,6 +314,7 @@ export class Traectory extends SeriesDrawingBase<TraectorySettings, TraectoryHan
 
     if (this.mode === 'dragging-body') {
       event.preventDefault();
+
       this.moveBody(point);
       this.render();
     }
@@ -435,7 +331,7 @@ export class Traectory extends SeriesDrawingBase<TraectorySettings, TraectoryHan
   };
 
   private startDrawing(point: Point): void {
-    const anchor = this.createAnchor(this.clampPointToContainer(point));
+    const anchor = this.createAnchorFromPoint(clampPointToContainer(point, this.container));
 
     if (!anchor) {
       return;
@@ -444,12 +340,11 @@ export class Traectory extends SeriesDrawingBase<TraectorySettings, TraectoryHan
     this.points = [anchor];
     this.previewAnchor = anchor;
     this.mode = 'drawing';
-
     this.render();
   }
 
   private appendPoint(point: Point): void {
-    const anchor = this.createAnchor(this.clampPointToContainer(point));
+    const anchor = this.createAnchorFromPoint(clampPointToContainer(point, this.container));
 
     if (!anchor) {
       return;
@@ -469,7 +364,7 @@ export class Traectory extends SeriesDrawingBase<TraectorySettings, TraectoryHan
   }
 
   private updatePreview(point: Point): void {
-    const anchor = this.createAnchor(this.clampPointToContainer(point));
+    const anchor = this.createAnchorFromPoint(clampPointToContainer(point, this.container));
 
     if (!anchor) {
       return;
@@ -493,30 +388,25 @@ export class Traectory extends SeriesDrawingBase<TraectorySettings, TraectoryHan
     this.previewAnchor = null;
     this.mode = 'ready';
     this.resolveReady?.();
-
     this.render();
   }
 
   private startDraggingPoint(point: Point, pointerId: number, pointIndex: number): void {
     this.mode = 'dragging-point';
-
     this.dragPointerId = pointerId;
     this.dragStartPoint = point;
     this.dragPointIndex = pointIndex;
     this.dragGeometrySnapshot = this.getGeometry();
-
     this.hideCrosshair();
     this.render();
   }
 
   private startDraggingBody(point: Point, pointerId: number): void {
     this.mode = 'dragging-body';
-
     this.dragPointerId = pointerId;
     this.dragStartPoint = point;
     this.dragPointIndex = null;
     this.dragGeometrySnapshot = this.getGeometry();
-
     this.hideCrosshair();
     this.render();
   }
@@ -524,26 +414,22 @@ export class Traectory extends SeriesDrawingBase<TraectorySettings, TraectoryHan
   private finishDragging(): void {
     this.mode = 'ready';
     this.resolveReady?.();
-
     this.dragPointerId = null;
     this.dragStartPoint = null;
     this.dragPointIndex = null;
     this.dragGeometrySnapshot = null;
-
     this.showCrosshair();
     this.render();
   }
 
   private resetToIdle(): void {
     this.mode = 'idle';
-
     this.points = [];
     this.previewAnchor = null;
     this.dragPointerId = null;
     this.dragStartPoint = null;
     this.dragPointIndex = null;
     this.dragGeometrySnapshot = null;
-
     this.showCrosshair();
     this.render();
   }
@@ -556,10 +442,9 @@ export class Traectory extends SeriesDrawingBase<TraectorySettings, TraectoryHan
       return;
     }
 
-    const nextPoint = this.clampPointToContainer(point);
     const nextPoints = [...geometry.points];
 
-    nextPoints[pointIndex] = nextPoint;
+    nextPoints[pointIndex] = clampPointToContainer(point, this.container);
 
     this.setAnchorsFromPoints(nextPoints);
   }
@@ -572,20 +457,21 @@ export class Traectory extends SeriesDrawingBase<TraectorySettings, TraectoryHan
       return;
     }
 
-    const { width, height } = this.getContainerSize();
-
+    const { width, height } = getContainerSize(this.container);
     const rawOffsetX = point.x - dragStartPoint.x;
     const rawOffsetY = point.y - dragStartPoint.y;
-
     const offsetX = clamp(rawOffsetX, -geometry.left, width - geometry.right);
     const offsetY = clamp(rawOffsetY, -geometry.top, height - geometry.bottom);
 
-    const nextPoints = geometry.points.map((item) =>
-      this.clampPointToContainer({
-        x: item.x + offsetX,
-        y: item.y + offsetY,
-      }),
-    );
+    const nextPoints = geometry.points.map((item) => {
+      return clampPointToContainer(
+        {
+          x: item.x + offsetX,
+          y: item.y + offsetY,
+        },
+        this.container,
+      );
+    });
 
     this.setAnchorsFromPoints(nextPoints);
   }
@@ -594,7 +480,7 @@ export class Traectory extends SeriesDrawingBase<TraectorySettings, TraectoryHan
     const nextAnchors: Anchor[] = [];
 
     for (const point of points) {
-      const anchor = this.createAnchor(point);
+      const anchor = this.createAnchorFromPoint(point);
 
       if (!anchor) {
         return;
@@ -606,43 +492,33 @@ export class Traectory extends SeriesDrawingBase<TraectorySettings, TraectoryHan
     this.points = nextAnchors;
   }
 
-  private createAnchor(point: Point): Anchor | null {
-    return getAnchorFromPoint(this.chart, this.series, point);
-  }
-
   protected getGeometry(): TraectoryGeometry | null {
     if (!this.points.length) {
       return null;
     }
 
-    const { width, height } = this.getContainerSize();
+    const { width, height } = getContainerSize(this.container);
     const screenPoints: Point[] = [];
 
     for (const anchor of this.points) {
-      const x = getXCoordinateFromTime(this.chart, anchor.time, this.series);
-      const y = getYCoordinateFromPrice(this.series, anchor.price);
+      const point = this.getPointFromAnchor(anchor);
 
-      if (x === null || y === null) {
+      if (!point) {
         return null;
       }
 
       screenPoints.push({
-        x: clamp(Math.round(Number(x)), 0, width),
-        y: clamp(Math.round(Number(y)), 0, height),
+        x: clamp(point.x, 0, width),
+        y: clamp(point.y, 0, height),
       });
     }
 
-    const left = Math.round(Math.min(...screenPoints.map((point) => point.x)));
-    const right = Math.round(Math.max(...screenPoints.map((point) => point.x)));
-    const top = Math.round(Math.min(...screenPoints.map((point) => point.y)));
-    const bottom = Math.round(Math.max(...screenPoints.map((point) => point.y)));
-
     return {
       points: screenPoints,
-      left,
-      right,
-      top,
-      bottom,
+      left: Math.min(...screenPoints.map((point) => point.x)),
+      right: Math.max(...screenPoints.map((point) => point.x)),
+      top: Math.min(...screenPoints.map((point) => point.y)),
+      bottom: Math.max(...screenPoints.map((point) => point.y)),
     };
   }
 
@@ -651,18 +527,17 @@ export class Traectory extends SeriesDrawingBase<TraectorySettings, TraectoryHan
       return null;
     }
 
-    const x = getXCoordinateFromTime(this.chart, this.previewAnchor.time, this.series);
-    const y = getYCoordinateFromPrice(this.series, this.previewAnchor.price);
+    const point = this.getPointFromAnchor(this.previewAnchor);
 
-    if (x === null || y === null) {
+    if (!point) {
       return null;
     }
 
-    const { width, height } = this.getContainerSize();
+    const { width, height } = getContainerSize(this.container);
 
     return {
-      x: clamp(Math.round(Number(x)), 0, width),
-      y: clamp(Math.round(Number(y)), 0, height),
+      x: clamp(point.x, 0, width),
+      y: clamp(point.y, 0, height),
     };
   }
 
@@ -696,12 +571,4 @@ export class Traectory extends SeriesDrawingBase<TraectorySettings, TraectoryHan
 
     return false;
   }
-
-  private getContainerSize(): { width: number; height: number } {
-    return getElementContainerSize(this.container);
-  }
-
-  private clampPointToContainer(point: Point): Point {
-    return clampPointToContainerInElement(point, this.container);
-  }
 }
diff --git a/src/core/Drawings/volumeProfile/volumeProfile.ts b/src/core/Drawings/volumeProfile/volumeProfile.ts
index 46bafc1f56c2eb767401b840421c86a78e662970..9b64c2be9f2a85b57b71685e4f1241d9df55b41f 100644
--- a/src/core/Drawings/volumeProfile/volumeProfile.ts
+++ b/src/core/Drawings/volumeProfile/volumeProfile.ts
@@ -17,16 +17,12 @@ import {
   isPointInBounds,
 } from '@core/Drawings/helpers';
 import { updateViews } from '@core/Drawings/utils';
-import { SeriesDrawingBase } from '@src/core/Drawings/SeriesDrawingBase';
 
-import { getThemeStore } from '@src/theme';
-import { SettingsTab } from '@src/types';
-import { Defaults } from '@src/types/defaults';
-import { formatPrice } from '@src/utils';
-import { formatDate } from '@src/utils/formatter';
+import { BaseDrawingParams, DrawingBase, ISeriesDrawing } from '@src/core/Drawings/base';
+import { type ChartOptionsModel, Defaults, type SettingsTab } from '@src/types';
+import { formatDate, formatPrice } from '@src/utils';
 
 import { VolumeProfilePaneView } from './paneView';
-
 import {
   createDefaultSettings,
   getVolumeProfileSettingsTabs,
@@ -36,8 +32,6 @@ import {
 
 import type { DrawingHandle } from '@core/Drawings/handles';
 import type { Anchor, AxisLabel, AxisSegment, Bounds, Point } from '@core/Drawings/types';
-import type { BaseDrawingParams, ISeriesDrawing } from '@src/core/Drawings/SeriesDrawingBase';
-import type { ChartOptionsModel } from '@src/types';
 
 export type VolumeProfileKind = 'fixedRange' | 'visibleRange';
 
@@ -109,14 +103,12 @@ const BODY_HIT_TOLERANCE = 4;
 const MIN_PROFILE_SIZE = 8;
 const MAX_VISIBLE_RANGE_START_RATIO = 0.95;
 
-export class VolumeProfile
-  extends SeriesDrawingBase<VolumeProfileSettings, VolumeProfileHandleId>
-  implements ISeriesDrawing
-{
+export class VolumeProfile extends DrawingBase<VolumeProfileSettings, VolumeProfileHandleId> implements ISeriesDrawing {
   private removeSelf?: () => void;
   private openSettings?: () => void;
 
   private readonly profileKind: VolumeProfileKind;
+
   protected settings: VolumeProfileSettings = createDefaultSettings();
   protected mode: VolumeProfileMode = 'idle';
 
@@ -126,7 +118,6 @@ export class VolumeProfile
   private profileRows: VolumeProfileDataRow[] = [];
   private profileMinPrice: number | null = null;
   private profileMaxPrice: number | null = null;
-
   private visibleRangeStartRatio = 0;
 
   private activeDragTarget: DragTarget = null;
@@ -170,44 +161,17 @@ export class VolumeProfile
     }
 
     this.paneView = new VolumeProfilePaneView(this);
-
-    this.timeAxisPaneView = new CustomTimeAxisPaneView({
-      getAxisSegments: () => this.getTimeAxisSegments(),
-    });
-
-    this.priceAxisPaneView = new CustomPriceAxisPaneView({
-      getAxisSegments: () => this.getPriceAxisSegments(),
-    });
-
-    this.startTimeAxisView = new CustomTimeAxisView({
-      getAxisLabel: (kind) => this.getTimeAxisLabel(kind),
-      labelKind: 'start',
-    });
-
-    this.endTimeAxisView = new CustomTimeAxisView({
-      getAxisLabel: (kind) => this.getTimeAxisLabel(kind),
-      labelKind: 'end',
-    });
-
-    this.startPriceAxisView = new CustomPriceAxisView({
-      getAxisLabel: (kind) => this.getPriceAxisLabel(kind),
-      labelKind: 'start',
-    });
-
-    this.endPriceAxisView = new CustomPriceAxisView({
-      getAxisLabel: (kind) => this.getPriceAxisLabel(kind),
-      labelKind: 'end',
+    this.timeAxisPaneView = this.createTimeAxisPaneView();
+    this.priceAxisPaneView = this.createPriceAxisPaneView();
+    this.startTimeAxisView = this.createTimeAxisView('start');
+    this.endTimeAxisView = this.createTimeAxisView('end');
+    this.startPriceAxisView = this.createPriceAxisView('start');
+    this.endPriceAxisView = this.createPriceAxisView('end');
+
+    this.subscribeFormat(formatObservable, (format) => {
+      this.displayFormat = format;
     });
 
-    if (formatObservable) {
-      this.subscriptions.add(
-        formatObservable.subscribe((format) => {
-          this.displayFormat = format;
-          this.render();
-        }),
-      );
-    }
-
     if (this.profileKind === 'visibleRange') {
       this.chart.timeScale().subscribeVisibleLogicalRangeChange(this.handleVisibleLogicalRangeChange);
       this.calculateProfile();
@@ -261,23 +225,8 @@ export class VolumeProfile
     }
 
     const nextState = state as Partial<VolumeProfileState>;
-
     this.hidden = typeof nextState.hidden === 'boolean' ? nextState.hidden : this.hidden;
-
-    if (this.profileKind === 'fixedRange') {
-      this.mode = nextState.mode ?? this.mode;
-      this.startAnchor = nextState.startAnchor ?? this.startAnchor;
-      this.endAnchor = nextState.endAnchor ?? this.endAnchor;
-    }
-
-    if (this.profileKind === 'visibleRange') {
-      this.mode = 'ready';
-      this.resolveReady?.();
-
-      if (typeof nextState.visibleRangeStartRatio === 'number') {
-        this.visibleRangeStartRatio = clamp(nextState.visibleRangeStartRatio, 0, MAX_VISIBLE_RANGE_START_RATIO);
-      }
-    }
+    this.restoreRangeState(nextState);
 
     if ('settings' in nextState && nextState.settings) {
       this.settings = {
@@ -311,35 +260,21 @@ export class VolumeProfile
   }
 
   public timeAxisPaneViews(): readonly IPrimitivePaneView[] {
-    if (this.profileKind === 'visibleRange') {
-      return [];
-    }
-
-    return [this.timeAxisPaneView];
+    return this.profileKind === 'visibleRange' ? [] : [this.timeAxisPaneView];
   }
 
   public priceAxisPaneViews(): readonly IPrimitivePaneView[] {
-    if (this.profileKind === 'visibleRange') {
-      return [];
-    }
-
-    return [this.priceAxisPaneView];
+    return this.profileKind === 'visibleRange' ? [] : [this.priceAxisPaneView];
   }
 
   public timeAxisViews() {
-    if (this.profileKind === 'visibleRange') {
-      return [this.startTimeAxisView];
-    }
-
-    return [this.startTimeAxisView, this.endTimeAxisView];
+    return this.profileKind === 'visibleRange'
+      ? [this.startTimeAxisView]
+      : [this.startTimeAxisView, this.endTimeAxisView];
   }
 
   public priceAxisViews() {
-    if (this.profileKind === 'visibleRange') {
-      return [];
-    }
-
-    return [this.startPriceAxisView, this.endPriceAxisView];
+    return this.profileKind === 'visibleRange' ? [] : [this.startPriceAxisView, this.endPriceAxisView];
   }
 
   public getRenderData(): VolumeProfileRenderData | null {
@@ -374,11 +309,7 @@ export class VolumeProfile
     if (this.profileKind === 'visibleRange') {
       const pocY = this.getPocY(geometry);
 
-      if (pocY === null) {
-        return [];
-      }
-
-      return [{ id: 'poc', x: geometry.left, y: pocY, shape: 'circle' }];
+      return pocY === null ? [] : [{ id: 'poc', x: geometry.left, y: pocY, shape: 'circle' }];
     }
 
     return [
@@ -388,47 +319,23 @@ export class VolumeProfile
   }
 
   protected getTimeAxisSegments(): AxisSegment[] {
-    if (this.profileKind === 'visibleRange' || (!this.isSelected() && !this.isCreationPending())) {
+    if (this.profileKind === 'visibleRange' || !this.shouldShowInteractiveAxis()) {
       return [];
     }
 
     const geometry = this.getGeometry();
 
-    if (!geometry) {
-      return [];
-    }
-
-    const { colors } = getThemeStore();
-
-    return [
-      {
-        from: geometry.left,
-        to: geometry.right,
-        color: colors.axisMarkerAreaFill,
-      },
-    ];
+    return geometry ? [this.createAxisSegment(geometry.left, geometry.right)] : [];
   }
 
   protected getPriceAxisSegments(): AxisSegment[] {
-    if (this.profileKind === 'visibleRange' || (!this.isSelected() && !this.isCreationPending())) {
+    if (this.profileKind === 'visibleRange' || !this.shouldShowInteractiveAxis()) {
       return [];
     }
 
     const geometry = this.getGeometry();
 
-    if (!geometry) {
-      return [];
-    }
-
-    const { colors } = getThemeStore();
-
-    return [
-      {
-        from: geometry.top,
-        to: geometry.bottom,
-        color: colors.axisMarkerAreaFill,
-      },
-    ];
+    return geometry ? [this.createAxisSegment(geometry.top, geometry.bottom)] : [];
   }
 
   protected getHoveredItem(x: number, y: number): PrimitiveHoveredItem | null {
@@ -458,7 +365,7 @@ export class VolumeProfile
   }
 
   protected getTimeAxisLabel(kind: string): AxisLabel | null {
-    if (!this.isSelected() && !this.isCreationPending()) {
+    if (!this.shouldShowInteractiveAxis()) {
       return null;
     }
 
@@ -479,7 +386,7 @@ export class VolumeProfile
         return null;
       }
 
-      return this.createTimeAxisLabel(geometry.left, time);
+      return this.createAxisLabel(geometry.left, this.formatTime(time));
     }
 
     if (kind !== 'start' && kind !== 'end') {
@@ -494,70 +401,30 @@ export class VolumeProfile
 
     const coordinate = getXCoordinateFromTime(this.chart, anchor.time, this.series);
 
-    if (coordinate === null) {
-      return null;
-    }
-
-    return this.createTimeAxisLabel(Number(coordinate), anchor.time);
-  }
-
-  private createTimeAxisLabel(coordinate: number, time: number): AxisLabel {
-    const { colors } = getThemeStore();
-
-    return {
-      coordinate,
-      text: formatDate(
-        time as UTCTimestamp,
-        this.displayFormat.dateFormat,
-        this.displayFormat.timeFormat,
-        this.displayFormat.showTime,
-      ),
-      textColor: colors.chartPriceLineText,
-      backgroundColor: colors.axisMarkerLabelFill,
-    };
+    return this.createAxisLabel(coordinate === null ? null : Number(coordinate), this.formatTime(anchor.time));
   }
 
   protected getPriceAxisLabel(kind: string): AxisLabel | null {
-    if (this.profileKind === 'visibleRange') {
-      return null;
-    }
-
-    if ((!this.isSelected() && !this.isCreationPending()) || (kind !== 'start' && kind !== 'end')) {
+    if (
+      this.profileKind === 'visibleRange' ||
+      !this.shouldShowInteractiveAxis() ||
+      (kind !== 'start' && kind !== 'end')
+    ) {
       return null;
     }
 
     // const anchor = kind === 'start' ? this.startAnchor : this.endAnchor;
     const price = kind === 'start' ? this.profileMinPrice : this.profileMaxPrice;
 
-    if (!price) {
+    if (price === null) {
       return null;
     }
 
     const coordinate = getYCoordinateFromPrice(this.series, price);
 
-    if (coordinate === null) {
-      return null;
-    }
-
-    const { colors } = getThemeStore();
-
-    return {
-      coordinate,
-      text: formatPrice(price) ?? '',
-      textColor: colors.chartPriceLineText,
-      backgroundColor: colors.axisMarkerLabelFill,
-    };
+    return this.createAxisLabel(coordinate === null ? null : Number(coordinate), formatPrice(price) ?? '');
   }
 
-  private handleVisibleLogicalRangeChange = (): void => {
-    if (this.profileKind !== 'visibleRange') {
-      return;
-    }
-
-    this.calculateProfile();
-    this.render();
-  };
-
   protected handleDoubleClick = (event: MouseEvent): void => {
     if (this.hidden || this.mode !== 'ready' || !this.isSelected()) {
       return;
@@ -569,9 +436,7 @@ export class VolumeProfile
       return;
     }
 
-    event.preventDefault();
-    event.stopPropagation();
-
+    this.consumeEvent(event);
     this.openSettings?.();
   };
 
@@ -590,7 +455,41 @@ export class VolumeProfile
     this.handleFixedRangePointerDown(event, point);
   };
 
-  protected handleVisibleRangePointerDown(event: PointerEvent, point: Point): void {
+  protected handlePointerMove = (event: PointerEvent): void => {
+    const point = this.getEventPoint(event);
+
+    if (this.profileKind === 'visibleRange') {
+      this.handleVisibleRangePointerMove(event, point);
+      return;
+    }
+
+    this.handleFixedRangePointerMove(event, point);
+  };
+
+  protected handlePointerUp = (event: PointerEvent): void => {
+    if (this.mode !== 'dragging' || this.dragPointerId !== event.pointerId) {
+      return;
+    }
+
+    this.finishDragging();
+  };
+
+  protected getGeometry(): VolumeProfileGeometry | null {
+    return this.profileKind === 'visibleRange'
+      ? this.getAnchoredVolumeProfileGeometry()
+      : this.getFixedRangeVolumeProfileGeometry();
+  }
+
+  private handleVisibleLogicalRangeChange = (): void => {
+    if (this.profileKind !== 'visibleRange') {
+      return;
+    }
+
+    this.calculateProfile();
+    this.render();
+  };
+
+  private handleVisibleRangePointerDown(event: PointerEvent, point: Point): void {
     let dragTarget = this.getVisibleRangeDragTarget(point);
 
     if (!dragTarget) {
@@ -601,8 +500,7 @@ export class VolumeProfile
       return;
     }
 
-    event.preventDefault();
-    event.stopPropagation();
+    this.consumeEvent(event);
 
     if (!this.isSelected()) {
       this.select();
@@ -616,24 +514,19 @@ export class VolumeProfile
     this.activeDragTarget = 'poc';
     this.dragPointerId = event.pointerId;
     this.mode = 'dragging';
-
     this.hideCrosshair();
     this.render();
   }
 
   private handleFixedRangePointerDown(event: PointerEvent, point: Point): void {
     if (this.mode === 'idle') {
-      event.preventDefault();
-      event.stopPropagation();
-
+      this.consumeEvent(event);
       this.startDrawing(point);
       return;
     }
 
     if (this.mode === 'drawing') {
-      event.preventDefault();
-      event.stopPropagation();
-
+      this.consumeEvent(event);
       this.updateDrawing(point);
       this.finishDrawing();
       return;
@@ -650,9 +543,7 @@ export class VolumeProfile
         return;
       }
 
-      event.preventDefault();
-      event.stopPropagation();
-
+      this.consumeEvent(event);
       this.select();
       return;
     }
@@ -662,23 +553,10 @@ export class VolumeProfile
       return;
     }
 
-    event.preventDefault();
-    event.stopPropagation();
-
+    this.consumeEvent(event);
     this.startDragging(point, event.pointerId, dragTarget);
   }
 
-  protected handlePointerMove = (event: PointerEvent): void => {
-    const point = this.getEventPoint(event);
-
-    if (this.profileKind === 'visibleRange') {
-      this.handleVisibleRangePointerMove(event, point);
-      return;
-    }
-
-    this.handleFixedRangePointerMove(event, point);
-  };
-
   private handleVisibleRangePointerMove(event: PointerEvent, point: Point): void {
     if (this.mode !== 'dragging' || this.dragPointerId !== event.pointerId || this.activeDragTarget !== 'poc') {
       return;
@@ -713,14 +591,6 @@ export class VolumeProfile
     this.render();
   }
 
-  protected handlePointerUp = (event: PointerEvent): void => {
-    if (this.mode !== 'dragging' || this.dragPointerId !== event.pointerId) {
-      return;
-    }
-
-    this.finishDragging();
-  };
-
   private applyInitialPoint(point: Point): void {
     if (this.profileKind === 'visibleRange') {
       this.moveVisibleRangeStart(point);
@@ -742,7 +612,6 @@ export class VolumeProfile
     this.startAnchor = anchor;
     this.endAnchor = anchor;
     this.mode = 'drawing';
-
     this.calculateProfile();
     this.render();
   }
@@ -755,7 +624,6 @@ export class VolumeProfile
     }
 
     this.endAnchor = anchor;
-
     this.calculateProfile();
     this.render();
   }
@@ -778,31 +646,26 @@ export class VolumeProfile
 
     this.mode = 'ready';
     this.resolveReady?.();
-
     this.showCrosshair();
     this.render();
   }
 
   private startDragging(point: Point, pointerId: number, dragTarget: Exclude<DragTarget, null>): void {
     this.mode = 'dragging';
-
     this.activeDragTarget = dragTarget;
     this.dragPointerId = pointerId;
     this.dragStartPoint = point;
     this.dragGeometrySnapshot = this.getGeometry();
-
     this.hideCrosshair();
     this.render();
   }
 
   private finishDragging(): void {
     this.mode = 'ready';
-
     this.activeDragTarget = null;
     this.dragPointerId = null;
     this.dragStartPoint = null;
     this.dragGeometrySnapshot = null;
-
     this.showCrosshair();
     this.render();
   }
@@ -810,18 +673,13 @@ export class VolumeProfile
   private resetToIdle(): void {
     this.hidden = false;
     this.mode = 'idle';
-
     this.startAnchor = null;
     this.endAnchor = null;
-    this.profileRows = [];
-    this.profileMinPrice = null;
-    this.profileMaxPrice = null;
-
+    this.clearProfile();
     this.activeDragTarget = null;
     this.dragPointerId = null;
     this.dragStartPoint = null;
     this.dragGeometrySnapshot = null;
-
     this.showCrosshair();
     this.render();
   }
@@ -867,10 +725,8 @@ export class VolumeProfile
     }
 
     const { width, height } = this.getContainerSize();
-
     const rawOffsetX = point.x - dragStartPoint.x;
     const rawOffsetY = point.y - dragStartPoint.y;
-
     const offsetX = clamp(rawOffsetX, -geometry.left, width - geometry.right);
     const offsetY = clamp(rawOffsetY, -geometry.top, height - geometry.bottom);
 
@@ -896,16 +752,13 @@ export class VolumeProfile
 
     this.startAnchor = startAnchor;
     this.endAnchor = endAnchor;
-
     this.calculateProfile();
   }
 
   private getDragTarget(point: Point): Exclude<DragTarget, null> | null {
-    if (this.profileKind === 'visibleRange') {
-      return this.getVisibleRangeDragTarget(point);
-    }
-
-    return this.getFixedRangeDragTarget(point);
+    return this.profileKind === 'visibleRange'
+      ? this.getVisibleRangeDragTarget(point)
+      : this.getFixedRangeDragTarget(point);
   }
 
   private getVisibleRangeDragTarget(point: Point): Exclude<DragTarget, null> | null {
@@ -915,11 +768,7 @@ export class VolumeProfile
       return 'poc';
     }
 
-    if (this.containsPoint(point)) {
-      return 'body';
-    }
-
-    return null;
+    return this.containsPoint(point) ? 'body' : null;
   }
 
   private getFixedRangeDragTarget(point: Point): Exclude<DragTarget, null> | null {
@@ -929,21 +778,13 @@ export class VolumeProfile
       return handle.id;
     }
 
-    if (this.containsPoint(point)) {
-      return 'body';
-    }
-
-    return null;
+    return this.containsPoint(point) ? 'body' : null;
   }
 
   private containsPoint(point: Point): boolean {
     const geometry = this.getGeometry();
 
-    if (!geometry) {
-      return false;
-    }
-
-    return isPointInBounds(point, geometry, BODY_HIT_TOLERANCE);
+    return geometry !== null && isPointInBounds(point, geometry, BODY_HIT_TOLERANCE);
   }
 
   private calculateProfile(): void {
@@ -966,7 +807,6 @@ export class VolumeProfile
     const from = Number(visibleRange.from);
     const to = Number(visibleRange.to);
     const start = from + (to - from) * this.visibleRangeStartRatio;
-
     this.calculateProfileByLogicalRange(start, to);
   }
 
@@ -978,16 +818,11 @@ export class VolumeProfile
 
     const leftFrameTime = Math.min(Number(this.startAnchor.time), Number(this.endAnchor.time));
     const rightFrameTime = Math.max(Number(this.startAnchor.time), Number(this.endAnchor.time));
-
     const candles = this.series.data() as SeriesCandleData[];
-    const selectedCandles: SeriesCandleData[] = [];
-
-    candles.forEach((candle) => {
+    const selectedCandles = candles.filter((candle) => {
       const candleTime = Number(candle.time);
 
-      if (candleTime >= leftFrameTime && candleTime <= rightFrameTime) {
-        selectedCandles.push(candle);
-      }
+      return candleTime >= leftFrameTime && candleTime <= rightFrameTime;
     });
 
     this.calculateProfileByCandles(selectedCandles);
@@ -1045,7 +880,6 @@ export class VolumeProfile
 
     this.profileMinPrice = minPrice;
     this.profileMaxPrice = maxPrice;
-
     const priceStep = (maxPrice - minPrice) / PROFILE_ROW_COUNT;
     const profileRows = this.createEmptyProfileRows(minPrice, priceStep);
 
@@ -1165,21 +999,12 @@ export class VolumeProfile
     return typeof value === 'number' && Number.isFinite(value) ? value : null;
   }
 
-  protected getGeometry(): VolumeProfileGeometry | null {
-    if (this.profileKind === 'visibleRange') {
-      return this.getAnchoredVolumeProfileGeometry();
-    }
-
-    return this.getFixedRangeVolumeProfileGeometry();
-  }
-
   private getAnchoredVolumeProfileGeometry(): VolumeProfileGeometry | null {
     if (this.profileMinPrice === null || this.profileMaxPrice === null) {
       return null;
     }
 
     const { width, height } = this.getContainerSize();
-
     const topCoordinate = getYCoordinateFromPrice(this.series, this.profileMaxPrice);
     const bottomCoordinate = getYCoordinateFromPrice(this.series, this.profileMinPrice);
 
@@ -1193,14 +1018,8 @@ export class VolumeProfile
     const bottom = clamp(Math.round(Math.max(Number(topCoordinate), Number(bottomCoordinate))), 0, height);
 
     return {
-      startPoint: {
-        x: left,
-        y: top,
-      },
-      endPoint: {
-        x: right,
-        y: bottom,
-      },
+      startPoint: { x: left, y: top },
+      endPoint: { x: right, y: bottom },
       left,
       right,
       top,
@@ -1211,11 +1030,7 @@ export class VolumeProfile
   }
 
   private getFixedRangeVolumeProfileGeometry(): VolumeProfileGeometry | null {
-    if (!this.startAnchor || !this.endAnchor) {
-      return null;
-    }
-
-    if (this.profileMinPrice === null || this.profileMaxPrice === null) {
+    if (!this.startAnchor || !this.endAnchor || this.profileMinPrice === null || this.profileMaxPrice === null) {
       return null;
     }
 
@@ -1316,6 +1131,15 @@ export class VolumeProfile
     };
   }
 
+  private formatTime(time: number): string {
+    return formatDate(
+      time as UTCTimestamp,
+      this.displayFormat.dateFormat,
+      this.displayFormat.timeFormat,
+      this.displayFormat.showTime,
+    );
+  }
+
   private createAnchor(point: Point): Anchor | null {
     return getAnchorFromPoint(this.chart, this.series, point);
   }
@@ -1327,6 +1151,29 @@ export class VolumeProfile
   private clampPointToContainer(point: Point): Point {
     return clampPointToContainerInElement(point, this.container);
   }
+
+  private restoreRangeState(nextState: Partial<VolumeProfileState>): void {
+    if (this.profileKind === 'fixedRange') {
+      this.mode = nextState.mode ?? this.mode;
+
+      if ('startAnchor' in nextState) {
+        this.startAnchor = nextState.startAnchor ?? null;
+      }
+
+      if ('endAnchor' in nextState) {
+        this.endAnchor = nextState.endAnchor ?? null;
+      }
+    }
+
+    if (this.profileKind === 'visibleRange') {
+      this.mode = 'ready';
+      this.resolveReady?.();
+
+      if (typeof nextState.visibleRangeStartRatio === 'number') {
+        this.visibleRangeStartRatio = clamp(nextState.visibleRangeStartRatio, 0, MAX_VISIBLE_RANGE_START_RATIO);
+      }
+    }
+  }
 }
 
 function addPointVolume(
diff --git a/src/core/DrawingsManager.tsx b/src/core/DrawingsManager.tsx
index cfc453c648c6535db7e046bd69b2a8a5b04bad46..a832e1895e75a22529a44be6106b9571695edb95 100644
--- a/src/core/DrawingsManager.tsx
+++ b/src/core/DrawingsManager.tsx
@@ -15,7 +15,7 @@ import { ActiveDrawingTool, DOMObjectSnapshot } from '@src/types';
 
 import { Pane } from './Pane';
 
-import type { DrawingInteraction } from '@src/core/Drawings/SeriesDrawingBase';
+import type { DrawingInteraction } from '@src/core/Drawings/base/DrawingBase';
 import type { SettingsValues } from '@src/types/settings';
 
 interface DrawingsManagerParams {
diff --git a/src/types/defaults.ts b/src/types/defaults.ts
index e1bc777a21eaf198e210e4f6e5fb73b7d69264d6..0a1ff1862647cb7f8b107489ec314ca8ace677e0 100644
--- a/src/types/defaults.ts
+++ b/src/types/defaults.ts
@@ -1,6 +1,6 @@
 import { ChartOptionsModel } from '@src/types/chart';
 import { TooltipConfig } from '@src/types/ohlc';
-import { DateFormat } from '@src/utils';
+import { DateFormat } from '@src/utils/formatter';
 
 export const Defaults: ChartOptionsModel = {
   timeFormat: '24h',
diff --git a/src/types/drawing.ts b/src/types/drawing.ts
index 7ab5844883ff2637b657f1b06094524123d0f42d..314ff195e43704768c863f2d0cc036c93c809013 100644
--- a/src/types/drawing.ts
+++ b/src/types/drawing.ts
@@ -3,7 +3,7 @@ import { IChartApi, ISeriesApi, MouseEventParams, SeriesType } from 'lightweight
 import { DrawingsNames } from '@src/constants';
 import { EventManager } from '@src/core';
 
-import type { DrawingInteraction, ISeriesDrawing } from '@src/core/Drawings/SeriesDrawingBase';
+import type { DrawingInteraction, ISeriesDrawing } from '@src/core/Drawings/base/DrawingBase';
 
 export enum LineMarker {
   normal = 'normal',
diff --git a/src/types/index.ts b/src/types/index.ts
index df65df6fff037c35bbf6cc2dbdd10dac659c9bd4..569a98dccce34215822f315dced079f00f59d585 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -1,6 +1,7 @@
 export * from '../theme/types';
 export * from './chart';
 export * from './compare';
+export * from './defaults';
 export * from './drawing';
 export * from './indicator';
 export * from './intervals';