Загрузка данных
diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index b6b3578..9982353 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -15,7 +15,7 @@ module.exports = {
'plugin:eslint-comments/recommended',
],
parser: '@typescript-eslint/parser',
- ignorePatterns: ['**/*.module.css', '**/*.scss', '**/*.md', '**/*.mdx'],
+ ignorePatterns: ['**/*.module.css', '**/*.scss', '**/*.md', '**/*.mdx', '**/*worker.ts', '**/*Worker.ts'],
parserOptions: {
ecmaFeatures: {
jsx: true,
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2908efd..5f7251d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,20 +1,17 @@
# latest
-- Добавлен новый инструмент рисования «Параллельный канал» в группу «Линии тренда»
-
-# 0.1.15
-
-- Добавлен плавающий тулбар для настройки объектов рисования
-- Добавлен компонент `ColorPicker` для выбора цвета из палитры
+- Продлена ось x вправо
+- Дродаун таймфреймов теперь по умолчанию раскрыт
+- Применены стили к скролбару интервалов
- Фикс контейнера дровингов. Теперь их контейнер не содержит `priceScale`
- Рефактор дровингов
- Фикс названий дровингов связанных с объёмом
-- Исправлено поведение дровинга `fixed range profile volume`
+- Исправлено поведение дровинга `fixed range profile volume`
- `NaN` получающийся в результате деления на ноль при расчете отосительной цены заменён на `0`
-- К форматированию чисел добавлена обработка бесконечностей
+- К форматированию чисел добавлена обработка бесконечностей
# 0.1.14
-
+-
- Исправлена обработка событий клика в модальных окнах
# 0.1.13
diff --git a/src/components/Accordion/index.tsx b/src/components/Accordion/index.tsx
index 0ae02eb..e7ece51 100644
--- a/src/components/Accordion/index.tsx
+++ b/src/components/Accordion/index.tsx
@@ -10,7 +10,7 @@ interface AccordionProps extends PropsWithChildren {
}
export function Accordion({ title, children }: AccordionProps) {
- const [isOpenAccordion, setIsOpenAccordion] = useState(false);
+ const [isOpenAccordion, setIsOpenAccordion] = useState(true);
const contentRef = useRef<HTMLDivElement>(null);
const [height, setHeight] = useState(0);
diff --git a/src/components/ColorPicker/constants.ts b/src/components/ColorPicker/constants.ts
deleted file mode 100644
index 0117e1c..0000000
--- a/src/components/ColorPicker/constants.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-export const COLOR_PALETTE = [
- ['#ffffff', '#d9d9d9', '#bfbfbf', '#a6a6a6', '#8c8c8c', '#737373', '#595959', '#404040', '#262626', '#000000'],
- ['#f23645', '#ff9800', '#ffeb3b', '#4caf50', '#009688', '#16b8c4', '#2962ff', '#673ab7', '#9c27b0', '#e91e63'],
- ['#f8c1c5', '#ffe0b2', '#fff9c4', '#c8e6c9', '#b2dfdb', '#b2ebf2', '#bbdefb', '#d1c4e9', '#e1bee7', '#f8bbd0'],
- ['#f58f97', '#ffcc80', '#fff59d', '#a5d6a7', '#80cbc4', '#80deea', '#90caf9', '#b39ddb', '#ce93d8', '#f48fb1'],
- ['#f16b75', '#ffb74d', '#ffee58', '#81c784', '#4db6ac', '#4dd0e1', '#64b5f6', '#9575cd', '#ba68c8', '#f06292'],
- ['#ed4c5a', '#ffa726', '#fdd835', '#66bb6a', '#26a69a', '#26c6da', '#4285e8', '#7e57c2', '#ab47bc', '#ec407a'],
- ['#c62835', '#f57c00', '#fbc02d', '#388e3c', '#00796b', '#0097a7', '#1f4dcc', '#5e35b1', '#8e24aa', '#c2185b'],
- ['#991b24', '#e65100', '#ef6c00', '#1b5e20', '#004d40', '#006064', '#173b99', '#311b92', '#4a148c', '#880e4f'],
-] as const;
diff --git a/src/components/ColorPicker/index.module.scss b/src/components/ColorPicker/index.module.scss
deleted file mode 100644
index 54300d8..0000000
--- a/src/components/ColorPicker/index.module.scss
+++ /dev/null
@@ -1,29 +0,0 @@
-.picker {
- width: 100%;
- display: flex;
- flex-direction: column;
- gap: var(--space-0750);
- padding: var(--space-0500) var(--space-0750);
-
- &_pallete {
- display: grid;
- grid-template-columns: repeat(10, var(--space-1000));
- gap: var(--space-0375);
- }
-
- &_option {
- min-width: none;
- width: var(--space-1000);
- height: var(--space-1000);
- border-radius: var(--space-0125);
-
- p {
- padding: 0;
- }
- }
-
- &_divider {
- height: var(--borderWidth-md);
- background-color: var(--neutral-7);
- }
-}
diff --git a/src/components/ColorPicker/index.tsx b/src/components/ColorPicker/index.tsx
deleted file mode 100644
index 255cf23..0000000
--- a/src/components/ColorPicker/index.tsx
+++ /dev/null
@@ -1,88 +0,0 @@
-import classNames from 'classnames';
-import { Divider } from 'exchange-elements/v2';
-import { ChangeEvent } from 'react';
-
-import { RangeField } from '@src/components/FormFields';
-import { t } from '@src/translations';
-import { normalizeColor } from '@src/utils';
-
-import { COLOR_PALETTE } from './constants';
-import styles from './index.module.scss';
-
-interface ColorPickerProps {
- value: string;
- onChange: (value: string) => void;
-}
-
-export function normalizeHexColor(value: string): string {
- const normalized = value.trim().startsWith('#') ? normalizeColor(value) : `#${normalizeColor(value)}`;
-
- if (/^#[0-9a-f]{6}$/i.test(normalized)) {
- return `${normalized}ff`;
- }
-
- if (/^#[0-9a-f]{8}$/i.test(normalized)) {
- return normalized;
- }
-
- return '#000000ff';
-}
-
-export function ColorPicker({ value, onChange }: ColorPickerProps) {
- const currentColor = normalizeHexColor(value);
- const rgb = currentColor.slice(0, 7).toLowerCase();
- const alpha = currentColor.slice(7, 9);
-
- const opacity = Math.round((parseInt(alpha, 16) / 255) * 100);
-
- const handleColorChange = (color: string): void => {
- onChange(`${color}${alpha}`);
- };
-
- const handleOpacityChange = (event: ChangeEvent<HTMLInputElement>): void => {
- const nextOpacity = Number(event.target.value);
- const nextAlpha = Math.round((nextOpacity / 100) * 255)
- .toString(16)
- .padStart(2, '0');
-
- onChange(`${rgb}${nextAlpha}`);
- };
-
- return (
- <div className={styles.picker}>
- <div className={styles.picker_pallete}>
- {COLOR_PALETTE.flatMap((row) =>
- row.map((color) => {
- const isSelected = color.toLowerCase() === rgb;
-
- return (
- <button
- key={color}
- type="button"
- className={classNames(styles.picker_option, {
- [styles.selected]: isSelected,
- })}
- style={{ backgroundColor: color }}
- onClick={() => handleColorChange(color)}
- />
- );
- }),
- )}
- </div>
-
- <Divider
- pt={{ divider: { className: styles.picker_divider } }}
- direction="horizontal"
- />
-
- <RangeField
- label={t('Opacity')}
- value={opacity}
- color={rgb}
- suffix="%"
- size="sm"
- onChange={handleOpacityChange}
- />
- </div>
- );
-}
diff --git a/src/components/Dropdown/index.module.scss b/src/components/Dropdown/index.module.scss
index b4b6fac..c62cfbb 100644
--- a/src/components/Dropdown/index.module.scss
+++ b/src/components/Dropdown/index.module.scss
@@ -11,15 +11,12 @@ $menu-width: 200px;
}
.menu {
+ font-family: var(--font-family);
+ width: $menu-width;
position: fixed;
- box-sizing: border-box;
- width: var(--dropdown-width, $menu-width);
- max-width: calc(100vw - var(--space-1000));
- max-height: calc(100dvh - var(--space-1000));
- padding: var(--space-0250) 0;
- overflow-y: auto;
- border-radius: var(--space-0250);
+
background-color: var(--neutral-2);
- font-family: var(--font-family);
- pointer-events: auto;
+ border-radius: var(--space-0250);
+ padding: var(--space-0250) 0;
+ z-index: 999999; // todo: временное решение
}
diff --git a/src/components/Dropdown/index.tsx b/src/components/Dropdown/index.tsx
index 20194ce..e8734e4 100644
--- a/src/components/Dropdown/index.tsx
+++ b/src/components/Dropdown/index.tsx
@@ -4,16 +4,12 @@ import { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { Portal } from '@src/components/Portal';
import styles from './index.module.scss';
-
-import { getDropdownCoords } from './utils';
-
-import type { Coords, DropdownProps } from './types';
+import { Coords, DropdownProps } from './types';
export function Dropdown({
children,
selectedValue,
position = 'bottom',
- horizontalAlign = 'auto',
className = '',
buttonClassName = '',
menuClassName = '',
@@ -27,98 +23,73 @@ export function Dropdown({
const triggerRef = useRef<HTMLDivElement>(null);
const menuRef = useRef<HTMLDivElement>(null);
- const handleToggle = (): void => {
- if (isOpenMenu) {
- handleClose();
-
- return;
- }
-
- setIsOpenMenu(true);
- };
-
- const handleClose = (): void => {
- setIsOpenMenu(false);
- setCoords(null);
- };
+ const handleToggle = () => setIsOpenMenu((prev) => !prev);
+ const handleClose = () => setIsOpenMenu(false);
useLayoutEffect(() => {
if (!isOpenMenu) {
+ setCoords(null);
return;
}
- const updatePosition = (): void => {
- const triggerElement = triggerRef.current;
- const menuElement = menuRef.current;
+ const calcMenuPosition = () => {
+ const triggerEl = triggerRef.current;
+ const menuEl = menuRef.current;
- if (!triggerElement || !menuElement) {
- return;
- }
+ if (!triggerEl || !menuEl) return;
- const triggerRect = triggerElement.getBoundingClientRect();
+ const triggerRect = triggerEl.getBoundingClientRect();
const anchorRect = anchorRef?.current?.getBoundingClientRect() ?? triggerRect;
- const menuRect = menuElement.getBoundingClientRect();
-
- setCoords(
- getDropdownCoords({
- position,
- horizontalAlign,
- triggerRect,
- anchorRect,
- menuRect,
- }),
- );
- };
- updatePosition();
+ let topMenu = 0;
+ let leftMenu = 0;
- const resizeObserver = new ResizeObserver(updatePosition);
+ if (position === 'bottom') {
+ topMenu = triggerRect.bottom;
+ leftMenu = triggerRect.left;
+ }
- if (menuRef.current) {
- resizeObserver.observe(menuRef.current);
- }
+ if (position === 'right') {
+ topMenu = anchorRect.top;
+ leftMenu = triggerRect.right;
+ }
- window.addEventListener('resize', updatePosition);
- window.addEventListener('scroll', updatePosition, true);
+ setCoords({ top: topMenu, left: leftMenu });
+ };
- return () => {
- resizeObserver.disconnect();
+ calcMenuPosition();
- window.removeEventListener('resize', updatePosition);
- window.removeEventListener('scroll', updatePosition, true);
+ window.addEventListener('resize', calcMenuPosition);
+ window.addEventListener('scroll', calcMenuPosition, true);
+
+ return () => {
+ window.removeEventListener('resize', calcMenuPosition);
+ window.removeEventListener('scroll', calcMenuPosition, true);
};
- }, [anchorRef, isOpenMenu, position]);
+ }, [isOpenMenu, position, anchorRef]);
useEffect(() => {
- if (!isOpenMenu) {
- return;
- }
-
- const handlePointerDown = (event: PointerEvent): void => {
- const target = event.target as Node;
+ const onPointerDown = (e: PointerEvent) => {
+ const t = e.target as Node;
- const isInsideRoot = rootRef.current?.contains(target);
- const isInsideMenu = menuRef.current?.contains(target);
+ const insideRoot = rootRef.current?.contains(t);
+ const insideMenu = menuRef.current?.contains(t);
- if (!isInsideRoot && !isInsideMenu) {
- handleClose();
- }
+ if (!insideRoot && !insideMenu) handleClose();
};
- const handleKeyDown = (event: KeyboardEvent): void => {
- if (event.key === 'Escape') {
- handleClose();
- }
+ const onKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') handleClose();
};
- document.addEventListener('pointerdown', handlePointerDown, true);
- document.addEventListener('keydown', handleKeyDown);
+ document.addEventListener('pointerdown', onPointerDown, true);
+ document.addEventListener('keydown', onKeyDown);
return () => {
- document.removeEventListener('pointerdown', handlePointerDown, true);
- document.removeEventListener('keydown', handleKeyDown);
+ document.removeEventListener('pointerdown', onPointerDown, true);
+ document.removeEventListener('keydown', onKeyDown);
};
- }, [isOpenMenu]);
+ }, []);
return (
<div
@@ -127,11 +98,7 @@ export function Dropdown({
>
<div ref={triggerRef}>
{renderTrigger ? (
- renderTrigger({
- isOpen: isOpenMenu,
- onToggle: handleToggle,
- onClose: handleClose,
- })
+ renderTrigger({ isOpen: isOpenMenu, onToggle: handleToggle, onClose: handleClose })
) : (
<Button
size="sm"
diff --git a/src/components/Dropdown/types.ts b/src/components/Dropdown/types.ts
index d9d01b8..515fa2a 100644
--- a/src/components/Dropdown/types.ts
+++ b/src/components/Dropdown/types.ts
@@ -1,8 +1,6 @@
-import type { PropsWithChildren, ReactNode, RefObject } from 'react';
+import { PropsWithChildren, ReactNode, RefObject } from 'react';
-export type DropdownPosition = 'bottom' | 'top' | 'right' | 'left';
-
-export type DropdownHorizontalAlign = 'auto' | 'left' | 'right';
+export type DropdownPosition = 'bottom' | 'right';
export interface TriggerRenderProps {
isOpen: boolean;
@@ -13,7 +11,6 @@ export interface TriggerRenderProps {
export interface DropdownProps extends PropsWithChildren {
selectedValue?: string | JSX.Element;
position?: DropdownPosition;
- horizontalAlign?: DropdownHorizontalAlign;
className?: string;
buttonClassName?: string;
diff --git a/src/components/Dropdown/utils.ts b/src/components/Dropdown/utils.ts
deleted file mode 100644
index e82935c..0000000
--- a/src/components/Dropdown/utils.ts
+++ /dev/null
@@ -1,133 +0,0 @@
-import { clamp } from 'lodash-es';
-
-import type { Coords, DropdownHorizontalAlign, DropdownPosition } from './types';
-
-interface GetDropdownCoordsParams {
- position: DropdownPosition;
- horizontalAlign: DropdownHorizontalAlign;
- triggerRect: DOMRect;
- anchorRect: DOMRect;
- menuRect: DOMRect;
-}
-
-const MENU_GAP = 4;
-const VIEWPORT_PADDING = 8;
-
-const oppositePosition: Record<DropdownPosition, DropdownPosition> = {
- bottom: 'top',
- top: 'bottom',
- right: 'left',
- left: 'right',
-};
-
-export function getDropdownCoords({
- position,
- horizontalAlign,
- triggerRect,
- anchorRect,
- menuRect,
-}: GetDropdownCoordsParams): Coords {
- const actualPosition = getActualPosition(position, triggerRect, menuRect);
-
- if (actualPosition === 'top') {
- return {
- top: clamp(triggerRect.top - menuRect.height - MENU_GAP, VIEWPORT_PADDING, getMaxTop(menuRect)),
- left: getHorizontalPosition(triggerRect, menuRect, horizontalAlign),
- };
- }
-
- if (actualPosition === 'right') {
- return {
- top: getVerticalPosition(anchorRect, menuRect),
- left: clamp(triggerRect.right + MENU_GAP, VIEWPORT_PADDING, getMaxLeft(menuRect)),
- };
- }
-
- if (actualPosition === 'left') {
- return {
- top: getVerticalPosition(anchorRect, menuRect),
- left: clamp(triggerRect.left - menuRect.width - MENU_GAP, VIEWPORT_PADDING, getMaxLeft(menuRect)),
- };
- }
-
- return {
- top: clamp(triggerRect.bottom + MENU_GAP, VIEWPORT_PADDING, getMaxTop(menuRect)),
- left: getHorizontalPosition(triggerRect, menuRect, horizontalAlign),
- };
-}
-
-function getActualPosition(position: DropdownPosition, triggerRect: DOMRect, menuRect: DOMRect): DropdownPosition {
- const fallbackPosition = oppositePosition[position];
-
- const menuSize = position === 'top' || position === 'bottom' ? menuRect.height : menuRect.width;
-
- const availableSpace = getAvailableSpace(position, triggerRect);
-
- if (availableSpace >= menuSize) {
- return position;
- }
-
- const fallbackAvailableSpace = getAvailableSpace(fallbackPosition, triggerRect);
-
- if (fallbackAvailableSpace >= menuSize) {
- return fallbackPosition;
- }
-
- return availableSpace >= fallbackAvailableSpace ? position : fallbackPosition;
-}
-
-function getAvailableSpace(position: DropdownPosition, triggerRect: DOMRect): number {
- if (position === 'top') {
- return triggerRect.top - MENU_GAP - VIEWPORT_PADDING;
- }
-
- if (position === 'right') {
- return window.innerWidth - triggerRect.right - MENU_GAP - VIEWPORT_PADDING;
- }
-
- if (position === 'left') {
- return triggerRect.left - MENU_GAP - VIEWPORT_PADDING;
- }
-
- return window.innerHeight - triggerRect.bottom - MENU_GAP - VIEWPORT_PADDING;
-}
-
-function getHorizontalPosition(
- triggerRect: DOMRect,
- menuRect: DOMRect,
- horizontalAlign: DropdownHorizontalAlign,
-): number {
- const leftAligned = triggerRect.left;
- const rightAligned = triggerRect.right - menuRect.width;
- const maxLeft = getMaxLeft(menuRect);
-
- if (horizontalAlign === 'left') {
- return clamp(leftAligned, VIEWPORT_PADDING, maxLeft);
- }
-
- if (horizontalAlign === 'right') {
- return clamp(rightAligned, VIEWPORT_PADDING, maxLeft);
- }
-
- const hasSpaceOnRight = leftAligned + menuRect.width <= window.innerWidth - VIEWPORT_PADDING;
-
- return clamp(hasSpaceOnRight ? leftAligned : rightAligned, VIEWPORT_PADDING, maxLeft);
-}
-
-function getVerticalPosition(anchorRect: DOMRect, menuRect: DOMRect): number {
- const topAligned = anchorRect.top;
- const bottomAligned = anchorRect.bottom - menuRect.height;
- const maxTop = getMaxTop(menuRect);
-
- const hasSpaceBelow = topAligned + menuRect.height <= window.innerHeight - VIEWPORT_PADDING;
-
- return clamp(hasSpaceBelow ? topAligned : bottomAligned, VIEWPORT_PADDING, maxTop);
-}
-
-function getMaxLeft(menuRect: DOMRect): number {
- return Math.max(VIEWPORT_PADDING, window.innerWidth - menuRect.width - VIEWPORT_PADDING);
-}
-
-function getMaxTop(menuRect: DOMRect): number {
- return Math.max(VIEWPORT_PADDING, window.innerHeight - menuRect.height - VIEWPORT_PADDING);
-}
diff --git a/src/components/FloatingToolbar/index.module.scss b/src/components/FloatingToolbar/index.module.scss
deleted file mode 100644
index 9ad0639..0000000
--- a/src/components/FloatingToolbar/index.module.scss
+++ /dev/null
@@ -1,51 +0,0 @@
-@use '../../theme/mixins' as m;
-
-$handle-size: 3px;
-
-.toolbar {
- position: absolute;
- top: 0;
- left: 0;
- display: flex;
- align-items: center;
- border-radius: var(--space-0250);
- background-color: var(--neutral-2);
- pointer-events: auto;
- user-select: none;
- touch-action: none;
- white-space: nowrap;
-
- &_tooltip {
- @include m.tooltipHint;
- }
-
- &_handle {
- display: grid;
- grid-template-columns: repeat(2, $handle-size);
- gap: $handle-size;
- align-content: center;
- justify-content: center;
- align-self: stretch;
- width: var(--space-1500);
- padding: 0;
- border: 0;
- background-color: transparent;
- cursor: grab;
- touch-action: none;
-
- &:active {
- cursor: grabbing;
- }
-
- span {
- width: var(--space-0125);
- height: var(--space-0125);
- border-radius: 50%;
- background-color: var(--neutral-700);
- }
- }
-
- .button {
- @include m.buttonBase;
- }
-}
diff --git a/src/components/FloatingToolbar/index.tsx b/src/components/FloatingToolbar/index.tsx
deleted file mode 100644
index d055097..0000000
--- a/src/components/FloatingToolbar/index.tsx
+++ /dev/null
@@ -1,298 +0,0 @@
-import classNames from 'classnames';
-import { Button, Tooltip } from 'exchange-elements/v2';
-import { useEffect, useLayoutEffect, useRef, useState } from 'react';
-
-import { GearIcon, LockIcon, LockOpenIcon, TrashIcon } from '@src/components/Icon';
-import { ToolbarColorControl } from '@src/components/ToolbarColorControl';
-import { t } from '@src/translations';
-
-import styles from './index.module.scss';
-
-import type { Drawing } from '@core/Drawings';
-import type { SettingsValues } from '@src/types/settings';
-import type { PointerEvent, SyntheticEvent } from 'react';
-import type { Observable } from 'rxjs';
-
-interface FloatingDrawingToolbarProps {
- selectedDrawing$: Observable<Drawing | null>;
- onToggleLock: () => void;
- onOpenSettings: () => void;
- onDelete: () => void;
-}
-
-interface Position {
- x: number;
- y: number;
-}
-
-interface DragState {
- pointerId: number;
- startX: number;
- startY: number;
- initialX: number;
- initialY: number;
-}
-
-const TOOLBAR_TOP_OFFSET = 12;
-
-export function FloatingDrawingToolbar({
- selectedDrawing$,
- onToggleLock,
- onOpenSettings,
- onDelete,
-}: FloatingDrawingToolbarProps) {
- const toolbarRef = useRef<HTMLDivElement | null>(null);
- const dragStateRef = useRef<DragState | null>(null);
- const positionRef = useRef<Position | null>(null);
-
- const [selectedDrawing, setSelectedDrawing] = useState<Drawing | null>(null);
- const [settings, setSettings] = useState<SettingsValues>({});
- const [isLocked, setIsLocked] = useState(false);
- const [isDragging, setIsDragging] = useState(false);
- const [position, setPosition] = useState<Position | null>(null);
-
- const shouldShowToolbar = selectedDrawing?.hasSettings() ?? false;
-
- useEffect(() => {
- const subscription = selectedDrawing$.subscribe(setSelectedDrawing);
-
- return () => {
- subscription.unsubscribe();
- };
- }, [selectedDrawing$]);
-
- useEffect(() => {
- if (!selectedDrawing || !shouldShowToolbar) {
- setIsLocked(false);
-
- return;
- }
-
- const subscription = selectedDrawing.subscribeIsLocked(setIsLocked);
-
- return () => {
- subscription.unsubscribe();
- };
- }, [selectedDrawing, shouldShowToolbar]);
-
- useEffect(() => {
- if (!selectedDrawing || !shouldShowToolbar) {
- setSettings({});
-
- return;
- }
-
- const subscription = selectedDrawing.subscribeSettings(setSettings);
-
- return () => {
- subscription.unsubscribe();
- };
- }, [selectedDrawing, shouldShowToolbar]);
-
- useLayoutEffect(() => {
- if (!selectedDrawing || !shouldShowToolbar) {
- return;
- }
-
- const toolbar = toolbarRef.current;
- const container = toolbar?.parentElement;
-
- if (!toolbar || !container) {
- return;
- }
-
- const currentPosition = positionRef.current;
-
- if (currentPosition) {
- updatePosition(clampPosition(currentPosition.x, currentPosition.y, toolbar, container));
-
- return;
- }
-
- updatePosition({
- x: Math.round((container.clientWidth - toolbar.offsetWidth) / 2),
- y: TOOLBAR_TOP_OFFSET,
- });
- }, [selectedDrawing, shouldShowToolbar]);
-
- const handleDragStart = (event: PointerEvent<HTMLButtonElement>): void => {
- const currentPosition = positionRef.current;
-
- if (event.button !== 0 || !currentPosition) {
- return;
- }
-
- event.preventDefault();
- event.currentTarget.setPointerCapture(event.pointerId);
-
- dragStateRef.current = {
- pointerId: event.pointerId,
- startX: event.clientX,
- startY: event.clientY,
- initialX: currentPosition.x,
- initialY: currentPosition.y,
- };
-
- setIsDragging(true);
- };
-
- const handleDrag = (event: PointerEvent<HTMLButtonElement>): void => {
- const dragState = dragStateRef.current;
- const toolbar = toolbarRef.current;
- const container = toolbar?.parentElement;
-
- if (!dragState || dragState.pointerId !== event.pointerId || !toolbar || !container) {
- return;
- }
-
- event.preventDefault();
-
- updatePosition(
- clampPosition(
- dragState.initialX + event.clientX - dragState.startX,
- dragState.initialY + event.clientY - dragState.startY,
- toolbar,
- container,
- ),
- );
- };
-
- const handleDragEnd = (event: PointerEvent<HTMLButtonElement>): void => {
- if (dragStateRef.current?.pointerId !== event.pointerId) {
- return;
- }
-
- if (event.currentTarget.hasPointerCapture(event.pointerId)) {
- event.currentTarget.releasePointerCapture(event.pointerId);
- }
-
- dragStateRef.current = null;
- setIsDragging(false);
- };
-
- const stopPropagation = (event: SyntheticEvent): void => {
- event.stopPropagation();
- };
-
- function updatePosition(nextPosition: Position): void {
- positionRef.current = nextPosition;
-
- setPosition((currentPosition) => {
- if (currentPosition?.x === nextPosition.x && currentPosition.y === nextPosition.y) {
- return currentPosition;
- }
-
- return nextPosition;
- });
- }
-
- if (!selectedDrawing || !shouldShowToolbar) {
- return null;
- }
-
- const toolbarSettings = selectedDrawing.getToolbarSettings();
- const lockLabel = isLocked ? t('Unlock') : t('Lock');
-
- return (
- <div
- ref={toolbarRef}
- className={styles.toolbar}
- style={{
- visibility: position ? 'visible' : 'hidden',
- transform: `translate3d(${position?.x ?? 0}px, ${position?.y ?? 0}px, 0)`,
- }}
- onPointerDown={stopPropagation}
- onClick={stopPropagation}
- onDoubleClick={stopPropagation}
- onContextMenu={stopPropagation}
- >
- <button
- type="button"
- className={classNames(styles.toolbar_handle, {
- [styles.dragging]: isDragging,
- })}
- onPointerDown={handleDragStart}
- onPointerMove={handleDrag}
- onPointerUp={handleDragEnd}
- onPointerCancel={handleDragEnd}
- >
- <span />
- <span />
- <span />
- <span />
- <span />
- <span />
- </button>
-
- {toolbarSettings.map((field) => {
- if (field.type !== 'color' || field.toolbar.control !== 'color') {
- return null;
- }
-
- return (
- <ToolbarColorControl
- key={field.key}
- role={field.toolbar.role}
- value={String(settings[field.key] ?? field.defaultValue)}
- onChange={(value) => {
- selectedDrawing.updateSettings({
- [field.key]: value,
- });
- }}
- />
- );
- })}
-
- <Tooltip
- tooltipClassName={styles.toolbar_tooltip}
- showMessageOnFocus
- label={t('Settings')}
- location="top"
- >
- <Button
- size="sm"
- className={styles.button}
- onClick={onOpenSettings}
- label={<GearIcon />}
- />
- </Tooltip>
-
- <Tooltip
- tooltipClassName={styles.toolbar_tooltip}
- showMessageOnFocus
- label={lockLabel}
- location="top"
- >
- <Button
- size="sm"
- className={classNames(styles.button, {
- [styles.pressed]: isLocked,
- })}
- onClick={onToggleLock}
- label={isLocked ? <LockIcon /> : <LockOpenIcon />}
- />
- </Tooltip>
-
- <Tooltip
- tooltipClassName={styles.toolbar_tooltip}
- showMessageOnFocus
- label={t('Remove')}
- location="top"
- >
- <Button
- size="sm"
- className={styles.button}
- onClick={onDelete}
- label={<TrashIcon />}
- />
- </Tooltip>
- </div>
- );
-}
-
-function clampPosition(x: number, y: number, toolbar: HTMLElement, container: HTMLElement): Position {
- return {
- x: Math.max(0, Math.min(Math.round(x), container.clientWidth - toolbar.offsetWidth)),
- y: Math.max(0, Math.min(Math.round(y), container.clientHeight - toolbar.offsetHeight)),
- };
-}
diff --git a/src/components/Footer/index.tsx b/src/components/Footer/index.tsx
index aa0ae62..1fb40c1 100644
--- a/src/components/Footer/index.tsx
+++ b/src/components/Footer/index.tsx
@@ -1,3 +1,4 @@
+import classNames from 'classnames';
import dayjs from 'dayjs';
import { Button, Tooltip } from 'exchange-elements/v2';
@@ -10,6 +11,8 @@ import { Intervals, IntervalsToTimeframe, Timeframes } from '@src/types';
import { formatUtcOffset, useObservable } from '@src/utils';
+import coreStyles from '../../core/styles.module.scss';
+
import styles from './index.module.scss';
interface FooterProps {
@@ -32,7 +35,7 @@ export function Footer({ setInterval: setIntervalValue, intervalObs, supportedTi
return (
<footer className={styles.footer}>
- <div className={styles.intervals}>
+ <div className={classNames(styles.intervals, coreStyles.scrollableBox)}>
{(Object.keys(Intervals) as Intervals[])
.filter((interval: Intervals) => {
if (!IntervalsToTimeframe[interval]) {
diff --git a/src/components/FormFields/ColorField.tsx b/src/components/FormFields/ColorField.tsx
index 6752790..0655705 100644
--- a/src/components/FormFields/ColorField.tsx
+++ b/src/components/FormFields/ColorField.tsx
@@ -1,7 +1,10 @@
-import { ColorPicker, normalizeHexColor } from '@src/components/ColorPicker';
-import { Dropdown } from '@src/components/Dropdown';
+import { InputText } from 'exchange-elements/v2';
+import { ChangeEvent, useEffect, useRef, useState } from 'react';
+
+import { normalizeColor } from '@src/utils';
import styles from './index.module.scss';
+import { RangeField } from './RangeField';
interface ColorFieldProps {
label: string;
@@ -9,36 +12,104 @@ interface ColorFieldProps {
onChange: (value: string) => void;
}
-export function ColorField({ label, value, onChange }: ColorFieldProps) {
- const currentColor = normalizeHexColor(value);
+function normalizeHex(value: string): string {
+ const next = value.trim().startsWith('#') ? normalizeColor(value) : `#${normalizeColor(value)}`;
+
+ if (/^#[0-9a-f]{6}$/.test(next)) {
+ return `${next}ff`;
+ }
+
+ if (/^#[0-9a-f]{8}$/.test(next)) {
+ return next;
+ }
+
+ return '#000000ff';
+}
+
+function isValidHex(value: string): boolean {
+ return /^#([0-9a-f]{6}|[0-9a-f]{8})$/i.test(value) || /^([0-9a-f]{6}|[0-9a-f]{8})$/i.test(value);
+}
+
+export const ColorField = ({ label, value, onChange }: ColorFieldProps) => {
+ const colorInputRef = useRef<HTMLInputElement | null>(null);
+ const [draft, setDraft] = useState(normalizeHex(value));
+
+ useEffect(() => {
+ setDraft(normalizeHex(value));
+ }, [value]);
+
+ const current = isValidHex(draft) ? normalizeHex(draft) : normalizeHex(value);
+ const opacity = Math.round((parseInt(current.slice(7, 9), 16) / 255) * 100);
+
+ const handleTextChange = (event: ChangeEvent<HTMLInputElement>) => {
+ setDraft(event.target.value);
+ };
+
+ const handleTextBlur = () => {
+ if (!isValidHex(draft)) {
+ setDraft(normalizeHex(value));
+ return;
+ }
+
+ const next = normalizeHex(draft);
+
+ setDraft(next);
+ onChange(next);
+ };
+
+ const handleColorChange = (event: ChangeEvent<HTMLInputElement>) => {
+ const next = `${event.target.value}${current.slice(7, 9)}`;
+
+ setDraft(next);
+ onChange(next);
+ };
+
+ const handleOpacityChange = (event: ChangeEvent<HTMLInputElement>) => {
+ const alpha = Math.round((Number(event.target.value) / 100) * 255)
+ .toString(16)
+ .padStart(2, '0');
+
+ const next = `${current.slice(0, 7)}${alpha}`;
+
+ setDraft(next);
+ onChange(next);
+ };
return (
- <div className={styles.color}>
- <span className={styles.color_label}>{label}</span>
-
- <Dropdown
- selectedValue={currentColor}
- position="bottom"
- horizontalAlign="right"
- menuClassName={styles.color_menu}
- renderTrigger={({ onToggle }) => (
- <button
- type="button"
- className={styles.color_trigger}
- onClick={onToggle}
- >
- <span
- className={styles.color_preview}
- style={{ backgroundColor: currentColor }}
- />
- </button>
- )}
- >
- <ColorPicker
- value={currentColor}
- onChange={onChange}
+ <div className={styles.input_color}>
+ <div className={styles.input_wrapper}>
+ <InputText
+ value={draft}
+ label={label}
+ labelPos="top"
+ size="sm"
+ onChange={handleTextChange}
+ onBlur={handleTextBlur}
/>
- </Dropdown>
+
+ <button
+ type="button"
+ className={styles.button_color}
+ style={{ backgroundColor: current }}
+ onClick={() => colorInputRef.current?.click()}
+ />
+
+ <input
+ ref={colorInputRef}
+ type="color"
+ className={styles.input_hidden}
+ value={current.slice(0, 7)}
+ onChange={handleColorChange}
+ tabIndex={-1}
+ />
+ </div>
+
+ <RangeField
+ value={opacity}
+ color={current.slice(0, 7)}
+ suffix="%"
+ onChange={handleOpacityChange}
+ />
</div>
);
-}
+};
diff --git a/src/components/FormFields/RangeField.tsx b/src/components/FormFields/RangeField.tsx
index 0e105b5..c189ee3 100644
--- a/src/components/FormFields/RangeField.tsx
+++ b/src/components/FormFields/RangeField.tsx
@@ -1,4 +1,3 @@
-import classNames from 'classnames';
import { ChangeEvent, useId } from 'react';
import styles from './index.module.scss';
@@ -11,7 +10,6 @@ interface RangeFieldProps {
step?: number;
color?: string;
suffix?: string;
- size?: 'sm' | 'md';
onChange: (event: ChangeEvent<HTMLInputElement>) => void;
}
@@ -23,13 +21,12 @@ export const RangeField = ({
step = 1,
color = 'var(--blue-1)',
suffix = '%',
- size = 'md',
onChange,
}: RangeFieldProps) => {
const inputId = useId();
return (
- <div className={classNames(styles.range, { [styles.range_sm]: size === 'sm' })}>
+ <div className={styles.range}>
{label && (
<label
htmlFor={inputId}
diff --git a/src/components/FormFields/index.module.scss b/src/components/FormFields/index.module.scss
index 0f8c124..d1816b1 100644
--- a/src/components/FormFields/index.module.scss
+++ b/src/components/FormFields/index.module.scss
@@ -1,5 +1,3 @@
-@use '../../theme/mixins' as m;
-
[data-theme='mxt'],
[data-theme='mb'][data-mode='light'] {
--dd-item-text-selected: var(--neutral-2);
@@ -17,6 +15,7 @@
}
$button-color-bottom: 5px;
+$slider-thumb-top: -3px;
.input {
color: var(--neutral-12);
@@ -81,6 +80,15 @@ $button-color-bottom: 5px;
border-radius: var(--space-0125);
}
+.input_hidden {
+ position: absolute;
+ bottom: $button-color-bottom;
+ right: var(--space-0500);
+ width: var(--space-1375);
+ height: var(--space-1375);
+ opacity: 0;
+}
+
.input_checkbox {
padding: var(--space-0375) 0 !important;
@@ -98,58 +106,40 @@ $button-color-bottom: 5px;
}
.range {
- --range-input-height: var(--space-1000);
- --range-track-height: var(--space-0750);
- --range-thumb-size: var(--space-1000);
- --range-pattern-size: var(--space-0500);
- --range-label-font-size: var(--space-0875);
- --range-value-font-size: var(--space-0750);
- --range-wrapper-gap: var(--space-0500);
-
display: flex;
flex-direction: column;
gap: var(--space-0250);
- &_sm {
- --range-input-height: var(--space-0875);
- --range-track-height: var(--space-0500);
- --range-thumb-size: var(--space-0875);
- --range-pattern-size: var(--space-0375);
- --range-label-font-size: var(--space-0750);
- --range-wrapper-gap: var(--space-0375);
- }
-
&_label {
- color: var(--neutral-12);
- font-size: var(--range-label-font-size);
+ font-size: var(--space-0875);
+ color: var(--neutral-11);
}
&_wrapper {
display: grid;
align-items: center;
grid-template: 1fr / 1fr auto;
- gap: var(--range-wrapper-gap);
+ gap: var(--space-0500);
}
&_value {
min-width: var(--space-2000);
- color: var(--neutral-12);
- font-size: var(--range-value-font-size);
- line-height: normal;
+ color: var(--neutral-11);
+ font-size: var(--space-0750);
text-align: right;
}
&_input {
--range-color: var(--blue-1);
-
- width: 100%;
- height: var(--range-input-height);
+ -webkit-appearance: none;
appearance: none;
- background: transparent;
+ width: 100%;
+ height: var(--space-1000);
cursor: pointer;
+ background: transparent;
&::-webkit-slider-runnable-track {
- height: var(--range-track-height);
+ height: var(--space-0750);
border: 1px solid var(--color-field-border);
border-radius: 999px;
background: linear-gradient(to right, transparent 0%, var(--range-color) 100%),
@@ -159,30 +149,31 @@ $button-color-bottom: 5px;
linear-gradient(-45deg, transparent 75%, var(--color-field-square) 75%);
background-size:
100% 100%,
- var(--range-pattern-size) var(--range-pattern-size),
- var(--range-pattern-size) var(--range-pattern-size),
- var(--range-pattern-size) var(--range-pattern-size),
- var(--range-pattern-size) var(--range-pattern-size);
+ var(--space-0500) var(--space-0500),
+ var(--space-0500) var(--space-0500),
+ var(--space-0500) var(--space-0500),
+ var(--space-0500) var(--space-0500);
background-position:
0 0,
0 0,
- 0 calc(var(--range-pattern-size) / 2),
- calc(var(--range-pattern-size) / 2) calc(var(--range-pattern-size) / -2),
- calc(var(--range-pattern-size) / -2) 0;
+ 0 calc(var(--space-0500) / 2),
+ calc(var(--space-0500) / 2) calc(var(--space-0500) / -2),
+ calc(var(--space-0500) / -2) 0;
}
&::-webkit-slider-thumb {
- width: var(--range-thumb-size);
- height: var(--range-thumb-size);
- margin-top: calc((var(--range-track-height) - var(--range-thumb-size)) / 2 - 1px);
+ -webkit-appearance: none;
appearance: none;
+ width: var(--space-1000);
+ height: var(--space-1000);
+ margin-top: $slider-thumb-top;
border: 1px solid var(--color-field-border);
border-radius: 50%;
background-color: var(--neutral-panel);
}
&::-moz-range-track {
- height: var(--range-track-height);
+ height: var(--space-0750);
border: 1px solid var(--color-field-border);
border-radius: 999px;
background: linear-gradient(to right, transparent 0%, var(--range-color) 100%),
@@ -192,16 +183,16 @@ $button-color-bottom: 5px;
linear-gradient(-45deg, transparent 75%, var(--color-field-square) 75%);
background-size:
100% 100%,
- var(--range-pattern-size) var(--range-pattern-size),
- var(--range-pattern-size) var(--range-pattern-size),
- var(--range-pattern-size) var(--range-pattern-size),
- var(--range-pattern-size) var(--range-pattern-size);
+ var(--space-0500) var(--space-0500),
+ var(--space-0500) var(--space-0500),
+ var(--space-0500) var(--space-0500),
+ var(--space-0500) var(--space-0500);
background-position:
0 0,
0 0,
- 0 calc(var(--range-pattern-size) / 2),
- calc(var(--range-pattern-size) / 2) calc(var(--range-pattern-size) / -2),
- calc(var(--range-pattern-size) / -2) 0;
+ 0 calc(var(--space-0500) / 2),
+ calc(var(--space-0500) / 2) calc(var(--space-0500) / -2),
+ calc(var(--space-0500) / -2) 0;
}
&::-moz-range-progress {
@@ -209,51 +200,11 @@ $button-color-bottom: 5px;
}
&::-moz-range-thumb {
- width: var(--range-thumb-size);
- height: var(--range-thumb-size);
+ width: var(--space-1000);
+ height: var(--space-1000);
border: 1px solid var(--color-field-border);
border-radius: 50%;
background-color: var(--neutral-panel);
}
}
}
-
-.color {
- width: 100%;
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: var(--space-1000);
-
- &_label {
- font-size: var(--space-0875);
- color: var(--neutral-12);
- font-weight: var(--font-medium);
- }
-
- &_menu {
- --dropdown-width: max-content;
- padding: var(--space-1000);
- }
-
- &_trigger {
- min-width: none;
- width: var(--space-2000);
- height: var(--space-2000);
- display: flex;
- justify-content: center;
- align-items: center;
- border-radius: var(--space-0375);
- border: var(--borderWidth-md) solid var(--neutral-9);
-
- p {
- padding: 0;
- }
- }
-
- &_preview {
- width: var(--space-1375);
- height: var(--space-1375);
- border-radius: var(--space-0250);
- }
-}
diff --git a/src/components/Icon/Fill.tsx b/src/components/Icon/Fill.tsx
deleted file mode 100644
index e8fb0ac..0000000
--- a/src/components/Icon/Fill.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import { SVGProps } from 'react';
-
-export default function FillIcon(props: SVGProps<SVGSVGElement>) {
- return (
- <svg
- width="16"
- height="16"
- viewBox="0 0 16 16"
- fill="none"
- xmlns="http://www.w3.org/2000/svg"
- {...props}
- >
- <g clipPath="url(#clip0_15020_16553)">
- <path
- d="M10.8 5.20039L8.40005 2.80039L2.80005 8.40039L8.87205 14.4724C9.17183 14.7704 9.57735 14.9377 10 14.9377C10.4227 14.9377 10.8283 14.7704 11.128 14.4724L14.472 11.1284C14.7701 10.8286 14.9373 10.4231 14.9373 10.0004C14.9373 9.57769 14.7701 9.17217 14.472 8.87239L10.8 5.20039ZM10.8 5.20039V2.00039C10.8 1.57604 10.6315 1.16908 10.3314 0.86902C10.0314 0.568962 9.6244 0.400391 9.20005 0.400391C8.7757 0.400391 8.36874 0.568962 8.06868 0.86902C7.76862 1.16908 7.60005 1.57604 7.60005 2.00039V6.80039"
- stroke="currentColor"
- />
- <path
- d="M0 13.1996C0 11.9996 2 9.59961 2 9.59961C2 9.59961 4 11.9996 4 13.1996C4 14.3996 3.2 15.1996 2 15.1996C0.8 15.1996 0 14.3996 0 13.1996Z"
- fill="currentColor"
- />
- <path
- d="M7.60002 8.80039C8.26277 8.80039 8.80002 8.26313 8.80002 7.60039C8.80002 6.93765 8.26277 6.40039 7.60002 6.40039C6.93728 6.40039 6.40002 6.93765 6.40002 7.60039C6.40002 8.26313 6.93728 8.80039 7.60002 8.80039Z"
- fill="currentColor"
- />
- </g>
- <defs>
- <clipPath id="clip0_15020_16553">
- <rect
- width="16"
- height="16"
- fill="currentColor"
- />
- </clipPath>
- </defs>
- </svg>
- );
-}
diff --git a/src/components/Icon/Lock.tsx b/src/components/Icon/Lock.tsx
deleted file mode 100644
index fd1c8a5..0000000
--- a/src/components/Icon/Lock.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import { SVGProps } from 'react';
-
-export default function LockIcon(props: SVGProps<SVGSVGElement>) {
- return (
- <svg
- width="16"
- height="16"
- viewBox="0 0 16 16"
- fill="none"
- xmlns="http://www.w3.org/2000/svg"
- {...props}
- >
- <g
- fill="currentColor"
- stroke="currentColor"
- strokeWidth="0.3"
- >
- <path
- fillRule="evenodd"
- clipRule="evenodd"
- d="M8.48571 1.68571C7.94013 1.68571 7.41688 1.90245 7.03109 2.28824C6.64531 2.67403 6.42857 3.19727 6.42857 3.74286V5.8H10.5429V3.74286C10.5429 3.19727 10.3261 2.67403 9.94033 2.28824C9.55455 1.90245 9.0313 1.68571 8.48571 1.68571ZM11.2286 5.8V3.74286C11.2286 3.01541 10.9396 2.31775 10.4252 1.80336C9.91082 1.28898 9.21317 1 8.48571 1C7.75826 1 7.06061 1.28898 6.54622 1.80336C6.03184 2.31775 5.74286 3.01541 5.74286 3.74286V5.8H4.71429C4.25963 5.8 3.82359 5.98061 3.5021 6.3021C3.18061 6.62359 3 7.05963 3 7.51429V12.3143C3 12.5394 3.04434 12.7623 3.13049 12.9703C3.21664 13.1783 3.34292 13.3673 3.5021 13.5265C3.66129 13.6857 3.85027 13.8119 4.05826 13.8981C4.26624 13.9842 4.48916 14.0286 4.71429 14.0286H12.2571C12.4823 14.0286 12.7052 13.9842 12.9132 13.8981C13.1212 13.8119 13.3101 13.6857 13.4693 13.5265C13.6285 13.3673 13.7548 13.1783 13.8409 12.9703C13.9271 12.7623 13.9714 12.5394 13.9714 12.3143V7.51429C13.9714 7.05963 13.7908 6.62359 13.4693 6.3021C13.1478 5.98061 12.7118 5.8 12.2571 5.8H11.2286ZM7.8 9.22857C7.8 9.04671 7.87224 8.87229 8.00084 8.7437C8.12944 8.6151 8.30385 8.54286 8.48571 8.54286C8.66758 8.54286 8.84199 8.6151 8.97059 8.7437C9.09918 8.87229 9.17143 9.04671 9.17143 9.22857V10.6C9.17143 10.7819 9.09918 10.9563 8.97059 11.0849C8.84199 11.2135 8.66758 11.2857 8.48571 11.2857C8.30385 11.2857 8.12944 11.2135 8.00084 11.0849C7.87224 10.9563 7.8 10.7819 7.8 10.6V9.22857ZM3.68571 7.51429C3.68571 6.94514 4.14514 6.48571 4.71429 6.48571H12.2571C12.8263 6.48571 13.2857 6.94514 13.2857 7.51429V12.3143C13.2857 12.8834 12.8263 13.3429 12.2571 13.3429H4.71429C4.44149 13.3429 4.17987 13.2345 3.98698 13.0416C3.79408 12.8487 3.68571 12.5871 3.68571 12.3143V7.51429Z"
- fill="currentColor"
- />
- </g>
- </svg>
- );
-}
diff --git a/src/components/Icon/LockOpen.tsx b/src/components/Icon/LockOpen.tsx
deleted file mode 100644
index 70924bb..0000000
--- a/src/components/Icon/LockOpen.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import { SVGProps } from 'react';
-
-export default function LockOpenIcon(props: SVGProps<SVGSVGElement>) {
- return (
- <svg
- width="16"
- height="16"
- viewBox="0 0 16 16"
- fill="none"
- xmlns="http://www.w3.org/2000/svg"
- {...props}
- >
- <g
- fill="currentColor"
- stroke="currentColor"
- strokeWidth="0.3"
- >
- <path
- fillRule="evenodd"
- clipRule="evenodd"
- d="M8.48632 1.68579C7.94067 1.68579 7.41737 1.90255 7.03154 2.28838C6.64571 2.67421 6.42895 3.19751 6.42895 3.74316V5.80053H12.2582C12.7129 5.80053 13.1489 5.98116 13.4705 6.30268C13.792 6.62421 13.9726 7.06029 13.9726 7.515V12.3155C13.9726 12.5407 13.9283 12.7636 13.8421 12.9716C13.756 13.1796 13.6297 13.3686 13.4705 13.5278C13.3113 13.687 13.1223 13.8133 12.9143 13.8995C12.7062 13.9857 12.4833 14.03 12.2582 14.03H4.71447C4.48933 14.03 4.26638 13.9857 4.05837 13.8995C3.85036 13.8133 3.66136 13.687 3.50216 13.5278C3.34295 13.3686 3.21667 13.1796 3.13051 12.9716C3.04435 12.7636 3 12.5407 3 12.3155V7.515C3 7.06029 3.18063 6.62421 3.50216 6.30268C3.82368 5.98116 4.25977 5.80053 4.71447 5.80053H5.74316V3.74316C5.74316 3.01563 6.03217 2.31789 6.54661 1.80345C7.06105 1.28901 7.75879 1 8.48632 1C9.21385 1 9.91158 1.28901 10.426 1.80345C10.9405 2.31789 11.2295 3.01563 11.2295 3.74316H10.5437C10.5437 3.19751 10.3269 2.67421 9.9411 2.28838C9.55526 1.90255 9.03196 1.68579 8.48632 1.68579ZM7.80053 9.22947C7.80053 9.04759 7.87278 8.87316 8.00139 8.74455C8.13 8.61594 8.30443 8.54368 8.48632 8.54368C8.6682 8.54368 8.84263 8.61594 8.97124 8.74455C9.09985 8.87316 9.17211 9.04759 9.17211 9.22947V10.6011C9.17211 10.7829 9.09985 10.9574 8.97124 11.086C8.84263 11.2146 8.6682 11.2868 8.48632 11.2868C8.30443 11.2868 8.13 11.2146 8.00139 11.086C7.87278 10.9574 7.80053 10.7829 7.80053 10.6011V9.22947ZM3.68579 7.515C3.68579 6.94579 4.14527 6.48632 4.71447 6.48632H12.2582C12.8274 6.48632 13.2868 6.94579 13.2868 7.515V12.3155C13.2868 12.8847 12.8274 13.3442 12.2582 13.3442H4.71447C4.44165 13.3442 4.18 13.2358 3.98708 13.0429C3.79417 12.85 3.68579 12.5884 3.68579 12.3155V7.515Z"
- fill="currentColor"
- />
- </g>
- </svg>
- );
-}
diff --git a/src/components/Icon/ParallelChannel.tsx b/src/components/Icon/ParallelChannel.tsx
deleted file mode 100644
index 24fe36c..0000000
--- a/src/components/Icon/ParallelChannel.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import { SVGProps } from 'react';
-
-export default function ParallelChannelIcon(props: SVGProps<SVGSVGElement>) {
- return (
- <svg
- width="16"
- height="16"
- viewBox="0 0 16 16"
- fill="none"
- xmlns="http://www.w3.org/2000/svg"
- {...props}
- >
- <path
- d="M3.98553 10.8439L10.8427 3.98675L10.3579 3.50195L3.50073 10.3591L3.98553 10.8439ZM6.72839 15.6439L10.157 12.2153L9.67216 11.7305L6.24359 15.1591L6.72839 15.6439Z"
- fill="currentColor"
- />
- <path
- d="M12.2142 10.1575L15.6427 6.72894L15.1579 6.24414L11.7294 9.67271L12.2142 10.1575Z"
- fill="currentColor"
- />
- <path
- d="M11.6286 3.74286C12.1963 3.74286 12.6571 3.28206 12.6571 2.71429C12.6571 2.14651 12.1963 1.68571 11.6286 1.68571C11.0608 1.68571 10.6 2.14651 10.6 2.71429C10.6 3.28206 11.0608 3.74286 11.6286 3.74286ZM11.6286 4.42857C10.6816 4.42857 9.91429 3.66126 9.91429 2.71429C9.91429 1.76731 10.6816 1 11.6286 1C12.5755 1 13.3429 1.76731 13.3429 2.71429C13.3429 3.66126 12.5755 4.42857 11.6286 4.42857ZM2.71429 12.6571C3.28206 12.6571 3.74286 12.1963 3.74286 11.6286C3.74286 11.0608 3.28206 10.6 2.71429 10.6C2.14651 10.6 1.68571 11.0608 1.68571 11.6286C1.68571 12.1963 2.14651 12.6571 2.71429 12.6571ZM2.71429 13.3429C1.76731 13.3429 1 12.5755 1 11.6286C1 10.6816 1.76731 9.91429 2.71429 9.91429C3.66126 9.91429 4.42857 10.6816 4.42857 11.6286C4.42857 12.5755 3.66126 13.3429 2.71429 13.3429ZM10.9429 11.9714C11.5106 11.9714 11.9714 11.5106 11.9714 10.9429C11.9714 10.3751 11.5106 9.91429 10.9429 9.91429C10.3751 9.91429 9.91429 10.3751 9.91429 10.9429C9.91429 11.5106 10.3751 11.9714 10.9429 11.9714ZM10.9429 12.6571C9.99589 12.6571 9.22857 11.8898 9.22857 10.9429C9.22857 9.99589 9.99589 9.22857 10.9429 9.22857C11.8898 9.22857 12.6571 9.99589 12.6571 10.9429C12.6571 11.8898 11.8898 12.6571 10.9429 12.6571Z"
- fill="currentColor"
- />
- </svg>
- );
-}
diff --git a/src/components/Icon/Pencil.tsx b/src/components/Icon/Pencil.tsx
deleted file mode 100644
index 6cc63ad..0000000
--- a/src/components/Icon/Pencil.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import { SVGProps } from 'react';
-
-export default function PencilIcon(props: SVGProps<SVGSVGElement>) {
- return (
- <svg
- width="16"
- height="16"
- viewBox="0 0 16 16"
- fill="none"
- xmlns="http://www.w3.org/2000/svg"
- {...props}
- >
- <path
- d="M9.9208 1.6106C10.1136 1.41706 10.3427 1.2635 10.5949 1.15872C10.8472 1.05394 11.1176 1 11.3908 1C11.664 1 11.9344 1.05394 12.1867 1.15872C12.4389 1.2635 12.668 1.41706 12.8608 1.6106L13.8352 2.585C14.6416 3.3998 14.6416 4.7186 13.8352 5.525L13.348 6.0122L5.872 13.4882L5.032 14.3282L4.9144 14.4458H1V10.5398L1.1176 10.4138L1.9576 9.57379L9.4336 2.0978L9.9208 1.6106ZM12.2728 2.1986C12.1573 2.08227 12.0198 1.98996 11.8685 1.92696C11.7171 1.86397 11.5548 1.83154 11.3908 1.83154C11.2268 1.83154 11.0645 1.86397 10.9131 1.92696C10.7618 1.98996 10.6243 2.08227 10.5088 2.1986L10.3156 2.3918L13.054 5.1302L13.2472 4.937C13.7344 4.4498 13.7344 3.6602 13.2472 3.173L12.2728 2.1986ZM12.466 5.7266L9.7276 2.9798L2.8396 9.86779L5.5696 12.6146L12.4576 5.7266H12.466ZM4.99 13.2026L2.2432 10.4642L1.8232 10.8842V13.6058H4.57L4.99 13.1858V13.2026Z"
- fill="currentColor"
- />
- </svg>
- );
-}
diff --git a/src/components/Icon/index.tsx b/src/components/Icon/index.tsx
index 7437ff4..ae6dab3 100644
--- a/src/components/Icon/index.tsx
+++ b/src/components/Icon/index.tsx
@@ -9,7 +9,6 @@ import DiapsonDatesIcon from './DiapsonDates';
import DiapsonPricesIcon from './DiapsonPrices';
import EyeBrushIcon from './EyeBrush';
import FibonacciRetracementIcon from './FibonacciRetracement';
-import FillIcon from './Fill';
import FixedProfileIcon from './FixedProfile';
import FullscreenIcon from './Fullscreen';
import GearIcon from './Gear';
@@ -18,12 +17,8 @@ import HorizontalLineIcon from './HorizontalLine';
import HorizontalRayIcon from './HorizontalRay';
import LayersIcon from './Layers';
import LineIcon from './Line';
-import LockIcon from './Lock';
-import LockOpenIcon from './LockOpen';
import MagnetIcon from './Magnet';
import MinusIcon from './Minus';
-import ParallelChannelIcon from './ParallelChannel';
-import PencilIcon from './Pencil';
import PencilLockerIcon from './PencilLocker';
import PlusIcon from './Plus';
import PlusCircleIcon from './PlusCircle';
@@ -61,7 +56,6 @@ export {
DiapsonPricesIcon,
EyeBrushIcon,
FibonacciRetracementIcon,
- FillIcon,
FixedProfileIcon,
FullscreenIcon,
GearIcon,
@@ -70,12 +64,8 @@ export {
HorizontalRayIcon,
LayersIcon,
LineIcon,
- LockIcon,
- LockOpenIcon,
MagnetIcon,
MinusIcon,
- ParallelChannelIcon,
- PencilIcon,
PencilLockerIcon,
PlusCircleIcon,
PlusIcon,
diff --git a/src/components/Portal/index.tsx b/src/components/Portal/index.tsx
index 9901066..22baa95 100644
--- a/src/components/Portal/index.tsx
+++ b/src/components/Portal/index.tsx
@@ -1,13 +1,12 @@
-import { ReactNode } from 'react';
+import { PropsWithChildren, ReactPortal } from 'react';
import { createPortal } from 'react-dom';
import { getPortalHost } from '@src/utils';
-interface PortalProps {
- children: ReactNode;
- container?: HTMLElement | null;
+interface PortalProps extends PropsWithChildren {
+ container?: Element | DocumentFragment;
}
-export function Portal({ children, container }: PortalProps) {
+export function Portal({ children, container }: PortalProps): ReactPortal {
return createPortal(children, container ?? getPortalHost());
}
diff --git a/src/components/PriceScaleControls/index.tsx b/src/components/PriceScaleControls/index.tsx
index a6aa4f3..7f342dc 100644
--- a/src/components/PriceScaleControls/index.tsx
+++ b/src/components/PriceScaleControls/index.tsx
@@ -1,7 +1,6 @@
import classNames from 'classnames';
import { Button, Tooltip } from 'exchange-elements/v2';
-import { CHART_PRICE_SCALE_CONTROLS_CONTENT } from '@src/constants';
import { t } from '@src/translations';
import styles from './index.module.scss';
@@ -27,7 +26,7 @@ export function PriceScaleControls({
return (
<section
- className={classNames(styles.root, CHART_PRICE_SCALE_CONTROLS_CONTENT)}
+ className={classNames(styles.root, 'moex-price-scale-controls-content')}
onPointerDown={stopPropagation}
onMouseDown={stopPropagation}
onClick={stopPropagation}
diff --git a/src/components/Toolbar/constants.tsx b/src/components/Toolbar/constants.tsx
index 45aaf28..8e64384 100644
--- a/src/components/Toolbar/constants.tsx
+++ b/src/components/Toolbar/constants.tsx
@@ -4,7 +4,6 @@ import {
FibonacciRetracementIcon,
FixedProfileIcon,
HorizontalLineIcon,
- ParallelChannelIcon,
RayIcon,
RectangleIcon,
SlidersHorizontalLongDashedIcon,
@@ -24,7 +23,6 @@ export const trendLines = (): MenuOption<DrawingsNames>[] => [
{ value: DrawingsNames.horizontalLine, icon: <HorizontalLineIcon />, label: t('Horizontal line') },
// { value: DrawingsNames.horizontalRay, icon: <HorizontalRayIcon />, label: t('Horizontal ray') },
{ value: DrawingsNames.verticalLine, icon: <VerticalLineIcon />, label: t('Vertical ray') },
- { value: DrawingsNames.parallelChannel, icon: <ParallelChannelIcon />, label: t('Parallel channel') },
];
export const measurementTools = (): MenuOption<DrawingsNames>[] => [
@@ -32,7 +30,7 @@ export const measurementTools = (): MenuOption<DrawingsNames>[] => [
{ value: DrawingsNames.sliderShort, icon: <SlidersHorizontalShortDashedIcon />, label: t('Short position') },
{ value: DrawingsNames.diapsonDates, icon: <DiapsonDatesIcon />, label: t('Dates range') },
{ value: DrawingsNames.diapsonPrices, icon: <DiapsonPricesIcon />, label: t('Prices range') },
- { value: DrawingsNames.fixedRangeProfile, icon: <FixedProfileIcon />, label: t('Fixed range volume profile') },
+ { value: DrawingsNames.fixedRangeProfile, icon: <FixedProfileIcon />, label: t('Fixed range profile volume') },
{
value: DrawingsNames.visibleRangeProfile,
icon: <VisibleRangeProfileIcon />,
diff --git a/src/components/ToolbarColorControl/constants.tsx b/src/components/ToolbarColorControl/constants.tsx
deleted file mode 100644
index 6417643..0000000
--- a/src/components/ToolbarColorControl/constants.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { FillIcon, PencilIcon, TypeIcon } from '@src/components/Icon';
-
-import type { ToolbarColorRole } from '@src/types/settings';
-
-type ToolbarColorLabelKey = 'Line color' | 'Background color' | 'Text color';
-
-interface ToolbarColorConfig {
- labelKey: ToolbarColorLabelKey;
- Icon: JSX.Element;
-}
-
-export const TOOLBAR_COLOR_CONFIG: Record<ToolbarColorRole, ToolbarColorConfig> = {
- line: {
- labelKey: 'Line color',
- Icon: <PencilIcon />,
- },
- fill: {
- labelKey: 'Background color',
- Icon: <FillIcon />,
- },
- text: {
- labelKey: 'Text color',
- Icon: <TypeIcon />,
- },
-};
diff --git a/src/components/ToolbarColorControl/index.module.scss b/src/components/ToolbarColorControl/index.module.scss
deleted file mode 100644
index 8c23a53..0000000
--- a/src/components/ToolbarColorControl/index.module.scss
+++ /dev/null
@@ -1,35 +0,0 @@
-@use '../../theme/mixins' as m;
-
-.toolbar {
- &_tooltip {
- @include m.tooltipHint;
- }
-
- &_trigger {
- position: relative;
- display: flex;
- align-items: center;
- justify-content: space-between;
- flex-direction: column;
- width: var(--space-2000);
- height: var(--space-2000);
- padding: var(--space-0250);
- background-color: transparent;
- }
-
- &_icon {
- display: block;
- color: var(--neutral-12);
- }
-
- &_preview {
- display: block;
- width: 100%;
- height: var(--space-0250);
- border-radius: var(--space-0125);
- }
-
- &_menu {
- --dropdown-width: max-content;
- }
-}
diff --git a/src/components/ToolbarColorControl/index.tsx b/src/components/ToolbarColorControl/index.tsx
deleted file mode 100644
index 36da71c..0000000
--- a/src/components/ToolbarColorControl/index.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import { Tooltip } from 'exchange-elements/v2';
-
-import { ColorPicker, normalizeHexColor } from '@src/components/ColorPicker';
-import { Dropdown } from '@src/components/Dropdown';
-import { TOOLBAR_COLOR_CONFIG } from '@src/components/ToolbarColorControl/constants';
-import { t } from '@src/translations';
-
-import styles from './index.module.scss';
-
-import type { ToolbarColorRole } from '@src/types/settings';
-
-interface ToolbarColorControlProps {
- role: ToolbarColorRole;
- value: string;
- onChange: (value: string) => void;
-}
-
-export function ToolbarColorControl({ role, value, onChange }: ToolbarColorControlProps) {
- const currentColor = normalizeHexColor(value);
- const { labelKey, Icon } = TOOLBAR_COLOR_CONFIG[role];
-
- return (
- <Dropdown
- position="bottom"
- horizontalAlign="auto"
- menuClassName={styles.toolbar_menu}
- renderTrigger={({ onToggle }) => (
- <Tooltip
- tooltipClassName={styles.toolbar_tooltip}
- showMessageOnFocus
- location="top"
- label={t(labelKey)}
- >
- <button
- type="button"
- className={styles.toolbar_trigger}
- onClick={onToggle}
- >
- <span className={styles.toolbar_icon}>{Icon}</span>
- <span
- className={styles.toolbar_preview}
- style={{ backgroundColor: currentColor }}
- />
- </button>
- </Tooltip>
- )}
- >
- <ColorPicker
- value={currentColor}
- onChange={onChange}
- />
- </Dropdown>
- );
-}
diff --git a/src/constants/dom.ts b/src/constants/dom.ts
index 472ea00..abbf7b6 100644
--- a/src/constants/dom.ts
+++ b/src/constants/dom.ts
@@ -1,8 +1,2 @@
export const CHART_ROOT_CLASSNAME = 'moex-chart-root';
export const CHART_PORTAL_HOST_CLASSNAME = 'moex-chart-portal-host';
-export const CHART_MODAL_CONTAINER_CLASSNAME = 'moex-chart-modal-container';
-export const CHART_PANE_OVERLAY_CONTAINER = 'moex-chart-pane-overlay-container';
-export const CHART_DRAWING_TOOLBAR_CONTAINER = 'moex-chart-drawing-toolbar-container';
-export const CHART_PRICE_SCALE_CONTROLS = 'moex-price-scale-controls';
-export const CHART_PRICE_SCALE_CONTROLS_CONTENT = `${CHART_PRICE_SCALE_CONTROLS}-content`;
-export const CHART_PRICE_SCALE_CONTROLS_VISIBLE = `${CHART_PRICE_SCALE_CONTROLS}_visible`;
diff --git a/src/constants/drawing.ts b/src/constants/drawing.ts
index 3899273..dc6b473 100644
--- a/src/constants/drawing.ts
+++ b/src/constants/drawing.ts
@@ -1,7 +1,6 @@
import { AxisLine } from '@src/core/Drawings/axisLine';
import { Diapson } from '@src/core/Drawings/diapson';
import { FibonacciRetracement } from '@src/core/Drawings/fibonacciRetracement';
-import { ParallelChannel } from '@src/core/Drawings/parallelChannel';
import { Ray } from '@src/core/Drawings/ray';
import { Rectangle } from '@src/core/Drawings/rectangle';
import { Ruler } from '@src/core/Drawings/ruler';
@@ -15,7 +14,6 @@ import { DrawingConfig } from '@src/types';
export enum DrawingsNames {
'trendLine' = 'trendLine',
- 'parallelChannel' = 'parallelChannel',
'ray' = 'ray',
'horizontalLine' = 'horizontalLine',
'horizontalRay' = 'horizontalRay',
@@ -39,7 +37,6 @@ export enum DrawingsNames {
export const drawingLabelById = (): Record<DrawingsNames, string> => ({
[DrawingsNames.trendLine]: t('Trend line'),
- [DrawingsNames.parallelChannel]: t('Parallel Channel'),
[DrawingsNames.ray]: t('Ray'),
[DrawingsNames.horizontalLine]: t('Horizontal line'),
[DrawingsNames.horizontalRay]: t('Horizontal ray'),
@@ -50,7 +47,7 @@ export const drawingLabelById = (): Record<DrawingsNames, string> => ({
[DrawingsNames.sliderShort]: t('Short position'),
[DrawingsNames.diapsonDates]: t('Dates range'),
[DrawingsNames.diapsonPrices]: t('Prices range'),
- [DrawingsNames.fixedRangeProfile]: t('Fixed range volume profile'),
+ [DrawingsNames.fixedRangeProfile]: t('Fixed range profile volume'),
[DrawingsNames.visibleRangeProfile]: t('Anchored volume profile'),
[DrawingsNames.rectangle]: t('Rectangle'),
[DrawingsNames.traectory]: t('Traectory'),
@@ -59,31 +56,19 @@ export const drawingLabelById = (): Record<DrawingsNames, string> => ({
export const drawingsMap: Record<DrawingsNames, DrawingConfig> = {
[DrawingsNames.trendLine]: {
- construct: ({ chart, series, container, eventManager, interaction, removeSelf, openSettings }) => {
+ construct: ({ chart, series, container, eventManager, removeSelf, openSettings }) => {
return new TrendLine(chart, series, {
container,
- interaction,
formatObservable: eventManager.getChartOptionsModel(),
removeSelf,
openSettings,
});
},
},
- [DrawingsNames.parallelChannel]: {
- construct: ({ chart, series, container, eventManager, interaction, openSettings }) => {
- return new ParallelChannel(chart, series, {
- container,
- interaction,
- formatObservable: eventManager.getChartOptionsModel(),
- openSettings,
- });
- },
- },
[DrawingsNames.ray]: {
- construct: ({ chart, series, container, eventManager, interaction, removeSelf, openSettings }) => {
+ construct: ({ chart, series, container, eventManager, removeSelf, openSettings }) => {
return new Ray(chart, series, {
container,
- interaction,
formatObservable: eventManager.getChartOptionsModel(),
removeSelf,
openSettings,
@@ -91,11 +76,10 @@ export const drawingsMap: Record<DrawingsNames, DrawingConfig> = {
},
},
[DrawingsNames.horizontalLine]: {
- construct: ({ chart, series, eventManager, container, interaction, removeSelf, openSettings }) => {
+ construct: ({ chart, series, eventManager, container, removeSelf, openSettings }) => {
return new AxisLine(chart, series, {
direction: 'horizontal',
container,
- interaction,
formatObservable: eventManager.getChartOptionsModel(),
removeSelf,
openSettings,
@@ -103,10 +87,9 @@ export const drawingsMap: Record<DrawingsNames, DrawingConfig> = {
},
},
[DrawingsNames.horizontalRay]: {
- construct: ({ chart, series, container, eventManager, interaction, removeSelf, openSettings }) => {
+ construct: ({ chart, series, container, eventManager, removeSelf, openSettings }) => {
return new TrendLine(chart, series, {
container,
- interaction,
formatObservable: eventManager.getChartOptionsModel(),
removeSelf,
openSettings,
@@ -114,11 +97,10 @@ export const drawingsMap: Record<DrawingsNames, DrawingConfig> = {
},
},
[DrawingsNames.verticalLine]: {
- construct: ({ chart, series, eventManager, container, interaction, removeSelf, openSettings }) => {
+ construct: ({ chart, series, eventManager, container, removeSelf, openSettings }) => {
return new AxisLine(chart, series, {
direction: 'vertical',
container,
- interaction,
formatObservable: eventManager.getChartOptionsModel(),
removeSelf,
openSettings,
@@ -126,11 +108,10 @@ export const drawingsMap: Record<DrawingsNames, DrawingConfig> = {
},
},
[DrawingsNames.sliderLong]: {
- construct: ({ chart, series, eventManager, container, interaction, removeSelf, openSettings }) => {
+ construct: ({ chart, series, eventManager, container, removeSelf, openSettings }) => {
return new SliderPosition(chart, series, {
side: 'long',
container,
- interaction,
formatObservable: eventManager.getChartOptionsModel(),
removeSelf,
openSettings,
@@ -138,10 +119,9 @@ export const drawingsMap: Record<DrawingsNames, DrawingConfig> = {
},
},
[DrawingsNames.fibonacciRetracement]: {
- construct: ({ chart, series, eventManager, container, interaction, removeSelf, openSettings }) => {
+ construct: ({ chart, series, eventManager, container, removeSelf, openSettings }) => {
return new FibonacciRetracement(chart, series, {
container,
- interaction,
formatObservable: eventManager.getChartOptionsModel(),
removeSelf,
openSettings,
@@ -149,11 +129,10 @@ export const drawingsMap: Record<DrawingsNames, DrawingConfig> = {
},
},
[DrawingsNames.sliderShort]: {
- construct: ({ chart, series, eventManager, container, interaction, removeSelf, openSettings }) => {
+ construct: ({ chart, series, eventManager, container, removeSelf, openSettings }) => {
return new SliderPosition(chart, series, {
side: 'short',
container,
- interaction,
formatObservable: eventManager.getChartOptionsModel(),
removeSelf,
openSettings,
@@ -161,11 +140,10 @@ export const drawingsMap: Record<DrawingsNames, DrawingConfig> = {
},
},
[DrawingsNames.diapsonDates]: {
- construct: ({ chart, series, container, eventManager, interaction, removeSelf, openSettings }) => {
+ construct: ({ chart, series, container, eventManager, removeSelf, openSettings }) => {
return new Diapson(chart, series, {
rangeMode: 'date',
container,
- interaction,
formatObservable: eventManager.getChartOptionsModel(),
removeSelf,
openSettings,
@@ -173,11 +151,10 @@ export const drawingsMap: Record<DrawingsNames, DrawingConfig> = {
},
},
[DrawingsNames.diapsonPrices]: {
- construct: ({ chart, series, container, eventManager, interaction, removeSelf, openSettings }) => {
+ construct: ({ chart, series, container, eventManager, removeSelf, openSettings }) => {
return new Diapson(chart, series, {
rangeMode: 'price',
container,
- interaction,
formatObservable: eventManager.getChartOptionsModel(),
removeSelf,
openSettings,
@@ -185,11 +162,10 @@ export const drawingsMap: Record<DrawingsNames, DrawingConfig> = {
},
},
[DrawingsNames.fixedRangeProfile]: {
- construct: ({ chart, series, container, eventManager, interaction, removeSelf, openSettings }) => {
+ construct: ({ chart, series, container, eventManager, removeSelf, openSettings }) => {
return new VolumeProfile(chart, series, {
profileKind: 'fixedRange',
container,
- interaction,
formatObservable: eventManager.getChartOptionsModel(),
removeSelf,
openSettings,
@@ -198,11 +174,10 @@ export const drawingsMap: Record<DrawingsNames, DrawingConfig> = {
},
[DrawingsNames.visibleRangeProfile]: {
singleInstance: true,
- construct: ({ chart, series, container, eventManager, interaction, removeSelf, openSettings }) => {
+ construct: ({ chart, series, container, eventManager, removeSelf, openSettings }) => {
return new VolumeProfile(chart, series, {
profileKind: 'visibleRange',
container,
- interaction,
formatObservable: eventManager.getChartOptionsModel(),
removeSelf,
openSettings,
@@ -210,10 +185,9 @@ export const drawingsMap: Record<DrawingsNames, DrawingConfig> = {
},
},
[DrawingsNames.rectangle]: {
- construct: ({ chart, series, eventManager, container, interaction, removeSelf, openSettings }) => {
+ construct: ({ chart, series, eventManager, container, removeSelf, openSettings }) => {
return new Rectangle(chart, series, {
container,
- interaction,
formatObservable: eventManager.getChartOptionsModel(),
removeSelf,
openSettings,
@@ -222,33 +196,30 @@ export const drawingsMap: Record<DrawingsNames, DrawingConfig> = {
},
[DrawingsNames.ruler]: {
singleInstance: true,
- construct: ({ chart, series, eventManager, container, interaction, removeSelf }) => {
+ construct: ({ chart, series, eventManager, container, removeSelf }) => {
return new Ruler(chart, series, {
formatObservable: eventManager.getChartOptionsModel(),
container,
- interaction,
resetTriggers: [eventManager.getTimeframeObs(), eventManager.getInterval()],
removeSelf,
});
},
},
[DrawingsNames.traectory]: {
- construct: ({ chart, series, eventManager, container, interaction, removeSelf, openSettings }) => {
+ construct: ({ chart, series, eventManager, container, removeSelf, openSettings }) => {
return new Traectory(chart, series, {
formatObservable: eventManager.getChartOptionsModel(),
container,
- interaction,
removeSelf,
openSettings,
});
},
},
[DrawingsNames.text]: {
- construct: ({ chart, series, eventManager, container, interaction, removeSelf, openSettings }) => {
+ construct: ({ chart, series, eventManager, container, removeSelf, openSettings }) => {
return new Text(chart, series, {
formatObservable: eventManager.getChartOptionsModel(),
container,
- interaction,
removeSelf,
openSettings,
});
diff --git a/src/core/Chart.ts b/src/core/Chart.ts
index dbf4238..28502b1 100644
--- a/src/core/Chart.ts
+++ b/src/core/Chart.ts
@@ -60,7 +60,7 @@ export enum Resize {
Expand,
}
-const HISTORY_LOAD_THRESHOLD = 50;
+const HISTORY_LOAD_THRESHOLD = 500;
interface ChartParams {
params: {
@@ -553,6 +553,8 @@ function getOptions(config: ChartConfig): DeepPartial<ChartOptions> {
borderVisible: false,
allowBoldLabels: false,
rightOffset: 25,
+ shiftVisibleRangeOnNewBar: true,
+ allowShiftVisibleRangeOnWhitespaceReplacement: true,
},
rightPriceScale: {
textColor: colors.chartTextPrimary,
diff --git a/src/core/ContainerManager.ts b/src/core/ContainerManager.ts
index b0be74e..de55a47 100644
--- a/src/core/ContainerManager.ts
+++ b/src/core/ContainerManager.ts
@@ -1,9 +1,4 @@
-import {
- CHART_DRAWING_TOOLBAR_CONTAINER,
- CHART_MODAL_CONTAINER_CLASSNAME,
- CHART_PANE_OVERLAY_CONTAINER,
- CHART_ROOT_CLASSNAME,
-} from '@src/constants';
+import { CHART_ROOT_CLASSNAME } from '@src/constants';
import styles from './styles.module.scss';
@@ -16,7 +11,6 @@ interface CreateContainersOptions {
enum ZIndex {
Chart = '0',
Base = '10',
- Floating = '20',
Modal = '1000',
}
@@ -33,10 +27,7 @@ const ContainerLayoutConfig = {
export class ContainerManager {
private static injectFont(): void {
const existingLink = document.querySelector('link[href*="fonts.googleapis.com"]');
-
- if (existingLink) {
- return;
- }
+ if (existingLink) return;
const link = document.createElement('link');
link.rel = 'stylesheet';
@@ -59,6 +50,7 @@ export class ContainerManager {
parentContainer.style.padding = 'var(--space-1000)';
parentContainer.style.backgroundColor = 'var(--neutral-0)';
parentContainer.style.borderRadius = 'var(--space-0500)';
+
parentContainer.style.display = 'grid';
parentContainer.style.rowGap = `${verticalGap}px`;
parentContainer.style.gridTemplateRows = showBottomPanel
@@ -104,22 +96,14 @@ export class ContainerManager {
controlBarContainer.style.bottom = 'var(--space-2000)';
controlBarContainer.style.zIndex = ZIndex.Base;
- const drawingToolbarContainer = document.createElement('div');
- drawingToolbarContainer.classList.add(CHART_DRAWING_TOOLBAR_CONTAINER);
- drawingToolbarContainer.style.position = 'absolute';
- drawingToolbarContainer.style.inset = '0';
- drawingToolbarContainer.style.zIndex = ZIndex.Floating;
- drawingToolbarContainer.style.pointerEvents = 'none';
- drawingToolbarContainer.style.overflow = 'hidden';
-
const modalContainer = document.createElement('div');
- modalContainer.classList.add(CHART_MODAL_CONTAINER_CLASSNAME);
+ modalContainer.className = 'moex-chart-modal-container';
modalContainer.style.position = 'absolute';
modalContainer.style.inset = '0';
modalContainer.style.zIndex = ZIndex.Modal;
modalContainer.hidden = true;
- chartAreaContainer.append(controlBarContainer, drawingToolbarContainer);
+ chartAreaContainer.append(controlBarContainer);
chartContainer.append(chartAreaContainer, modalContainer);
parentContainer.append(headerContainer, chartContainer);
@@ -158,7 +142,7 @@ export class ContainerManager {
modalContainer,
controlBarContainer,
- drawingToolbarContainer,
+
toggleToolbar,
};
}
@@ -180,7 +164,7 @@ export class ContainerManager {
legendContainer.style.pointerEvents = 'none';
const paneOverlayContainer = document.createElement('div');
- paneOverlayContainer.classList.add(CHART_PANE_OVERLAY_CONTAINER);
+ paneOverlayContainer.className = 'moex-chart-pane-overlay-container';
paneOverlayContainer.style.position = 'absolute';
paneOverlayContainer.style.inset = '0';
paneOverlayContainer.style.zIndex = ZIndex.Base;
diff --git a/src/core/Drawings.ts b/src/core/Drawings.ts
index ed9f8da..dbc6e6c 100644
--- a/src/core/Drawings.ts
+++ b/src/core/Drawings.ts
@@ -1,15 +1,12 @@
import { IChartApi, ISeriesApi, SeriesType } from 'lightweight-charts';
-import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { DOMObject, DOMObjectParams } from '@core/DOMObject';
+import { ISeriesDrawing } from '@core/Drawings/common';
import { DrawingSnapshotItem } from '@core/DrawingsManager';
import { Hotkeys, Keys } from '@core/Hotkeys';
-
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 '@core/Drawings/common';
+import { SettingsTab, SettingsValues } from '@src/types/settings';
type IDrawing = DOMObject;
@@ -18,12 +15,7 @@ interface DrawingParams extends DOMObjectParams {
lwcChart: IChartApi;
mainSeries: SeriesStrategies;
onDelete: (id: string) => void;
- construct: (chart: IChartApi, series: ISeriesApi<SeriesType>, interaction: DrawingInteraction) => ISeriesDrawing;
- selected$: Observable<boolean>;
- isSelected: () => boolean;
- select: () => void;
- deselect: () => void;
- isLocked?: boolean;
+ construct: (chart: IChartApi, series: ISeriesApi<SeriesType>) => ISeriesDrawing;
hotkeys: Hotkeys;
setCopyPasteBuffer: (copiedObject: DrawingSnapshotItem) => void;
resetActiveTool: () => void;
@@ -34,8 +26,6 @@ export class Drawing extends DOMObject implements IDrawing {
private mainSeries: SeriesStrategies;
private drawingName: DrawingsNames;
private hotkeys: Hotkeys;
- private lockedSubject: BehaviorSubject<boolean>;
- private subscriptions = new Subscription();
private escapeUnregisterHash: string | null = null;
private deleteUnregisterHash: string | null = null;
@@ -52,35 +42,15 @@ export class Drawing extends DOMObject implements IDrawing {
moveUp,
moveDown,
construct,
- selected$,
- isSelected,
- select,
- deselect,
- isLocked = false,
paneId,
hotkeys,
setCopyPasteBuffer,
resetActiveTool,
}: DrawingParams) {
super({ id, name, zIndex, onDelete, moveUp, moveDown, paneId });
-
this.hotkeys = hotkeys;
- this.mainSeries = mainSeries;
- this.drawingName = drawingName;
- this.lockedSubject = new BehaviorSubject(isLocked);
-
- const interaction: DrawingInteraction = {
- selected$,
- locked$: this.lockedSubject.asObservable(),
- isSelected,
- isLocked: () => this.lockedSubject.value,
- select,
- deselect,
- };
-
- this.lwcDrawing = construct(lwcChart, mainSeries, interaction);
+ this.lwcDrawing = construct(lwcChart, mainSeries);
this.onDelete = onDelete;
-
this.escapeUnregisterHash = hotkeys.register({
keys: [Keys.escape],
callback: () => {
@@ -89,59 +59,50 @@ export class Drawing extends DOMObject implements IDrawing {
},
});
- this.subscriptions.add(
- selected$.subscribe((isSelectedDrawing) => {
- if (isSelectedDrawing) {
- this.deleteUnregisterHash = hotkeys.register({
- keys: [Keys.delete],
- callback: () => {
- this.delete();
- },
- });
-
- this.copyUnregisterHash = hotkeys.register({
- keys: [Keys.control, Keys.c],
- callback: () => {
- if (!this.isCreationPending()) {
- setCopyPasteBuffer({
- id: this.id,
- drawingName: this.getDrawingName(),
- state: this.getState(),
- isLocked: this.isLocked(),
- });
- }
- },
- });
-
- return;
- }
-
+ this.lwcDrawing.subscribeIsSelected((isSelected) => {
+ if (isSelected) {
+ this.deleteUnregisterHash = hotkeys.register({
+ keys: [Keys.delete],
+ callback: () => {
+ this.delete();
+ },
+ });
+ this.copyUnregisterHash = hotkeys.register({
+ keys: [Keys.control, Keys.c],
+ callback: () => {
+ if (!this.isCreationPending()) {
+ const copiedObject = {
+ id: this.id,
+ drawingName: this.getDrawingName(),
+ state: this.getState(),
+ };
+ setCopyPasteBuffer(copiedObject);
+ }
+ },
+ });
+ } else {
hotkeys.unregister({
keys: [Keys.delete],
hash: this.deleteUnregisterHash,
});
-
hotkeys.unregister({
keys: [Keys.control, Keys.c],
hash: this.copyUnregisterHash,
});
-
- this.deleteUnregisterHash = null;
- this.copyUnregisterHash = null;
- }),
- );
+ }
+ });
+ this.mainSeries = mainSeries;
+ this.drawingName = drawingName;
this.afterCreation(() => {
hotkeys.unregister({
keys: [Keys.escape],
hash: this.escapeUnregisterHash,
});
-
- this.escapeUnregisterHash = null;
});
}
- public delete(): void {
+ public delete() {
this.destroy();
super.delete();
}
@@ -150,21 +111,21 @@ export class Drawing extends DOMObject implements IDrawing {
return this.drawingName;
}
- public getLwcDrawing(): ISeriesDrawing {
+ public getLwcDrawing() {
return this.lwcDrawing;
}
- public show(): void {
+ public show() {
this.lwcDrawing.show();
super.show();
}
- public hide(): void {
+ public hide() {
this.lwcDrawing.hide();
super.hide();
}
- public rebind = (nextMainSeries: SeriesStrategies): void => {
+ public rebind = (nextMainSeries: SeriesStrategies) => {
this.lwcDrawing.rebind(nextMainSeries);
this.mainSeries = nextMainSeries;
};
@@ -173,22 +134,6 @@ export class Drawing extends DOMObject implements IDrawing {
return this.lwcDrawing.isCreationPending();
}
- public subscribeIsLocked(callback: (isLocked: boolean) => void): Subscription {
- return this.lockedSubject.subscribe(callback);
- }
-
- public subscribeSettings(callback: (settings: SettingsValues) => void): Subscription {
- return this.lwcDrawing.subscribeSettings(callback);
- }
-
- public isLocked(): boolean {
- return this.lockedSubject.value;
- }
-
- public toggleLock(): void {
- this.lockedSubject.next(!this.lockedSubject.value);
- }
-
public async waitForCreation(): Promise<void> {
return this.lwcDrawing.waitTillReady();
}
@@ -217,41 +162,29 @@ export class Drawing extends DOMObject implements IDrawing {
return this.lwcDrawing.getSettingsTabs();
}
- public getToolbarSettings(): ToolbarSettingField[] {
- return this.getSettingsTabs()
- .flatMap((tab) => tab.fields)
- .filter((field): field is ToolbarSettingField => field.toolbar !== undefined);
- }
-
public hasSettings(): boolean {
return this.getSettingsTabs().some((tab) => tab.fields.length > 0);
}
- public destroy(): void {
- this.subscriptions.unsubscribe();
-
+ public destroy() {
this.hotkeys.unregister({
keys: [Keys.delete],
hash: this.deleteUnregisterHash,
});
-
this.hotkeys.unregister({
keys: [Keys.escape],
hash: this.escapeUnregisterHash,
});
-
this.hotkeys.unregister({
keys: [Keys.control, Keys.c],
hash: this.copyUnregisterHash,
});
-
- this.lockedSubject.complete();
this.mainSeries.detachPrimitive(this.lwcDrawing);
this.lwcDrawing.destroy();
}
- private async afterCreation(callback: () => void): Promise<void> {
+ private async afterCreation(cb: () => void) {
await this.lwcDrawing.waitTillReady();
- callback();
+ cb();
}
}
diff --git a/src/core/Drawings/axisLine/axisLine.ts b/src/core/Drawings/axisLine/axisLine.ts
index c2f2096..7d789c2 100644
--- a/src/core/Drawings/axisLine/axisLine.ts
+++ b/src/core/Drawings/axisLine/axisLine.ts
@@ -28,7 +28,7 @@ import {
getAxisLineSettingsTabs,
} from './settings';
-import type { DrawingInteraction, ISeriesDrawing } from '@core/Drawings/common';
+import type { ISeriesDrawing } from '@core/Drawings/common';
import type { AxisLabel, Point, SeriesApi } from '@core/Drawings/types';
import type { ChartOptionsModel, SettingsTab } from '@src/types';
@@ -39,7 +39,6 @@ type AxisLineMode = 'idle' | 'ready' | 'dragging';
interface AxisLineParams {
direction: AxisLineDirection;
container: HTMLElement;
- interaction: DrawingInteraction;
formatObservable?: Observable<ChartOptionsModel>;
removeSelf?: () => void;
openSettings?: () => void;
@@ -47,6 +46,7 @@ interface AxisLineParams {
interface AxisLineState {
hidden: boolean;
+ isActive: boolean;
mode: AxisLineMode;
time: Time | null;
price: number | null;
@@ -89,10 +89,9 @@ export class AxisLine extends SeriesDrawingBase<AxisLineSettings> implements ISe
constructor(
chart: IChartApi,
series: SeriesApi,
- { direction, container, interaction, formatObservable, removeSelf, openSettings }: AxisLineParams,
+ { direction, container, formatObservable, removeSelf, openSettings }: AxisLineParams,
) {
- super({ chart, series, container, interaction });
-
+ super({ chart, series, container });
this.direction = direction;
this.removeSelf = removeSelf;
this.openSettings = openSettings;
@@ -128,6 +127,7 @@ export class AxisLine extends SeriesDrawingBase<AxisLineSettings> implements ISe
public getState(): AxisLineState {
return {
hidden: this.hidden,
+ isActive: this.isActive.value,
mode: this.mode,
time: this.time,
price: this.price,
@@ -150,6 +150,10 @@ export class AxisLine extends SeriesDrawingBase<AxisLineSettings> implements ISe
this.hidden = nextState.hidden;
}
+ if ('isActive' in nextState && typeof nextState.isActive === 'boolean') {
+ this.isActive.next(nextState.isActive);
+ }
+
if ('mode' in nextState && nextState.mode) {
this.mode = nextState.mode;
}
@@ -209,12 +213,12 @@ export class AxisLine extends SeriesDrawingBase<AxisLineSettings> implements ISe
direction: this.direction,
coordinate,
handle: this.direction === 'vertical' ? { x: coordinate, y: height / 2 } : { x: width / 2, y: coordinate },
- showHandle: this.shouldShowHandles(),
+ showHandle: this.isActive.value,
...this.settings,
};
}
- protected getHoveredItem(x: number, y: number): PrimitiveHoveredItem | null {
+ public hitTest(x: number, y: number): PrimitiveHoveredItem | null {
if (this.hidden || this.mode === 'idle') {
return null;
}
@@ -226,7 +230,7 @@ export class AxisLine extends SeriesDrawingBase<AxisLineSettings> implements ISe
return null;
}
- if (this.isSelected() && isNearPoint(point, data.handle.x, data.handle.y, HANDLE_HIT_TOLERANCE)) {
+ if (this.isActive.value && isNearPoint(point, data.handle.x, data.handle.y, HANDLE_HIT_TOLERANCE)) {
return {
cursorStyle: this.getCursorStyle(),
externalId: 'axis-line',
@@ -246,7 +250,7 @@ export class AxisLine extends SeriesDrawingBase<AxisLineSettings> implements ISe
}
protected getTimeAxisLabel(kind: string): AxisLabel | null {
- if (kind !== 'main' || this.direction !== 'vertical' || !this.isSelected() || this.time === null) {
+ if (kind !== 'main' || this.direction !== 'vertical' || !this.isActive.value || this.time === null) {
return null;
}
@@ -272,7 +276,7 @@ export class AxisLine extends SeriesDrawingBase<AxisLineSettings> implements ISe
}
protected getPriceAxisLabel(kind: string): AxisLabel | null {
- if (kind !== 'main' || this.direction !== 'horizontal' || !this.isSelected() || this.price === null) {
+ if (kind !== 'main' || this.direction !== 'horizontal' || !this.isActive.value || this.price === null) {
return null;
}
@@ -321,7 +325,7 @@ export class AxisLine extends SeriesDrawingBase<AxisLineSettings> implements ISe
y: event.clientY - rect.top,
};
- const isNearHandle = this.isSelected() && isNearPoint(point, data.handle.x, data.handle.y, HANDLE_HIT_TOLERANCE);
+ const isNearHandle = this.isActive.value && isNearPoint(point, data.handle.x, data.handle.y, HANDLE_HIT_TOLERANCE);
const isNearLine = this.isPointNearLine(point, data.coordinate);
if (!isNearHandle && !isNearLine) {
@@ -346,6 +350,7 @@ export class AxisLine extends SeriesDrawingBase<AxisLineSettings> implements ISe
event.stopPropagation();
this.updateLine(point);
+ this.isActive.next(true);
this.mode = 'ready';
this.resolveReady?.();
@@ -363,10 +368,10 @@ export class AxisLine extends SeriesDrawingBase<AxisLineSettings> implements ISe
return;
}
- const isNearHandle = this.isSelected() && isNearPoint(point, data.handle.x, data.handle.y, HANDLE_HIT_TOLERANCE);
+ const isNearHandle = this.isActive.value && isNearPoint(point, data.handle.x, data.handle.y, HANDLE_HIT_TOLERANCE);
const isNearLine = this.isPointNearLine(point, data.coordinate);
- if (!this.isSelected()) {
+ if (!this.isActive.value) {
if (!isNearLine) {
return;
}
@@ -374,12 +379,14 @@ export class AxisLine extends SeriesDrawingBase<AxisLineSettings> implements ISe
event.preventDefault();
event.stopPropagation();
- this.select();
+ this.isActive.next(true);
+ this.render();
return;
}
if (!isNearHandle && !isNearLine) {
- this.deselect();
+ this.isActive.next(false);
+ this.render();
return;
}
@@ -433,7 +440,6 @@ export class AxisLine extends SeriesDrawingBase<AxisLineSettings> implements ISe
}
const coordinate = getXCoordinateFromTime(this.chart, this.time, this.series);
-
return coordinate === null ? null : Number(coordinate);
}
@@ -442,7 +448,6 @@ export class AxisLine extends SeriesDrawingBase<AxisLineSettings> implements ISe
}
const coordinate = getYCoordinateFromPrice(this.series, this.price);
-
return coordinate === null ? null : Number(coordinate);
}
diff --git a/src/core/Drawings/axisLine/settings.ts b/src/core/Drawings/axisLine/settings.ts
index 19c5eae..725f7ca 100644
--- a/src/core/Drawings/axisLine/settings.ts
+++ b/src/core/Drawings/axisLine/settings.ts
@@ -33,13 +33,9 @@ export function getAxisLineSettingsTabs(settings: AxisLineSettings): SettingsTab
const styleFields: SettingField[] = [
{
key: 'lineColor',
- label: t('Line'),
+ label: t('Line color'),
type: 'color',
defaultValue: settings.lineColor,
- toolbar: {
- control: 'color',
- role: 'line',
- },
},
];
@@ -67,19 +63,15 @@ export function getAxisLineSettingsTabs(settings: AxisLineSettings): SettingsTab
},
{
key: 'isItalic',
- label: t('Italic'),
+ label: t('Italics'),
type: 'boolean',
defaultValue: settings.isItalic,
},
{
key: 'textColor',
- label: t('Text'),
+ label: t('Text color'),
type: 'color',
defaultValue: settings.textColor,
- toolbar: {
- control: 'color',
- role: 'text',
- },
},
];
diff --git a/src/core/Drawings/common.ts b/src/core/Drawings/common.ts
index be986da..17b684c 100644
--- a/src/core/Drawings/common.ts
+++ b/src/core/Drawings/common.ts
@@ -13,42 +13,32 @@ import {
SeriesType,
Time,
} from 'lightweight-charts';
-import { Observable, Subject, Subscription } from 'rxjs';
+
+import { BehaviorSubject, distinctUntilChanged, Observable, Subscription } from 'rxjs';
import { getPointerPoint as getPointerPointFromEvent } from '@core/Drawings/helpers';
import { AxisLabel, AxisSegment, Point, SeriesApi } from '@core/Drawings/types';
-import { SettingsTab, SettingsValues } from '@src/types';
-
-export interface DrawingInteraction {
- selected$: Observable<boolean>;
- locked$: Observable<boolean>;
-
- isSelected(): boolean;
- isLocked(): boolean;
-
- select(): void;
- deselect(): void;
-}
+import { ChartOptionsModel, SettingsTab, SettingsValues } from '@src/types';
export interface ISeriesDrawing extends ISeriesPrimitive<Time> {
show(): void;
hide(): void;
rebind(series: ISeriesApi<SeriesType>): void;
destroy(): void;
-
- waitTillReady(): Promise<void>;
isCreationPending(): boolean;
+ waitTillReady(): Promise<void>;
+
shouldShowInObjectTree(): boolean;
getState(): unknown;
setState(state: unknown): void;
getSettings(): SettingsValues;
- getSettingsTabs(): SettingsTab[];
updateSettings(settings: SettingsValues): void;
+ getSettingsTabs(): SettingsTab[];
- subscribeSettings(callback: (settings: SettingsValues) => void): Subscription;
+ subscribeIsSelected(cb: (isSelected: boolean) => void): void;
getRenderData(): unknown;
}
@@ -57,7 +47,6 @@ interface SeriesDrawingBaseParams {
container: HTMLElement;
chart: IChartApi;
series: SeriesApi;
- interaction: DrawingInteraction;
}
export abstract class SeriesDrawingBase<TSettings extends SettingsValues = SettingsValues> implements ISeriesDrawing {
@@ -68,27 +57,22 @@ export abstract class SeriesDrawingBase<TSettings extends SettingsValues = Setti
protected abstract mode: unknown; // todo: хочется иметь единый mode
protected abstract settings: TSettings;
protected readonly container: HTMLElement;
+ protected isActive: BehaviorSubject<boolean> = new BehaviorSubject(false);
protected isBound = false;
- private readonly interaction: DrawingInteraction;
- private readonly settingsSubject = new Subject<SettingsValues>();
- private isInteractionBound = false;
+ protected readyPromise: null | Promise<void> = null;
+ protected resolveReady: null | (() => void) = null;
- protected readyPromise: Promise<void> | null = null;
- protected resolveReady: (() => void) | null = null;
protected requestUpdate: (() => void) | null = null;
- constructor({ chart, series, container, interaction }: SeriesDrawingBaseParams) {
+ constructor({ chart, series, container }: SeriesDrawingBaseParams) {
this.chart = chart;
this.series = series;
this.container = container;
- this.interaction = interaction;
}
- public subscribeSettings(callback: (settings: SettingsValues) => void): Subscription {
- callback(this.getSettings());
-
- return this.settingsSubject.subscribe(callback);
+ public subscribeIsSelected(cb: (isSelected: boolean) => void) {
+ return this.isActive.pipe(distinctUntilChanged()).subscribe(cb);
}
public show(): void {
@@ -122,7 +106,6 @@ export abstract class SeriesDrawingBase<TSettings extends SettingsValues = Setti
this.showCrosshair();
this.unbindEvents();
this.subscriptions.unsubscribe();
- this.settingsSubject.complete();
this.series.detachPrimitive(this as unknown as ISeriesPrimitive<Time>);
this.requestUpdate = null;
this.resolveReady?.();
@@ -132,10 +115,9 @@ export abstract class SeriesDrawingBase<TSettings extends SettingsValues = Setti
if (this.mode === 'ready') {
return Promise.resolve();
}
-
if (!this.readyPromise) {
this.readyPromise = new Promise((resolve) => {
- this.resolveReady = resolve;
+ this.resolveReady = resolve as () => void;
});
}
@@ -156,13 +138,11 @@ export abstract class SeriesDrawingBase<TSettings extends SettingsValues = Setti
...settings,
};
- this.settingsSubject.next(this.getSettings());
this.render();
}
public attached(param: SeriesAttachedParameter<Time, keyof SeriesOptionsMap>): void {
this.requestUpdate = param.requestUpdate;
- this.bindInteraction();
this.bindEvents();
}
@@ -176,24 +156,13 @@ export abstract class SeriesDrawingBase<TSettings extends SettingsValues = Setti
return null;
}
- public hitTest(x: number, y: number): PrimitiveHoveredItem | null {
- const hoveredItem = this.getHoveredItem(x, y);
-
- if (!hoveredItem || !this.isLocked()) {
- return hoveredItem;
- }
-
- return {
- ...hoveredItem,
- cursorStyle: 'pointer',
- };
- }
-
public abstract getRenderData(): unknown; // todo: make proper type
public abstract getState(): unknown;
public abstract getSettingsTabs(): SettingsTab[];
public abstract isCreationPending(): boolean;
public abstract setState(state: unknown): void;
+
+ public abstract hitTest(x: number, y: number): PrimitiveHoveredItem | null;
public abstract updateAllViews(): void;
public abstract paneViews(): readonly IPrimitivePaneView[];
public abstract priceAxisPaneViews(): readonly IPrimitivePaneView[];
@@ -201,26 +170,6 @@ export abstract class SeriesDrawingBase<TSettings extends SettingsValues = Setti
public abstract timeAxisPaneViews(): readonly IPrimitivePaneView[];
public abstract timeAxisViews(): readonly ISeriesPrimitiveAxisView[];
- protected isSelected(): boolean {
- return this.interaction.isSelected();
- }
-
- protected isLocked(): boolean {
- return this.interaction.isLocked();
- }
-
- protected select(): void {
- this.interaction.select();
- }
-
- protected deselect(): void {
- this.interaction.deselect();
- }
-
- protected shouldShowHandles(): boolean {
- return !this.isLocked() && (this.isSelected() || this.isCreationPending());
- }
-
protected render(): void {
this.updateAllViews();
this.requestUpdate?.();
@@ -254,7 +203,7 @@ export abstract class SeriesDrawingBase<TSettings extends SettingsValues = Setti
this.isBound = true;
this.container.addEventListener('dblclick', this.handleDoubleClick);
- this.container.addEventListener('pointerdown', this.handlePointerDownEvent);
+ this.container.addEventListener('pointerdown', this.handlePointerDown);
this.container.addEventListener('contextmenu', this.handleContextMenu);
window.addEventListener('pointermove', this.handlePointerMove);
@@ -270,7 +219,7 @@ export abstract class SeriesDrawingBase<TSettings extends SettingsValues = Setti
this.isBound = false;
this.container.removeEventListener('dblclick', this.handleDoubleClick);
- this.container.removeEventListener('pointerdown', this.handlePointerDownEvent);
+ this.container.removeEventListener('pointerdown', this.handlePointerDown);
this.container.removeEventListener('contextmenu', this.handleContextMenu);
window.removeEventListener('pointermove', this.handlePointerMove);
@@ -285,55 +234,9 @@ export abstract class SeriesDrawingBase<TSettings extends SettingsValues = Setti
protected handlePointerMove(event: PointerEvent): void {}
protected handlePointerUp(event: PointerEvent): void {}
- protected abstract getHoveredItem(x: number, y: number): PrimitiveHoveredItem | null;
protected abstract getGeometry(): unknown; // todo: make proper type
protected abstract getTimeAxisSegments(): AxisSegment[];
protected abstract getPriceAxisSegments(): AxisSegment[];
protected abstract getTimeAxisLabel(kind: string): AxisLabel | null;
protected abstract getPriceAxisLabel(kind: string): AxisLabel | null;
-
- private bindInteraction(): void {
- if (this.isInteractionBound) {
- return;
- }
-
- this.isInteractionBound = true;
-
- this.subscriptions.add(
- this.interaction.selected$.subscribe(() => {
- this.render();
- }),
- );
-
- this.subscriptions.add(
- this.interaction.locked$.subscribe((isLocked) => {
- if (isLocked) {
- this.showCrosshair();
- }
-
- this.render();
- }),
- );
- }
-
- private handlePointerDownEvent = (event: PointerEvent): void => {
- if (!this.isLocked() || this.isCreationPending() || event.button !== 0) {
- this.handlePointerDown(event);
-
- return;
- }
-
- const point = this.getEventPoint(event);
- const isDrawingHit = this.getHoveredItem(point.x, point.y) !== null;
-
- if (isDrawingHit) {
- this.select();
-
- return;
- }
-
- if (this.isSelected()) {
- this.deselect();
- }
- };
}
diff --git a/src/core/Drawings/diapson/diapson.ts b/src/core/Drawings/diapson/diapson.ts
index b241e8d..023a81e 100644
--- a/src/core/Drawings/diapson/diapson.ts
+++ b/src/core/Drawings/diapson/diapson.ts
@@ -1,4 +1,3 @@
-import { clamp } from 'lodash-es';
import { Observable } from 'rxjs';
import {
@@ -9,6 +8,7 @@ import {
} from '@core/Drawings/axis';
import { SeriesDrawingBase } from '@core/Drawings/common';
import {
+ clamp,
clampPointToContainer as clampPointToContainerInElement,
getAnchorFromPoint,
getContainerSize as getElementContainerSize,
@@ -35,7 +35,7 @@ import {
getDiapsonSettingsTabs,
} from './settings';
-import type { DrawingInteraction, ISeriesDrawing } from '@core/Drawings/common';
+import type { ISeriesDrawing } from '@core/Drawings/common';
import type { Anchor, AxisLabel, AxisSegment, Point, SeriesApi } from '@core/Drawings/types';
import type { ChartOptionsModel, SettingsTab } from '@src/types';
import type { IChartApi, IPrimitivePaneView, PrimitiveHoveredItem, Time, UTCTimestamp } from 'lightweight-charts';
@@ -49,7 +49,6 @@ type PriceLabelKind = 'top' | 'bottom';
interface DiapsonParams {
container: HTMLElement;
- interaction: DrawingInteraction;
rangeMode: DiapsonRangeMode;
formatObservable?: Observable<ChartOptionsModel>;
removeSelf?: () => void;
@@ -60,6 +59,7 @@ interface DiapsonParams {
export interface DiapsonState {
hidden: boolean;
+ isActive: boolean;
mode: DiapsonMode;
rangeMode: DiapsonRangeMode;
startTime: Time | null;
@@ -141,12 +141,7 @@ export class Diapson extends SeriesDrawingBase<DiapsonSettings> implements ISeri
private readonly bottomPriceAxisView: CustomPriceAxisView;
constructor(chart: IChartApi, series: SeriesApi, params: DiapsonParams) {
- super({
- chart,
- series,
- container: params.container,
- interaction: params.interaction,
- });
+ super({ chart, series, container: params.container });
const { rangeMode, formatObservable, removeSelf, openSettings, stepSize = 1, stepLabel = '' } = params;
@@ -214,6 +209,7 @@ export class Diapson extends SeriesDrawingBase<DiapsonSettings> implements ISeri
public getState(): DiapsonState {
return {
hidden: this.hidden,
+ isActive: this.isActive.value,
mode: this.mode,
rangeMode: this.rangeMode,
startTime: this.startTime,
@@ -232,6 +228,9 @@ export class Diapson extends SeriesDrawingBase<DiapsonSettings> implements ISeri
const nextState = state as Partial<DiapsonState>;
this.hidden = typeof nextState.hidden === 'boolean' ? nextState.hidden : this.hidden;
+ if (typeof nextState.isActive === 'boolean') {
+ this.isActive.next(nextState.isActive);
+ }
this.mode = nextState.mode ?? this.mode;
this.rangeMode = nextState.rangeMode ?? this.rangeMode;
this.startTime = 'startTime' in nextState ? (nextState.startTime ?? null) : this.startTime;
@@ -300,14 +299,14 @@ export class Diapson extends SeriesDrawingBase<DiapsonSettings> implements ISeri
...geometry,
rangeMode: this.rangeMode,
showFill: true,
- showHandles: this.shouldShowHandles(),
+ showHandles: this.isActive.value,
labelLines: this.getLabelLines(),
...this.settings,
};
}
protected getTimeAxisSegments(): AxisSegment[] {
- if (!this.isSelected() && !this.isCreationPending()) {
+ if (!this.isActive.value) {
return [];
}
@@ -329,7 +328,7 @@ export class Diapson extends SeriesDrawingBase<DiapsonSettings> implements ISeri
}
protected getPriceAxisSegments(): AxisSegment[] {
- if (!this.isSelected() && !this.isCreationPending()) {
+ if (!this.isActive.value) {
return [];
}
@@ -351,7 +350,7 @@ export class Diapson extends SeriesDrawingBase<DiapsonSettings> implements ISeri
}
protected getTimeAxisLabel(kind: string): AxisLabel | null {
- if ((!this.isSelected() && !this.isCreationPending()) || (kind !== 'left' && kind !== 'right')) {
+ if (!this.isActive.value || (kind !== 'left' && kind !== 'right')) {
return null;
}
@@ -374,7 +373,7 @@ export class Diapson extends SeriesDrawingBase<DiapsonSettings> implements ISeri
}
protected getPriceAxisLabel(kind: string): AxisLabel | null {
- if ((!this.isSelected() && !this.isCreationPending()) || (kind !== 'top' && kind !== 'bottom')) {
+ if (!this.isActive.value || (kind !== 'top' && kind !== 'bottom')) {
return null;
}
@@ -396,14 +395,14 @@ export class Diapson extends SeriesDrawingBase<DiapsonSettings> implements ISeri
};
}
- protected getHoveredItem(x: number, y: number): PrimitiveHoveredItem | null {
+ public hitTest(x: number, y: number): PrimitiveHoveredItem | null {
if (this.hidden || this.mode === 'idle' || this.mode === 'drawing') {
return null;
}
const point = { x, y };
- if (!this.isSelected()) {
+ if (!this.isActive.value) {
if (!this.containsPoint(point)) {
return null;
}
@@ -481,7 +480,7 @@ export class Diapson extends SeriesDrawingBase<DiapsonSettings> implements ISeri
return;
}
- if (!this.isSelected()) {
+ if (!this.isActive.value) {
if (!this.containsPoint(point)) {
return;
}
@@ -489,14 +488,16 @@ export class Diapson extends SeriesDrawingBase<DiapsonSettings> implements ISeri
event.preventDefault();
event.stopPropagation();
- this.select();
+ this.isActive.next(true);
+ this.render();
return;
}
const dragTarget = this.getDragTarget(point);
if (!dragTarget) {
- this.deselect();
+ this.isActive.next(false);
+ this.render();
return;
}
@@ -550,6 +551,7 @@ export class Diapson extends SeriesDrawingBase<DiapsonSettings> implements ISeri
this.startPrice = anchor.price;
this.endPrice = anchor.price;
+ this.isActive.next(true);
this.mode = 'drawing';
this.render();
}
@@ -618,7 +620,7 @@ export class Diapson extends SeriesDrawingBase<DiapsonSettings> implements ISeri
private resetToIdle(): void {
this.hidden = false;
- this.deselect();
+ this.isActive.next(false);
this.mode = 'idle';
this.startTime = null;
this.endTime = null;
diff --git a/src/core/Drawings/diapson/settings.ts b/src/core/Drawings/diapson/settings.ts
index ede33c0..92a9644 100644
--- a/src/core/Drawings/diapson/settings.ts
+++ b/src/core/Drawings/diapson/settings.ts
@@ -35,23 +35,15 @@ export function getDiapsonSettingsTabs(settings: DiapsonSettings): SettingsTab[]
const styleFields: SettingField[] = [
{
key: 'borderColor',
- label: t('Border'),
+ label: t('Border color'),
type: 'color',
defaultValue: settings.borderColor,
- toolbar: {
- control: 'color',
- role: 'line',
- },
},
{
key: 'fillColor',
- label: t('Background'),
+ label: t('Area color'),
type: 'color',
defaultValue: settings.fillColor,
- toolbar: {
- control: 'color',
- role: 'fill',
- },
},
];
@@ -72,23 +64,19 @@ export function getDiapsonSettingsTabs(settings: DiapsonSettings): SettingsTab[]
},
{
key: 'isItalic',
- label: t('Italic'),
+ label: t('Italics'),
type: 'boolean',
defaultValue: settings.isItalic,
},
{
key: 'labelTextColor',
- label: t('Text'),
+ label: t('Text color'),
type: 'color',
defaultValue: settings.labelTextColor,
- toolbar: {
- control: 'color',
- role: 'text',
- },
},
{
key: 'labelBackgroundColor',
- label: t('Background'),
+ label: t('Background color'),
type: 'color',
defaultValue: settings.labelBackgroundColor,
},
diff --git a/src/core/Drawings/fibonacciRetracement/fibonacciRetracement.ts b/src/core/Drawings/fibonacciRetracement/fibonacciRetracement.ts
index 44ab2bc..023f081 100644
--- a/src/core/Drawings/fibonacciRetracement/fibonacciRetracement.ts
+++ b/src/core/Drawings/fibonacciRetracement/fibonacciRetracement.ts
@@ -1,4 +1,3 @@
-import { clamp } from 'lodash-es';
import { Observable } from 'rxjs';
import {
@@ -9,6 +8,7 @@ import {
} from '@core/Drawings/axis';
import { SeriesDrawingBase } from '@core/Drawings/common';
import {
+ clamp,
clampPointToContainer as clampPointToContainerInElement,
getAnchorFromPoint,
getContainerSize as getElementContainerSize,
@@ -38,7 +38,7 @@ import {
mergeFibonacciRetracementSettings,
} from './settings';
-import type { DrawingInteraction, ISeriesDrawing } from '@core/Drawings/common';
+import type { ISeriesDrawing } from '@core/Drawings/common';
import type { AxisLabel, AxisSegment, Bounds, Point, SeriesApi } from '@core/Drawings/types';
import type { ChartOptionsModel, SettingsTab, SettingsValues } from '@src/types';
import type { IChartApi, IPrimitivePaneView, PrimitiveHoveredItem, Time, UTCTimestamp } from 'lightweight-charts';
@@ -51,7 +51,6 @@ type PriceLabelKind = 'top' | 'bottom';
interface FibonacciRetracementParams {
container: HTMLElement;
- interaction: DrawingInteraction;
formatObservable?: Observable<ChartOptionsModel>;
removeSelf?: () => void;
openSettings?: () => void;
@@ -59,6 +58,7 @@ interface FibonacciRetracementParams {
interface FibonacciRetracementState {
hidden: boolean;
+ isActive: boolean;
mode: FibonacciRetracementMode;
startTime: Time | null;
endTime: Time | null;
@@ -147,10 +147,9 @@ export class FibonacciRetracement extends SeriesDrawingBase<FibonacciRetracement
constructor(
chart: IChartApi,
series: SeriesApi,
- { container, interaction, formatObservable, removeSelf, openSettings }: FibonacciRetracementParams,
+ { container, formatObservable, removeSelf, openSettings }: FibonacciRetracementParams,
) {
- super({ chart, series, container, interaction });
-
+ super({ chart, series, container });
this.removeSelf = removeSelf;
this.openSettings = openSettings;
@@ -203,6 +202,7 @@ export class FibonacciRetracement extends SeriesDrawingBase<FibonacciRetracement
public getState(): FibonacciRetracementState {
return {
hidden: this.hidden,
+ isActive: this.isActive.value,
mode: this.mode,
startTime: this.startTime,
endTime: this.endTime,
@@ -213,13 +213,12 @@ export class FibonacciRetracement extends SeriesDrawingBase<FibonacciRetracement
}
public setState(state: unknown): void {
- if (!state || typeof state !== 'object') {
- return;
- }
-
const next = state as Partial<FibonacciRetracementState>;
this.hidden = next.hidden ?? this.hidden;
+ if (next.isActive !== undefined) {
+ this.isActive.next(next.isActive);
+ }
this.mode = next.mode ?? this.mode;
this.startTime = next.startTime ?? this.startTime;
@@ -288,7 +287,7 @@ export class FibonacciRetracement extends SeriesDrawingBase<FibonacciRetracement
return {
...geometry,
- showHandles: this.shouldShowHandles(),
+ showHandles: this.isActive.value,
showBackground: this.settings.showBackground,
backgroundOpacity: this.settings.backgroundOpacity / PERCENT_DIVIDER,
@@ -303,18 +302,18 @@ export class FibonacciRetracement extends SeriesDrawingBase<FibonacciRetracement
};
}
- protected getHoveredItem(x: number, y: number): PrimitiveHoveredItem | null {
+ public hitTest(x: number, y: number): PrimitiveHoveredItem | null {
if (this.hidden || this.mode === 'idle' || this.mode === 'drawing') {
return null;
}
const point = { x, y };
- if (!this.isSelected() && !this.containsPoint(point)) {
+ if (!this.isActive.value && !this.containsPoint(point)) {
return null;
}
- const handleTarget = this.isSelected() ? this.getHandleTarget(point) : null;
+ const handleTarget = this.isActive.value ? this.getHandleTarget(point) : null;
if (handleTarget) {
return {
@@ -329,14 +328,14 @@ export class FibonacciRetracement extends SeriesDrawingBase<FibonacciRetracement
}
return {
- cursorStyle: this.isSelected() ? 'grab' : 'pointer',
+ cursorStyle: this.isActive.value ? 'grab' : 'pointer',
externalId: 'fibonacci-retracement-position',
zOrder: 'top',
};
}
protected getTimeAxisSegments(): AxisSegment[] {
- const bounds = this.isSelected() || this.isCreationPending() ? this.getTimeBounds() : null;
+ const bounds = this.isActive.value ? this.getTimeBounds() : null;
if (!bounds) {
return [];
@@ -354,7 +353,7 @@ export class FibonacciRetracement extends SeriesDrawingBase<FibonacciRetracement
}
protected getPriceAxisSegments(): AxisSegment[] {
- const bounds = this.isSelected() || this.isCreationPending() ? this.getPriceBounds() : null;
+ const bounds = this.isActive.value ? this.getPriceBounds() : null;
if (!bounds) {
return [];
@@ -372,12 +371,8 @@ export class FibonacciRetracement extends SeriesDrawingBase<FibonacciRetracement
}
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);
+ const coordinate = this.isActive.value ? this.getTimeCoordinate(kind as TimeLabelKind) : null;
+ const text = this.getTimeText(kind as TimeLabelKind);
if (coordinate === null || !text) {
return null;
@@ -394,12 +389,8 @@ export class FibonacciRetracement extends SeriesDrawingBase<FibonacciRetracement
}
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);
+ const coordinate = this.isActive.value ? this.getPriceCoordinate(kind as PriceLabelKind) : null;
+ const text = this.getPriceText(kind as PriceLabelKind);
if (coordinate === null || !text) {
return null;
@@ -456,22 +447,17 @@ export class FibonacciRetracement extends SeriesDrawingBase<FibonacciRetracement
return;
}
- if (!this.isSelected()) {
- if (!this.containsPoint(point)) {
- return;
- }
-
- event.preventDefault();
- event.stopPropagation();
-
- this.select();
+ if (!this.isActive.value) {
+ this.isActive.next(this.containsPoint(point));
+ this.render();
return;
}
const dragTarget = this.getDragTarget(point);
if (!dragTarget) {
- this.deselect();
+ this.isActive.next(false);
+ this.render();
return;
}
@@ -527,6 +513,8 @@ export class FibonacciRetracement extends SeriesDrawingBase<FibonacciRetracement
this.endTime = anchor.time;
this.startPrice = anchor.price;
this.endPrice = anchor.price;
+
+ this.isActive.next(true);
this.mode = 'drawing';
this.render();
@@ -590,6 +578,7 @@ export class FibonacciRetracement extends SeriesDrawingBase<FibonacciRetracement
private resetToIdle(): void {
this.hidden = false;
+ this.isActive.next(false);
this.mode = 'idle';
this.startTime = null;
diff --git a/src/core/Drawings/fibonacciRetracement/settings.ts b/src/core/Drawings/fibonacciRetracement/settings.ts
index 91dfd04..f6d7cab 100644
--- a/src/core/Drawings/fibonacciRetracement/settings.ts
+++ b/src/core/Drawings/fibonacciRetracement/settings.ts
@@ -197,7 +197,7 @@ export function getFibonacciRetracementSettingsTabs(settings: FibonacciRetraceme
},
{
key: 'isItalic',
- label: t('Italic'),
+ label: t('Italics'),
type: 'boolean',
defaultValue: settings.isItalic,
},
@@ -212,7 +212,7 @@ export function getFibonacciRetracementSettingsTabs(settings: FibonacciRetraceme
},
{
key: getLevelColorFieldKey(level.id),
- label: `${t('Level')} ${formatLevelLabel(level.value)}`,
+ label: `${t('Color')} ${formatLevelLabel(level.value)}`,
type: 'color',
defaultValue: level.color,
},
diff --git a/src/core/Drawings/helpers.ts b/src/core/Drawings/helpers.ts
index 951ca6d..95b9991 100644
--- a/src/core/Drawings/helpers.ts
+++ b/src/core/Drawings/helpers.ts
@@ -1,5 +1,3 @@
-import { clamp } from 'lodash-es';
-
import type { Anchor, Bounds, ContainerSize, Point, SeriesApi } from './types';
import type { Coordinate, IChartApi, Logical, Time } from 'lightweight-charts';
@@ -51,6 +49,10 @@ export function getXCoordinateFromTime(chart: IChartApi, time: Time, series?: Se
return projectedCoordinate;
}
+export function clamp(value: number, min: number, max: number): number {
+ return Math.max(min, Math.min(value, max));
+}
+
export function getContainerSize(container: HTMLElement): ContainerSize {
const rect = container.getBoundingClientRect();
diff --git a/src/core/Drawings/parallelChannel/index.ts b/src/core/Drawings/parallelChannel/index.ts
deleted file mode 100644
index 43c9218..0000000
--- a/src/core/Drawings/parallelChannel/index.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { ParallelChannelPaneRenderer } from './paneRenderer';
-import { ParallelChannelPaneView } from './paneView';
-import { ParallelChannel } from './parallelChannel';
-
-export { ParallelChannel, ParallelChannelPaneRenderer, ParallelChannelPaneView };
-
-export type { ParallelChannelRenderData } from './parallelChannel';
diff --git a/src/core/Drawings/parallelChannel/paneRenderer.ts b/src/core/Drawings/parallelChannel/paneRenderer.ts
deleted file mode 100644
index f394adf..0000000
--- a/src/core/Drawings/parallelChannel/paneRenderer.ts
+++ /dev/null
@@ -1,266 +0,0 @@
-import { CanvasRenderingTarget2D } from 'fancy-canvas';
-import { IPrimitivePaneRenderer } from 'lightweight-charts';
-
-import { getThemeStore } from '@src/theme';
-
-import type { ParallelChannel } from './parallelChannel';
-
-import type { Point } from '@core/Drawings/types';
-
-const UI = {
- lineWidth: 2,
- middleLineWidth: 1,
- middleLineDash: 6,
- middleLineGap: 4,
- handleRadius: 5,
- handleBorderWidth: 2,
- textLineHeightMultiplier: 1.2,
- textOffset: 5,
- textStartGap: 5,
-};
-
-export class ParallelChannelPaneRenderer implements IPrimitivePaneRenderer {
- private parallelChannel: ParallelChannel;
-
- constructor(parallelChannel: ParallelChannel) {
- this.parallelChannel = parallelChannel;
- }
-
- public draw(target: CanvasRenderingTarget2D): void {
- const data = this.parallelChannel.getRenderData();
-
- if (!data) {
- return;
- }
-
- target.useBitmapCoordinateSpace(({ context, horizontalPixelRatio, verticalPixelRatio }) => {
- const pixelRatio = Math.max(horizontalPixelRatio, verticalPixelRatio);
-
- const startPoint = scalePoint(data.startPoint, horizontalPixelRatio, verticalPixelRatio);
- const mainMiddlePoint = scalePoint(data.mainMiddlePoint, horizontalPixelRatio, verticalPixelRatio);
- const endPoint = scalePoint(data.endPoint, horizontalPixelRatio, verticalPixelRatio);
-
- const parallelStartPoint = scalePoint(data.parallelStartPoint, horizontalPixelRatio, verticalPixelRatio);
- const parallelMiddlePoint = scalePoint(data.parallelMiddlePoint, horizontalPixelRatio, verticalPixelRatio);
- const parallelEndPoint = scalePoint(data.parallelEndPoint, horizontalPixelRatio, verticalPixelRatio);
-
- const middleStartPoint = scalePoint(data.middleStartPoint, horizontalPixelRatio, verticalPixelRatio);
- const middleEndPoint = scalePoint(data.middleEndPoint, horizontalPixelRatio, verticalPixelRatio);
-
- context.save();
-
- drawChannelFill(context, [startPoint, endPoint, parallelEndPoint, parallelStartPoint], data.backgroundColor);
-
- context.strokeStyle = data.lineColor;
- context.lineWidth = UI.lineWidth * pixelRatio;
-
- drawLine(context, startPoint, endPoint);
- drawLine(context, parallelStartPoint, parallelEndPoint);
-
- if (data.showMiddleLine) {
- context.save();
-
- context.lineWidth = UI.middleLineWidth * pixelRatio;
- context.setLineDash([UI.middleLineDash * pixelRatio, UI.middleLineGap * pixelRatio]);
-
- drawLine(context, middleStartPoint, middleEndPoint);
-
- context.restore();
- }
-
- if (data.text.trim()) {
- const channelCenterPoint = getMiddlePoint(mainMiddlePoint, parallelMiddlePoint);
- const isMainLineAbove = mainMiddlePoint.y <= parallelMiddlePoint.y;
-
- drawTextAlongLine(context, {
- startPoint: isMainLineAbove ? startPoint : parallelStartPoint,
- endPoint: isMainLineAbove ? endPoint : parallelEndPoint,
- channelCenterPoint,
- text: data.text,
- fontSize: data.fontSize,
- isBold: data.isBold,
- isItalic: data.isItalic,
- textColor: data.textColor,
- pixelRatio,
- verticalPixelRatio,
- });
- }
-
- if (data.showHandles) {
- const { colors } = getThemeStore();
-
- context.fillStyle = colors.chartBackground;
-
- context.strokeStyle = colors.chartLineColor;
-
- context.lineWidth = UI.handleBorderWidth * pixelRatio;
-
- const radius = UI.handleRadius * pixelRatio;
-
- drawHandle(context, startPoint, radius);
- drawHandle(context, mainMiddlePoint, radius);
- drawHandle(context, endPoint, radius);
- drawHandle(context, parallelStartPoint, radius);
- drawHandle(context, parallelMiddlePoint, radius);
- drawHandle(context, parallelEndPoint, radius);
- }
-
- context.restore();
- });
- }
-}
-
-function scalePoint(point: Point, horizontalPixelRatio: number, verticalPixelRatio: number): Point {
- return {
- x: point.x * horizontalPixelRatio,
- y: point.y * verticalPixelRatio,
- };
-}
-
-function drawChannelFill(context: CanvasRenderingContext2D, points: Point[], color: string): void {
- const [startPoint, endPoint, parallelEndPoint, parallelStartPoint] = points;
-
- context.save();
- context.fillStyle = color;
-
- context.beginPath();
- context.moveTo(startPoint.x, startPoint.y);
- context.lineTo(endPoint.x, endPoint.y);
- context.lineTo(parallelEndPoint.x, parallelEndPoint.y);
- context.lineTo(parallelStartPoint.x, parallelStartPoint.y);
- context.closePath();
- context.fill();
-
- context.restore();
-}
-
-function drawLine(context: CanvasRenderingContext2D, startPoint: Point, endPoint: Point): void {
- context.beginPath();
- context.moveTo(startPoint.x, startPoint.y);
- context.lineTo(endPoint.x, endPoint.y);
- context.stroke();
-}
-
-function drawHandle(context: CanvasRenderingContext2D, point: Point, radius: number): void {
- context.beginPath();
- context.arc(point.x, point.y, radius, 0, Math.PI * 2);
- context.fill();
- context.stroke();
-}
-
-function drawTextAlongLine(
- context: CanvasRenderingContext2D,
- params: {
- startPoint: Point;
- endPoint: Point;
- channelCenterPoint: Point;
- text: string;
- fontSize: number;
- isBold: boolean;
- isItalic: boolean;
- textColor: string;
- pixelRatio: number;
- verticalPixelRatio: number;
- },
-): void {
- const {
- startPoint,
- endPoint,
- channelCenterPoint,
- text,
- fontSize,
- isBold,
- isItalic,
- textColor,
- pixelRatio,
- verticalPixelRatio,
- } = params;
-
- let renderStartPoint = startPoint;
- let renderEndPoint = endPoint;
-
- let deltaX = renderEndPoint.x - renderStartPoint.x;
- let deltaY = renderEndPoint.y - renderStartPoint.y;
- let angle = Math.atan2(deltaY, deltaX);
-
- if (angle > Math.PI / 2 || angle < -Math.PI / 2) {
- renderStartPoint = endPoint;
- renderEndPoint = startPoint;
-
- deltaX = renderEndPoint.x - renderStartPoint.x;
- deltaY = renderEndPoint.y - renderStartPoint.y;
- angle = Math.atan2(deltaY, deltaX);
- }
-
- const lineLength = Math.hypot(deltaX, deltaY);
-
- if (!lineLength) {
- return;
- }
-
- const directionX = deltaX / lineLength;
-
- const directionY = deltaY / lineLength;
-
- const lineMiddlePoint = getMiddlePoint(startPoint, endPoint);
-
- const outwardX = lineMiddlePoint.x - channelCenterPoint.x;
-
- const outwardY = lineMiddlePoint.y - channelCenterPoint.y;
-
- const outwardLength = Math.hypot(outwardX, outwardY);
-
- const normalX = outwardLength ? outwardX / outwardLength : 0;
-
- const normalY = outwardLength ? outwardY / outwardLength : -1;
-
- const lines = text.split('\n');
- const safeFontSize = Math.max(1, fontSize);
- const fontSizePx = safeFontSize * verticalPixelRatio;
- const lineHeight = safeFontSize * UI.textLineHeightMultiplier * verticalPixelRatio;
- const fontWeight = isBold ? '700 ' : '';
- const fontStyle = isItalic ? 'italic ' : '';
-
- context.save();
-
- context.font = `${fontStyle}${fontWeight}${fontSizePx}px Inter, sans-serif`;
-
- const textWidth = lines.reduce((maxWidth, line) => {
- return Math.max(maxWidth, context.measureText(line || ' ').width);
- }, 0);
-
- const blockHeight = lines.length * lineHeight;
-
- const startPadding = (UI.handleRadius * 2 + UI.textStartGap) * pixelRatio;
-
- const desiredDistance = startPadding + textWidth / 2;
- const availableDistance = lineLength - textWidth / 2 - UI.textStartGap * pixelRatio;
-
- const distanceAlongLine = availableDistance >= desiredDistance ? desiredDistance : lineLength / 2;
- const outwardDistance = blockHeight / 2 + UI.textOffset * verticalPixelRatio;
-
- const textCenterX = renderStartPoint.x + directionX * distanceAlongLine + normalX * outwardDistance;
- const textCenterY = renderStartPoint.y + directionY * distanceAlongLine + normalY * outwardDistance;
-
- context.translate(textCenterX, textCenterY);
- context.rotate(angle);
-
- context.fillStyle = textColor;
- context.textAlign = 'center';
- context.textBaseline = 'middle';
-
- const firstLineY = (-(lines.length - 1) * lineHeight) / 2;
-
- lines.forEach((line, index) => {
- context.fillText(line, 0, firstLineY + index * lineHeight);
- });
-
- context.restore();
-}
-
-function getMiddlePoint(startPoint: Point, endPoint: Point): Point {
- return {
- x: (startPoint.x + endPoint.x) / 2,
- y: (startPoint.y + endPoint.y) / 2,
- };
-}
diff --git a/src/core/Drawings/parallelChannel/paneView.ts b/src/core/Drawings/parallelChannel/paneView.ts
deleted file mode 100644
index a4153c6..0000000
--- a/src/core/Drawings/parallelChannel/paneView.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { IPrimitivePaneRenderer, IPrimitivePaneView, PrimitivePaneViewZOrder } from 'lightweight-charts';
-
-import { ParallelChannelPaneRenderer } from './paneRenderer';
-
-import type { ParallelChannel } from './parallelChannel';
-
-export class ParallelChannelPaneView implements IPrimitivePaneView {
- private paneRenderer: ParallelChannelPaneRenderer;
-
- constructor(parallelChannel: ParallelChannel) {
- this.paneRenderer = new ParallelChannelPaneRenderer(parallelChannel);
- }
-
- public update(): void {}
-
- public renderer(): IPrimitivePaneRenderer {
- return this.paneRenderer;
- }
-
- public zOrder(): PrimitivePaneViewZOrder {
- return 'top';
- }
-}
diff --git a/src/core/Drawings/parallelChannel/parallelChannel.ts b/src/core/Drawings/parallelChannel/parallelChannel.ts
deleted file mode 100644
index 9a127b6..0000000
--- a/src/core/Drawings/parallelChannel/parallelChannel.ts
+++ /dev/null
@@ -1,1072 +0,0 @@
-import { IChartApi, IPrimitivePaneView, PrimitiveHoveredItem, UTCTimestamp } from 'lightweight-charts';
-import { Observable } from 'rxjs';
-
-import {
- CustomPriceAxisPaneView,
- CustomPriceAxisView,
- CustomTimeAxisPaneView,
- CustomTimeAxisView,
-} from '@core/Drawings/axis';
-import { SeriesDrawingBase } from '@core/Drawings/common';
-import {
- getAnchorFromPoint,
- getPriceDelta as getPriceDeltaFromCoordinates,
- getPriceFromYCoordinate,
- getXCoordinateFromTime,
- getYCoordinateFromPrice,
- isNearPoint,
- shiftTimeByPixels,
-} from '@core/Drawings/helpers';
-import { updateViews } 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 { ParallelChannelPaneView } from './paneView';
-import {
- createDefaultSettings,
- getParallelChannelSettingsTabs,
- ParallelChannelSettings,
- ParallelChannelStyle,
- ParallelChannelTextStyle,
-} from './settings';
-
-import type { DrawingInteraction, ISeriesDrawing } from '@core/Drawings/common';
-import type { Anchor, AxisLabel, AxisSegment, Point, SeriesApi } from '@core/Drawings/types';
-import type { ChartOptionsModel, SettingsTab } from '@src/types';
-
-type ParallelChannelMode = 'idle' | 'drawing-line' | 'drawing-channel' | 'ready' | 'dragging';
-
-type ParallelChannelDragTarget =
- | 'main-start'
- | 'main-middle'
- | 'main-end'
- | 'parallel-start'
- | 'parallel-middle'
- | 'parallel-end'
- | 'body';
-
-type TimeLabelKind = 'start' | 'end';
-
-type PriceLabelKind = 'main-start' | 'main-end' | 'parallel-start' | 'parallel-end';
-
-interface ParallelChannelParams {
- container: HTMLElement;
- interaction: DrawingInteraction;
- formatObservable?: Observable<ChartOptionsModel>;
- openSettings?: () => void;
-}
-
-interface ParallelChannelState {
- hidden: boolean;
- mode: ParallelChannelMode;
- startAnchor: Anchor | null;
- endAnchor: Anchor | null;
- priceOffset: number | null;
- settings: ParallelChannelSettings;
-}
-
-interface ParallelChannelGeometry {
- startPoint: Point;
- mainMiddlePoint: Point;
- endPoint: Point;
- parallelStartPoint: Point;
- parallelMiddlePoint: Point;
- parallelEndPoint: Point;
- middleStartPoint: Point;
- middleEndPoint: Point;
- left: number;
- right: number;
- top: number;
- bottom: number;
-}
-
-export interface ParallelChannelRenderData
- extends ParallelChannelGeometry,
- ParallelChannelStyle,
- ParallelChannelTextStyle {
- showHandles: boolean;
-}
-
-const HANDLE_HIT_TOLERANCE = 8;
-const LINE_HIT_TOLERANCE = 6;
-const MIN_LINE_SIZE = 4;
-const MIN_CHANNEL_WIDTH = 4;
-const VERTICAL_LINE_TOLERANCE = 0.001;
-
-export class ParallelChannel extends SeriesDrawingBase<ParallelChannelSettings> implements ISeriesDrawing {
- private openSettings?: () => void;
-
- 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;
- private dragStateSnapshot: ParallelChannelState | null = null;
-
- private displayFormat: ChartOptionsModel = {
- dateFormat: Defaults.dateFormat,
- timeFormat: Defaults.timeFormat,
- 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(
- chart: IChartApi,
- series: SeriesApi,
- { container, interaction, formatObservable, openSettings }: ParallelChannelParams,
- ) {
- 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.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);
- }
-
- public isCreationPending(): boolean {
- return this.mode === 'idle' || this.mode === 'drawing-line' || this.mode === 'drawing-channel';
- }
-
- public getState(): ParallelChannelState {
- return {
- hidden: this.hidden,
- mode: this.mode,
- startAnchor: this.startAnchor,
- endAnchor: this.endAnchor,
- priceOffset: this.priceOffset,
- settings: { ...this.settings },
- };
- }
-
- public setState(state: unknown): void {
- if (!state || typeof state !== 'object') {
- return;
- }
-
- const nextState = state as Partial<ParallelChannelState>;
-
- if (typeof nextState.hidden === 'boolean') {
- this.hidden = nextState.hidden;
- }
-
- if (nextState.mode) {
- this.mode = nextState.mode === 'dragging' ? 'ready' : nextState.mode;
- }
-
- if ('startAnchor' in nextState) {
- this.startAnchor = nextState.startAnchor ?? null;
- }
-
- if ('endAnchor' in nextState) {
- this.endAnchor = nextState.endAnchor ?? null;
- }
-
- if ('priceOffset' in nextState) {
- this.priceOffset = nextState.priceOffset ?? null;
- }
-
- if (nextState.settings) {
- this.settings = {
- ...createDefaultSettings(),
- ...nextState.settings,
- };
- }
-
- this.render();
- }
-
- public getSettingsTabs(): SettingsTab[] {
- 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;
- }
-
- const geometry = this.getGeometry();
-
- if (!geometry) {
- return null;
- }
-
- return {
- ...geometry,
- ...this.settings,
- showHandles: this.shouldShowHandles(),
- };
- }
-
- protected getHoveredItem(x: number, y: number): PrimitiveHoveredItem | null {
- if (this.hidden || this.mode !== 'ready') {
- return null;
- }
-
- const point = { x, y };
- const pointTarget = this.getPointTarget(point);
- const isChannelHit = this.isPointOnChannel(point);
-
- if (!pointTarget && !isChannelHit) {
- return null;
- }
-
- if (!this.isSelected()) {
- return {
- cursorStyle: 'pointer',
- externalId: 'parallel-channel',
- zOrder: 'top',
- };
- }
-
- return {
- cursorStyle: pointTarget ? 'move' : 'grab',
- externalId: 'parallel-channel',
- zOrder: 'top',
- };
- }
-
- 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 anchor = kind === 'start' ? this.startAnchor : this.endAnchor;
-
- 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(
- 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)) {
- return null;
- }
-
- const price = this.getPriceLabelValue(kind);
-
- 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,
- };
- }
-
- 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.getPointTarget(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-line') {
- event.preventDefault();
- event.stopPropagation();
-
- this.setEndAnchor(point);
-
- if (!this.hasValidMainLine()) {
- this.render();
- return;
- }
-
- this.priceOffset = 0;
- this.mode = 'drawing-channel';
-
- this.render();
- return;
- }
-
- if (this.mode === 'drawing-channel') {
- event.preventDefault();
- event.stopPropagation();
-
- this.setPriceOffset(point);
-
- if (!this.hasValidChannelWidth()) {
- this.render();
- return;
- }
-
- this.finishDrawing();
- return;
- }
-
- if (this.mode !== 'ready') {
- return;
- }
-
- const pointTarget = this.getPointTarget(point);
- const isChannelHit = this.isPointOnChannel(point);
- const isDrawingHit = pointTarget !== null || isChannelHit;
-
- if (!this.isSelected()) {
- if (!isDrawingHit) {
- return;
- }
-
- event.preventDefault();
- event.stopPropagation();
-
- this.select();
- return;
- }
-
- if (pointTarget) {
- event.preventDefault();
- event.stopPropagation();
-
- this.startDragging(pointTarget, point, event.pointerId);
-
- return;
- }
-
- if (isChannelHit) {
- event.preventDefault();
- event.stopPropagation();
-
- this.startDragging('body', point, event.pointerId);
-
- return;
- }
-
- this.deselect();
- };
-
- protected handlePointerMove = (event: PointerEvent): void => {
- const point = this.getEventPoint(event);
-
- if (this.mode === 'drawing-line') {
- this.setEndAnchor(point);
- this.render();
- return;
- }
-
- if (this.mode === 'drawing-channel') {
- this.setPriceOffset(point);
- this.render();
- return;
- }
-
- if (this.mode !== 'dragging' || this.dragPointerId !== event.pointerId || !this.activeDragTarget) {
- return;
- }
-
- event.preventDefault();
- event.stopPropagation();
-
- this.applyDrag(point);
- this.render();
- };
-
- protected handlePointerUp = (event: PointerEvent): void => {
- if (this.mode !== 'dragging' || this.dragPointerId !== event.pointerId) {
- return;
- }
-
- this.finishDragging();
- };
-
- protected getGeometry(): ParallelChannelGeometry | null {
- if (!this.startAnchor || !this.endAnchor || this.priceOffset === null) {
- return null;
- }
-
- const startPoint = this.getAnchorPoint(this.startAnchor);
- const endPoint = this.getAnchorPoint(this.endAnchor);
-
- const parallelStartPoint = this.getAnchorPoint({
- time: this.startAnchor.time,
- price: this.startAnchor.price + this.priceOffset,
- });
-
- const parallelEndPoint = this.getAnchorPoint({
- time: this.endAnchor.time,
- price: this.endAnchor.price + this.priceOffset,
- });
-
- if (!startPoint || !endPoint || !parallelStartPoint || !parallelEndPoint) {
- return null;
- }
-
- const mainMiddlePoint = getMiddlePoint(startPoint, endPoint);
-
- const parallelMiddlePoint = getMiddlePoint(parallelStartPoint, parallelEndPoint);
-
- const middleStartPoint = getMiddlePoint(startPoint, parallelStartPoint);
-
- const middleEndPoint = getMiddlePoint(endPoint, parallelEndPoint);
-
- const points = [startPoint, endPoint, parallelStartPoint, parallelEndPoint];
-
- return {
- startPoint,
- mainMiddlePoint,
- endPoint,
- parallelStartPoint,
- parallelMiddlePoint,
- parallelEndPoint,
- middleStartPoint,
- middleEndPoint,
- 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)),
- };
- }
-
- private startDrawing(point: Point): void {
- const anchor = this.createAnchor(point);
-
- if (!anchor) {
- return;
- }
-
- this.startAnchor = anchor;
- this.endAnchor = anchor;
- this.priceOffset = 0;
- this.mode = 'drawing-line';
-
- this.render();
- }
-
- private finishDrawing(): void {
- this.mode = 'ready';
- this.resolveReady?.();
-
- this.render();
- }
-
- private startDragging(target: ParallelChannelDragTarget, point: Point, pointerId: number): void {
- this.mode = 'dragging';
- this.activeDragTarget = target;
- 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();
- }
-
- private applyDrag(point: Point): void {
- switch (this.activeDragTarget) {
- 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;
- }
- }
-
- private moveMainEdge(kind: TimeLabelKind, point: Point): void {
- const anchor = this.createAnchor(point);
-
- if (!anchor) {
- return;
- }
-
- const previousAnchor = kind === 'start' ? this.startAnchor : this.endAnchor;
-
- if (kind === 'start') {
- this.startAnchor = anchor;
- } else {
- this.endAnchor = anchor;
- }
-
- if (this.hasValidMainLine()) {
- return;
- }
-
- if (kind === 'start') {
- this.startAnchor = previousAnchor;
- } else {
- this.endAnchor = previousAnchor;
- }
- }
-
- private moveParallelEdge(kind: TimeLabelKind, point: Point): void {
- const snapshot = this.dragStateSnapshot;
- const anchor = this.createAnchor(point);
-
- if (!snapshot || snapshot.priceOffset === null || !anchor) {
- return;
- }
-
- const previousAnchor = kind === 'start' ? this.startAnchor : this.endAnchor;
-
- 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;
- }
-
- if (kind === 'start') {
- this.startAnchor = previousAnchor;
- } else {
- this.endAnchor = previousAnchor;
- }
- }
-
- private moveMainMiddle(point: Point): void {
- const snapshot = this.dragStateSnapshot;
-
- if (!snapshot?.startAnchor || !snapshot.endAnchor || snapshot.priceOffset === null) {
- return;
- }
-
- const pointerPrice = getPriceFromYCoordinate(this.series, point.y);
-
- const linePrice = this.getLinePriceAtX(snapshot.startAnchor, snapshot.endAnchor, point.x);
-
- if (pointerPrice === null || linePrice === null) {
- return;
- }
-
- const priceDelta = pointerPrice - linePrice;
-
- this.startAnchor = {
- ...snapshot.startAnchor,
- price: snapshot.startAnchor.price + priceDelta,
- };
-
- this.endAnchor = {
- ...snapshot.endAnchor,
- price: snapshot.endAnchor.price + priceDelta,
- };
-
- this.priceOffset = snapshot.priceOffset - priceDelta;
-
- if (this.hasValidChannelWidth()) {
- return;
- }
-
- 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;
- }
-
- this.priceOffset = previousOffset;
- }
-
- private moveBody(point: Point): void {
- const snapshot = this.dragStateSnapshot;
-
- if (!snapshot?.startAnchor || !snapshot.endAnchor || snapshot.priceOffset === null || !this.dragStartPoint) {
- 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.priceOffset = snapshot.priceOffset;
- }
-
- private setEndAnchor(point: Point): void {
- const anchor = this.createAnchor(point);
-
- if (!anchor) {
- return;
- }
-
- this.endAnchor = anchor;
- }
-
- private setPriceOffset(point: Point): void {
- if (!this.startAnchor || !this.endAnchor) {
- return;
- }
-
- const pointerPrice = getPriceFromYCoordinate(this.series, point.y);
-
- const linePrice = this.getLinePriceAtX(this.startAnchor, this.endAnchor, point.x);
-
- if (pointerPrice === null || linePrice === null) {
- return;
- }
-
- this.priceOffset = pointerPrice - linePrice;
- }
-
- private getLinePriceAtX(startAnchor: Anchor, endAnchor: Anchor, x: number): number | null {
- const startPoint = this.getAnchorPoint(startAnchor);
- const endPoint = this.getAnchorPoint(endAnchor);
-
- if (!startPoint || !endPoint) {
- return null;
- }
-
- const deltaX = endPoint.x - startPoint.x;
-
- if (Math.abs(deltaX) <= VERTICAL_LINE_TOLERANCE) {
- return getPriceFromYCoordinate(this.series, (startPoint.y + endPoint.y) / 2);
- }
-
- const ratio = (x - startPoint.x) / deltaX;
-
- const y = startPoint.y + (endPoint.y - startPoint.y) * ratio;
-
- return getPriceFromYCoordinate(this.series, y);
- }
-
- private hasValidMainLine(): boolean {
- const geometry = this.getGeometry();
-
- if (!geometry) {
- return false;
- }
-
- return 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;
- }
-
- private getPointTarget(point: Point): ParallelChannelDragTarget | null {
- const geometry = this.getGeometry();
-
- if (!geometry) {
- return null;
- }
-
- const targets: [ParallelChannelDragTarget, Point][] = [
- ['main-start', geometry.startPoint],
- ['main-middle', geometry.mainMiddlePoint],
- ['main-end', geometry.endPoint],
- ['parallel-start', geometry.parallelStartPoint],
- ['parallel-middle', geometry.parallelMiddlePoint],
- ['parallel-end', geometry.parallelEndPoint],
- ];
-
- for (const [target, targetPoint] of targets) {
- if (isNearPoint(point, targetPoint.x, targetPoint.y, HANDLE_HIT_TOLERANCE)) {
- return target;
- }
- }
-
- return null;
- }
-
- private isPointOnChannel(point: Point): boolean {
- const geometry = this.getGeometry();
-
- if (!geometry) {
- return false;
- }
-
- if (getDistanceToSegment(point, geometry.startPoint, geometry.endPoint) <= LINE_HIT_TOLERANCE) {
- return true;
- }
-
- if (getDistanceToSegment(point, geometry.parallelStartPoint, geometry.parallelEndPoint) <= LINE_HIT_TOLERANCE) {
- return true;
- }
-
- if (
- this.settings.showMiddleLine &&
- getDistanceToSegment(point, geometry.middleStartPoint, geometry.middleEndPoint) <= LINE_HIT_TOLERANCE
- ) {
- return true;
- }
-
- return isPointInPolygon(point, [
- geometry.startPoint,
- geometry.endPoint,
- geometry.parallelEndPoint,
- geometry.parallelStartPoint,
- ]);
- }
-
- private getPriceLabelValue(kind: PriceLabelKind): number | null {
- if (!this.startAnchor || !this.endAnchor || this.priceOffset === null) {
- return null;
- }
-
- 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 {
- return kind === 'main-start' || kind === 'main-end' || kind === 'parallel-start' || kind === 'parallel-end';
-}
-
-function getMiddlePoint(startPoint: Point, endPoint: Point): Point {
- return {
- x: (startPoint.x + endPoint.x) / 2,
- y: (startPoint.y + endPoint.y) / 2,
- };
-}
-
-function getDistance(startPoint: Point, endPoint: Point): number {
- return Math.hypot(endPoint.x - startPoint.x, endPoint.y - startPoint.y);
-}
-
-function getDistanceToSegment(point: Point, startPoint: Point, endPoint: Point): number {
- const deltaX = endPoint.x - startPoint.x;
- const deltaY = endPoint.y - startPoint.y;
-
- if (deltaX === 0 && deltaY === 0) {
- return getDistance(point, startPoint);
- }
-
- const ratio = Math.max(
- 0,
- Math.min(
- 1,
- ((point.x - startPoint.x) * deltaX + (point.y - startPoint.y) * deltaY) / (deltaX * deltaX + deltaY * deltaY),
- ),
- );
-
- const projectionX = startPoint.x + ratio * deltaX;
-
- const projectionY = startPoint.y + ratio * deltaY;
-
- return Math.hypot(point.x - projectionX, point.y - projectionY);
-}
-
-function isPointInPolygon(point: Point, polygon: Point[]): boolean {
- let isInside = false;
-
- for (let index = 0, previousIndex = polygon.length - 1; index < polygon.length; previousIndex = index, index += 1) {
- const currentPoint = polygon[index];
- const previousPoint = polygon[previousIndex];
-
- const intersects =
- currentPoint.y > point.y !== previousPoint.y > point.y &&
- point.x <
- ((previousPoint.x - currentPoint.x) * (point.y - currentPoint.y)) / (previousPoint.y - currentPoint.y) +
- currentPoint.x;
-
- if (intersects) {
- isInside = !isInside;
- }
- }
-
- return isInside;
-}
diff --git a/src/core/Drawings/parallelChannel/settings.ts b/src/core/Drawings/parallelChannel/settings.ts
deleted file mode 100644
index 81695e9..0000000
--- a/src/core/Drawings/parallelChannel/settings.ts
+++ /dev/null
@@ -1,119 +0,0 @@
-import { getThemeStore } from '@src/theme';
-import { t } from '@src/translations';
-
-import { SettingField, SettingsTab, SettingsValues } from '@src/types';
-
-export interface ParallelChannelStyle {
- lineColor: string;
- backgroundColor: string;
- showMiddleLine: boolean;
-}
-
-export interface ParallelChannelTextStyle {
- fontSize: number;
- text: string;
- isBold: boolean;
- isItalic: boolean;
- textColor: string;
-}
-
-export type ParallelChannelSettings = ParallelChannelStyle & ParallelChannelTextStyle & SettingsValues;
-
-export function createDefaultSettings(): ParallelChannelSettings {
- const { colors } = getThemeStore();
-
- return {
- lineColor: colors.chartLineColor,
- backgroundColor: colors.axisMarkerAreaFill,
- showMiddleLine: true,
- fontSize: 14,
- text: '',
- isBold: false,
- isItalic: false,
- textColor: colors.chartLineColor,
- };
-}
-
-export function getParallelChannelSettingsTabs(settings: ParallelChannelSettings): SettingsTab[] {
- const styleFields: SettingField[] = [
- {
- key: 'lineColor',
- label: t('Line'),
- type: 'color',
- defaultValue: settings.lineColor,
- toolbar: {
- control: 'color',
- role: 'line',
- },
- },
- {
- key: 'backgroundColor',
- label: t('Background'),
- type: 'color',
- defaultValue: settings.backgroundColor,
- toolbar: {
- control: 'color',
- role: 'fill',
- },
- },
- {
- key: 'showMiddleLine',
- label: t('Middle line'),
- type: 'boolean',
- defaultValue: settings.showMiddleLine,
- },
- ];
-
- const textFields: SettingField[] = [
- {
- key: 'fontSize',
- label: t('Font size'),
- type: 'number',
- defaultValue: settings.fontSize,
- min: 8,
- max: 24,
- },
- {
- key: 'text',
- label: t('Text'),
- type: 'textarea',
- defaultValue: settings.text,
- placeholder: t('Enter text'),
- },
- {
- key: 'isBold',
- label: t('Bold'),
- type: 'boolean',
- defaultValue: settings.isBold,
- },
- {
- key: 'isItalic',
- label: t('Italic'),
- type: 'boolean',
- defaultValue: settings.isItalic,
- },
- {
- key: 'textColor',
- label: t('Text'),
- type: 'color',
- defaultValue: settings.textColor,
- toolbar: {
- control: 'color',
- role: 'text',
- },
- },
- ];
-
- return [
- {
- key: 'style',
- label: t('Style'),
- fields: styleFields,
- },
- {
- key: 'text',
- label: t('Text'),
- fields: textFields,
- },
- ];
-}
diff --git a/src/core/Drawings/ray/ray.ts b/src/core/Drawings/ray/ray.ts
index e37a606..7c1c0a8 100644
--- a/src/core/Drawings/ray/ray.ts
+++ b/src/core/Drawings/ray/ray.ts
@@ -27,7 +27,7 @@ import { RayPaneView } from './paneView';
import { createDefaultSettings, getRaySettingTabs, RaySettings, RayStyle, RayTextStyle } from './settings';
-import type { DrawingInteraction, ISeriesDrawing } from '@core/Drawings/common';
+import type { ISeriesDrawing } from '@core/Drawings/common';
import type { Anchor, AxisLabel, AxisSegment, Point, SeriesApi } from '@core/Drawings/types';
import type { ChartOptionsModel, SettingsTab } from '@src/types';
@@ -37,7 +37,6 @@ type PriceLabelKind = 'start' | 'direction';
interface RayParams {
container: HTMLElement;
- interaction: DrawingInteraction;
formatObservable?: Observable<ChartOptionsModel>;
removeSelf?: () => void;
openSettings?: () => void;
@@ -45,6 +44,7 @@ interface RayParams {
interface RayState {
hidden: boolean;
+ isActive: boolean;
mode: RayMode;
startAnchor: Anchor | null;
directionAnchor: Anchor | null;
@@ -101,10 +101,9 @@ export class Ray extends SeriesDrawingBase<RaySettings> implements ISeriesDrawin
constructor(
chart: IChartApi,
series: SeriesApi,
- { container, interaction, formatObservable, removeSelf, openSettings }: RayParams,
+ { container, formatObservable, removeSelf, openSettings }: RayParams,
) {
- super({ chart, series, container, interaction });
-
+ super({ chart, series, container });
this.removeSelf = removeSelf;
this.openSettings = openSettings;
@@ -157,6 +156,7 @@ export class Ray extends SeriesDrawingBase<RaySettings> implements ISeriesDrawin
public getState(): RayState {
return {
hidden: this.hidden,
+ isActive: this.isActive.value,
mode: this.mode,
startAnchor: this.startAnchor,
directionAnchor: this.directionAnchor,
@@ -171,6 +171,10 @@ export class Ray extends SeriesDrawingBase<RaySettings> implements ISeriesDrawin
this.hidden = nextState.hidden;
}
+ if ('isActive' in nextState && typeof nextState.isActive === 'boolean') {
+ this.isActive.next(nextState.isActive);
+ }
+
if ('mode' in nextState && nextState.mode) {
this.mode = nextState.mode;
}
@@ -242,12 +246,12 @@ export class Ray extends SeriesDrawingBase<RaySettings> implements ISeriesDrawin
return {
...geometry,
- showHandles: this.shouldShowHandles(),
+ showHandles: this.isActive.value,
...this.settings,
};
}
- protected getHoveredItem(x: number, y: number): PrimitiveHoveredItem | null {
+ public hitTest(x: number, y: number): PrimitiveHoveredItem | null {
if (this.hidden || this.mode === 'idle' || this.mode === 'drawing') {
return null;
}
@@ -274,7 +278,7 @@ export class Ray extends SeriesDrawingBase<RaySettings> implements ISeriesDrawin
}
protected getTimeAxisSegments(): AxisSegment[] {
- if (!this.isSelected() && !this.isCreationPending()) {
+ if (!this.isActive.value) {
return [];
}
@@ -296,7 +300,7 @@ export class Ray extends SeriesDrawingBase<RaySettings> implements ISeriesDrawin
}
protected getPriceAxisSegments(): AxisSegment[] {
- if (!this.isSelected() && !this.isCreationPending()) {
+ if (!this.isActive.value) {
return [];
}
@@ -318,7 +322,7 @@ export class Ray extends SeriesDrawingBase<RaySettings> implements ISeriesDrawin
}
protected getTimeAxisLabel(kind: string): AxisLabel | null {
- if ((!this.isSelected() && !this.isCreationPending()) || (kind !== 'start' && kind !== 'direction')) {
+ if (!this.isActive.value || (kind !== 'start' && kind !== 'direction')) {
return null;
}
@@ -340,7 +344,7 @@ export class Ray extends SeriesDrawingBase<RaySettings> implements ISeriesDrawin
}
protected getPriceAxisLabel(kind: string): AxisLabel | null {
- if ((!this.isSelected() && !this.isCreationPending()) || (kind !== 'start' && kind !== 'direction')) {
+ if (!this.isActive.value || (kind !== 'start' && kind !== 'direction')) {
return null;
}
@@ -411,18 +415,17 @@ export class Ray extends SeriesDrawingBase<RaySettings> implements ISeriesDrawin
}
const pointTarget = this.getPointTarget(point);
- const isNearRay = this.isPointNearRay(point);
- const isDrawingHit = pointTarget !== null || isNearRay;
- if (!this.isSelected()) {
- if (!isDrawingHit) {
+ if (!this.isActive.value) {
+ if (!pointTarget && !this.isPointNearRay(point)) {
return;
}
event.preventDefault();
event.stopPropagation();
- this.select();
+ this.isActive.next(true);
+ this.render();
return;
}
@@ -442,7 +445,7 @@ export class Ray extends SeriesDrawingBase<RaySettings> implements ISeriesDrawin
return;
}
- if (isNearRay) {
+ if (this.isPointNearRay(point)) {
event.preventDefault();
event.stopPropagation();
@@ -450,7 +453,8 @@ export class Ray extends SeriesDrawingBase<RaySettings> implements ISeriesDrawin
return;
}
- this.deselect();
+ this.isActive.next(false);
+ this.render();
};
protected handlePointerMove = (event: PointerEvent): void => {
@@ -502,6 +506,7 @@ export class Ray extends SeriesDrawingBase<RaySettings> implements ISeriesDrawin
this.startAnchor = anchor;
this.directionAnchor = anchor;
+ this.isActive.next(true);
this.mode = 'drawing';
this.render();
diff --git a/src/core/Drawings/ray/settings.ts b/src/core/Drawings/ray/settings.ts
index 25c1eb4..f0e37a8 100644
--- a/src/core/Drawings/ray/settings.ts
+++ b/src/core/Drawings/ray/settings.ts
@@ -33,13 +33,9 @@ export function getRaySettingTabs(settings: RaySettings): SettingsTab[] {
const styleFields: SettingField[] = [
{
key: 'lineColor',
- label: t('Line'),
+ label: t('Line color'),
type: 'color',
defaultValue: settings.lineColor,
- toolbar: {
- control: 'color',
- role: 'line',
- },
},
];
@@ -67,19 +63,15 @@ export function getRaySettingTabs(settings: RaySettings): SettingsTab[] {
},
{
key: 'isItalic',
- label: t('Italic'),
+ label: t('Italics'),
type: 'boolean',
defaultValue: settings.isItalic,
},
{
key: 'textColor',
- label: t('Text'),
+ label: t('Text color'),
type: 'color',
defaultValue: settings.textColor,
- toolbar: {
- control: 'color',
- role: 'text',
- },
},
];
diff --git a/src/core/Drawings/rectangle/rectangle.ts b/src/core/Drawings/rectangle/rectangle.ts
index ff6fa8b..117c65b 100644
--- a/src/core/Drawings/rectangle/rectangle.ts
+++ b/src/core/Drawings/rectangle/rectangle.ts
@@ -1,4 +1,3 @@
-import { clamp } from 'lodash-es';
import { Observable } from 'rxjs';
import {
@@ -9,9 +8,11 @@ import {
} from '@core/Drawings/axis';
import { SeriesDrawingBase } from '@core/Drawings/common';
import {
+ clamp,
clampPointToContainer as clampPointToContainerInElement,
getAnchorFromPoint,
getContainerSize as getElementContainerSize,
+ getPointerPoint as getPointerPointFromEvent,
getPriceDelta as getPriceDeltaFromCoordinates,
getPriceFromYCoordinate,
getTimeFromXCoordinate,
@@ -38,10 +39,20 @@ import {
RectangleTextStyle,
} from './settings';
-import type { DrawingInteraction, ISeriesDrawing } from '@core/Drawings/common';
+import type { ISeriesDrawing } from '@core/Drawings/common';
import type { AxisLabel, AxisSegment, Point, SeriesApi } from '@core/Drawings/types';
-import type { ChartOptionsModel, SettingsTab } from '@src/types';
-import type { IChartApi, IPrimitivePaneView, PrimitiveHoveredItem, Time, UTCTimestamp } from 'lightweight-charts';
+import type { ChartOptionsModel, SettingsTab, SettingsValues } from '@src/types';
+import type {
+ AutoscaleInfo,
+ IChartApi,
+ IPrimitivePaneView,
+ Logical,
+ PrimitiveHoveredItem,
+ SeriesAttachedParameter,
+ SeriesOptionsMap,
+ Time,
+ UTCTimestamp,
+} from 'lightweight-charts';
type RectangleMode = 'idle' | 'drawing' | 'ready' | 'dragging';
type RectangleHandle = 'body' | 'nw' | 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w' | null;
@@ -51,7 +62,6 @@ type PriceLabelKind = 'top' | 'bottom';
interface RectangleParams {
container: HTMLElement;
- interaction: DrawingInteraction;
formatObservable?: Observable<ChartOptionsModel>;
removeSelf?: () => void;
openSettings?: () => void;
@@ -59,6 +69,7 @@ interface RectangleParams {
interface RectangleState {
hidden: boolean;
+ isActive: boolean;
mode: RectangleMode;
startTime: Time | null;
endTime: Time | null;
@@ -122,10 +133,9 @@ export class Rectangle extends SeriesDrawingBase<RectangleSettings> implements I
constructor(
chart: IChartApi,
series: SeriesApi,
- { container, interaction, formatObservable, removeSelf, openSettings }: RectangleParams,
+ { container, formatObservable, removeSelf, openSettings }: RectangleParams,
) {
- super({ chart, series, container, interaction });
-
+ super({ chart, series, container });
this.removeSelf = removeSelf;
this.openSettings = openSettings;
@@ -178,6 +188,7 @@ export class Rectangle extends SeriesDrawingBase<RectangleSettings> implements I
public getState(): RectangleState {
return {
hidden: this.hidden,
+ isActive: this.isActive.value,
mode: this.mode,
startTime: this.startTime,
endTime: this.endTime,
@@ -194,6 +205,10 @@ export class Rectangle extends SeriesDrawingBase<RectangleSettings> implements I
this.hidden = nextState.hidden;
}
+ if ('isActive' in nextState && typeof nextState.isActive === 'boolean') {
+ this.isActive.next(nextState.isActive);
+ }
+
if ('mode' in nextState && nextState.mode) {
this.mode = nextState.mode;
}
@@ -274,19 +289,19 @@ export class Rectangle extends SeriesDrawingBase<RectangleSettings> implements I
return {
...geometry,
showFill: true,
- showHandles: this.shouldShowHandles(),
+ showHandles: this.isActive.value,
...this.settings,
};
}
- protected getHoveredItem(x: number, y: number): PrimitiveHoveredItem | null {
+ public hitTest(x: number, y: number): PrimitiveHoveredItem | null {
if (this.hidden || this.mode === 'idle' || this.mode === 'drawing') {
return null;
}
const point = { x, y };
- if (!this.isSelected()) {
+ if (!this.isActive.value) {
if (!this.containsPoint(point)) {
return null;
}
@@ -320,7 +335,7 @@ export class Rectangle extends SeriesDrawingBase<RectangleSettings> implements I
}
protected getTimeAxisSegments(): AxisSegment[] {
- if (!this.isSelected() && !this.isCreationPending()) {
+ if (!this.isActive.value) {
return [];
}
@@ -342,7 +357,7 @@ export class Rectangle extends SeriesDrawingBase<RectangleSettings> implements I
}
protected getPriceAxisSegments(): AxisSegment[] {
- if (!this.isSelected() && !this.isCreationPending()) {
+ if (!this.isActive.value) {
return [];
}
@@ -364,7 +379,7 @@ export class Rectangle extends SeriesDrawingBase<RectangleSettings> implements I
}
protected getTimeAxisLabel(kind: string): AxisLabel | null {
- if ((!this.isSelected() && !this.isCreationPending()) || (kind !== 'left' && kind !== 'right')) {
+ if (!this.isActive.value || (kind !== 'left' && kind !== 'right')) {
return null;
}
@@ -387,7 +402,7 @@ export class Rectangle extends SeriesDrawingBase<RectangleSettings> implements I
}
protected getPriceAxisLabel(kind: string): AxisLabel | null {
- if ((!this.isSelected() && !this.isCreationPending()) || (kind !== 'top' && kind !== 'bottom')) {
+ if (!this.isActive.value || (kind !== 'top' && kind !== 'bottom')) {
return null;
}
@@ -458,7 +473,7 @@ export class Rectangle extends SeriesDrawingBase<RectangleSettings> implements I
return;
}
- if (!this.isSelected()) {
+ if (!this.isActive.value) {
if (!this.containsPoint(point)) {
return;
}
@@ -466,14 +481,16 @@ export class Rectangle extends SeriesDrawingBase<RectangleSettings> implements I
event.preventDefault();
event.stopPropagation();
- this.select();
+ this.isActive.next(true);
+ this.render();
return;
}
const dragTarget = this.getDragTarget(point);
if (!dragTarget) {
- this.deselect();
+ this.isActive.next(false);
+ this.render();
return;
}
@@ -526,6 +543,8 @@ export class Rectangle extends SeriesDrawingBase<RectangleSettings> implements I
this.endTime = anchor.time;
this.startPrice = anchor.price;
this.endPrice = anchor.price;
+
+ this.isActive.next(true);
this.mode = 'drawing';
this.render();
@@ -594,13 +613,13 @@ export class Rectangle extends SeriesDrawingBase<RectangleSettings> implements I
private resetToIdle(): void {
this.hidden = false;
+ this.isActive.next(false);
this.mode = 'idle';
this.startTime = null;
this.endTime = null;
this.startPrice = null;
this.endPrice = null;
-
this.clearInteractionState();
this.render();
}
diff --git a/src/core/Drawings/rectangle/settings.ts b/src/core/Drawings/rectangle/settings.ts
index 2797fa9..04c9fe4 100644
--- a/src/core/Drawings/rectangle/settings.ts
+++ b/src/core/Drawings/rectangle/settings.ts
@@ -35,23 +35,15 @@ export function getRectangleSettingsTabs(settings: RectangleSettings): SettingsT
const styleFields: SettingField[] = [
{
key: 'borderColor',
- label: t('Border'),
+ label: t('Border color'),
type: 'color',
defaultValue: settings.borderColor,
- toolbar: {
- control: 'color',
- role: 'line',
- },
},
{
key: 'fillColor',
- label: t('Background'),
+ label: t('Background color'),
type: 'color',
defaultValue: settings.fillColor,
- toolbar: {
- control: 'color',
- role: 'fill',
- },
},
];
@@ -79,19 +71,15 @@ export function getRectangleSettingsTabs(settings: RectangleSettings): SettingsT
},
{
key: 'isItalic',
- label: t('Italic'),
+ label: t('Italics'),
type: 'boolean',
defaultValue: settings.isItalic,
},
{
key: 'textColor',
- label: t('Text'),
+ label: t('Text color'),
type: 'color',
defaultValue: settings.textColor,
- toolbar: {
- control: 'color',
- role: 'text',
- },
},
];
diff --git a/src/core/Drawings/ruler/ruler.ts b/src/core/Drawings/ruler/ruler.ts
index af6595d..d4f9297 100644
--- a/src/core/Drawings/ruler/ruler.ts
+++ b/src/core/Drawings/ruler/ruler.ts
@@ -21,7 +21,7 @@ import { formatDate } from '@src/utils/formatter';
import { RulerPaneView } from './paneView';
-import type { DrawingInteraction, ISeriesDrawing } from '@core/Drawings/common';
+import type { ISeriesDrawing } from '@core/Drawings/common';
import type { Anchor, AxisLabel, AxisSegment, Point } from '@core/Drawings/types';
import type {
AutoscaleInfo,
@@ -33,6 +33,7 @@ import type {
Logical,
MouseEventHandler,
MouseEventParams,
+ SeriesAttachedParameter,
SeriesOptionsMap,
Time,
UTCTimestamp,
@@ -50,7 +51,6 @@ interface RulerState {
interface RulerParams {
container: HTMLElement;
- interaction: DrawingInteraction;
formatObservable?: Observable<ChartOptionsModel>;
resetTriggers?: Observable<unknown>[];
removeSelf?: () => void;
@@ -70,9 +70,7 @@ export interface RulerRenderData {
export class Ruler extends SeriesDrawingBase implements ISeriesDrawing {
private removeSelf?: () => void;
-
protected settings: SettingsValues = {};
- protected mode: RulerMode = 'idle';
private displayFormat: ChartOptionsModel = {
dateFormat: Defaults.dateFormat,
@@ -80,6 +78,8 @@ export class Ruler extends SeriesDrawingBase implements ISeriesDrawing {
showTime: Defaults.showTime,
};
+ protected mode: RulerMode = 'idle';
+
private startAnchor: Anchor | null = null;
private endAnchor: Anchor | null = null;
@@ -97,15 +97,9 @@ export class Ruler extends SeriesDrawingBase implements ISeriesDrawing {
constructor(
chart: IChartApi,
series: SeriesApi,
- { resetTriggers = [], formatObservable, removeSelf, container, interaction }: RulerParams,
+ { resetTriggers = [], formatObservable, removeSelf, container }: RulerParams,
) {
- super({
- chart,
- series,
- container,
- interaction,
- });
-
+ super({ chart, series, container });
this.removeSelf = removeSelf;
this.clickHandler = (params) => this.handleClick(params);
@@ -150,7 +144,7 @@ export class Ruler extends SeriesDrawingBase implements ISeriesDrawing {
);
}
- resetTriggers.forEach((trigger) => {
+ (resetTriggers ?? []).forEach((trigger) => {
this.subscriptions.add(
trigger.pipe(skip(1)).subscribe(() => {
this.removeSelf?.();
@@ -381,7 +375,7 @@ export class Ruler extends SeriesDrawingBase implements ISeriesDrawing {
return formatPrice(Number(anchor.price)) ?? '';
}
- protected getHoveredItem(x: number, y: number): PrimitiveHoveredItem | null {
+ public hitTest(x: number, y: number): PrimitiveHoveredItem | null {
return null;
}
@@ -533,7 +527,6 @@ export class Ruler extends SeriesDrawingBase implements ISeriesDrawing {
this.endAnchor = anchor;
this.mode = 'ready';
this.resolveReady?.();
-
this.showCrosshair();
this.render();
}
diff --git a/src/core/Drawings/sliderPosition/settings.ts b/src/core/Drawings/sliderPosition/settings.ts
index ae187c0..fcc8880 100644
--- a/src/core/Drawings/sliderPosition/settings.ts
+++ b/src/core/Drawings/sliderPosition/settings.ts
@@ -35,29 +35,21 @@ export function getSliderPositionSettingsTabs(settings: SliderPositionSettings):
const styleFields: SettingField[] = [
{
key: 'lineColor',
- label: t('Line'),
+ label: t('Line color'),
type: 'color',
defaultValue: settings.lineColor,
},
{
key: 'positiveFillColor',
- label: t('Profit zone'),
+ label: t('Profit zone color'),
type: 'color',
defaultValue: settings.positiveFillColor,
- toolbar: {
- control: 'color',
- role: 'fill',
- },
},
{
key: 'negativeFillColor',
- label: t('Loss zone'),
+ label: t('Loss zone color'),
type: 'color',
defaultValue: settings.negativeFillColor,
- toolbar: {
- control: 'color',
- role: 'fill',
- },
},
];
@@ -78,19 +70,15 @@ export function getSliderPositionSettingsTabs(settings: SliderPositionSettings):
},
{
key: 'isItalic',
- label: t('Italic'),
+ label: t('Italics'),
type: 'boolean',
defaultValue: settings.isItalic,
},
{
key: 'textColor',
- label: t('Text'),
+ label: t('Text color'),
type: 'color',
defaultValue: settings.textColor,
- toolbar: {
- control: 'color',
- role: 'text',
- },
},
];
diff --git a/src/core/Drawings/sliderPosition/sliderPosition.ts b/src/core/Drawings/sliderPosition/sliderPosition.ts
index f30eb28..d7bc6dd 100644
--- a/src/core/Drawings/sliderPosition/sliderPosition.ts
+++ b/src/core/Drawings/sliderPosition/sliderPosition.ts
@@ -8,6 +8,7 @@ import {
} from '@core/Drawings/axis';
import { SeriesDrawingBase } from '@core/Drawings/common';
import {
+ getPointerPoint as getPointerPointFromEvent,
getPriceDelta as getPriceDeltaFromCoordinates,
getPriceFromYCoordinate,
getPriceRangeInContainer,
@@ -36,7 +37,7 @@ import {
SliderPositionTextStyle,
} from './settings';
-import type { DrawingInteraction, ISeriesDrawing } from '@core/Drawings/common';
+import type { ISeriesDrawing } from '@core/Drawings/common';
import type { AxisLabel, AxisSegment, Bounds, Point, SeriesApi } from '@core/Drawings/types';
import type { ChartOptionsModel, SettingsTab } from '@src/types';
import type {
@@ -58,7 +59,6 @@ type PriceLabelKind = 'target' | 'entry' | 'stop';
interface SliderPositionParams {
side: SliderSide;
container: HTMLElement;
- interaction: DrawingInteraction;
formatObservable?: Observable<ChartOptionsModel>;
resetTriggers?: Observable<unknown>[];
removeSelf?: () => void;
@@ -67,6 +67,7 @@ interface SliderPositionParams {
interface SliderPositionState {
hidden: boolean;
+ active: boolean;
mode: SliderMode;
startTime: Time | null;
endTime: Time | null;
@@ -123,7 +124,6 @@ export class SliderPosition extends SeriesDrawingBase<SliderPositionSettings> im
};
protected mode: SliderMode = 'idle';
-
private side: SliderSide;
private startTime: Time | null = null;
@@ -136,41 +136,30 @@ export class SliderPosition extends SeriesDrawingBase<SliderPositionSettings> im
private dragPointerId: number | null = null;
private dragStartPoint: Point | null = null;
private dragStateSnapshot: SliderPositionState | null = null;
+ private didDrag = false;
private defaultRiskRewardRatio = 1;
private amount = 1000;
private tickSize = 1;
- private clickHandler: MouseEventHandler<Time>;
- private paneView: SliderPaneView;
- private timeAxisPaneView: CustomTimeAxisPaneView;
- private priceAxisPaneView: CustomPriceAxisPaneView;
- private startTimeAxisView: CustomTimeAxisView;
- private endTimeAxisView: CustomTimeAxisView;
- private targetPriceAxisView: CustomPriceAxisView;
- private entryPriceAxisView: CustomPriceAxisView;
- private stopPriceAxisView: CustomPriceAxisView;
+ private ignoreNextChartClick = false;
+
+ 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;
constructor(
chart: IChartApi,
series: SeriesApi,
- {
- side,
- container,
- interaction,
- formatObservable,
- resetTriggers = [],
- removeSelf,
- openSettings,
- }: SliderPositionParams,
+ { side, container, formatObservable, resetTriggers = [], removeSelf, openSettings }: SliderPositionParams,
) {
- super({
- chart,
- series,
- container,
- interaction,
- });
-
+ super({ chart, series, container });
this.side = side;
this.removeSelf = removeSelf;
this.openSettings = openSettings;
@@ -239,6 +228,7 @@ export class SliderPosition extends SeriesDrawingBase<SliderPositionSettings> im
public getState(): SliderPositionState {
return {
hidden: this.hidden,
+ active: this.isActive.value,
mode: this.mode,
startTime: this.startTime,
endTime: this.endTime,
@@ -253,13 +243,12 @@ export class SliderPosition extends SeriesDrawingBase<SliderPositionSettings> im
}
public setState(state: unknown): void {
- if (!state || typeof state !== 'object') {
- return;
- }
-
const next = state as Partial<SliderPositionState>;
this.hidden = next.hidden ?? this.hidden;
+ if (typeof next.active === 'boolean') {
+ this.isActive.next(next.active);
+ }
this.mode = next.mode ?? this.mode;
this.startTime = next.startTime ?? this.startTime;
@@ -366,8 +355,8 @@ export class SliderPosition extends SeriesDrawingBase<SliderPositionSettings> im
stopText: this.getStopText(geometry),
centerBoxColor: pnl >= 0 ? colors.chartCandleUp : colors.chartCandleDown,
showFill: true,
- showHandles: this.shouldShowHandles(),
- showLabels: this.isSelected(),
+ showHandles: this.isActive.value,
+ showLabels: this.isActive.value,
...this.settings,
};
}
@@ -394,7 +383,6 @@ export class SliderPosition extends SeriesDrawingBase<SliderPositionSettings> im
}
const coordinate = getXCoordinateFromTime(this.chart, time, this.series);
-
return coordinate === null ? null : Number(coordinate);
}
@@ -451,10 +439,10 @@ export class SliderPosition extends SeriesDrawingBase<SliderPositionSettings> im
}
}
- protected getHoveredItem(x: number, y: number): PrimitiveHoveredItem | null {
+ public hitTest(x: number, y: number): PrimitiveHoveredItem | null {
const point = { x, y };
- if (!this.isSelected()) {
+ if (!this.isActive.value) {
if (!this.containsPoint(point)) {
return null;
}
@@ -490,7 +478,7 @@ export class SliderPosition extends SeriesDrawingBase<SliderPositionSettings> im
}
protected getTimeAxisSegments(): AxisSegment[] {
- if (!this.isSelected()) {
+ if (!this.isActive.value) {
return [];
}
@@ -512,7 +500,7 @@ export class SliderPosition extends SeriesDrawingBase<SliderPositionSettings> im
}
protected getPriceAxisSegments(): AxisSegment[] {
- if (!this.isSelected()) {
+ if (!this.isActive.value) {
return [];
}
@@ -539,7 +527,7 @@ export class SliderPosition extends SeriesDrawingBase<SliderPositionSettings> im
}
protected getTimeAxisLabel(kind: string): AxisLabel | null {
- if (!this.isSelected() || (kind !== 'start' && kind !== 'end')) {
+ if (!this.isActive.value || (kind !== 'start' && kind !== 'end')) {
return null;
}
@@ -595,29 +583,45 @@ export class SliderPosition extends SeriesDrawingBase<SliderPositionSettings> im
}
protected bindEvents(): void {
+ // todo: почему то отличается от базового класса
if (this.isBound) {
return;
}
- super.bindEvents();
+ this.isBound = true;
+
this.chart.subscribeClick(this.clickHandler);
+
+ this.container.addEventListener('dblclick', this.handleDoubleClick);
+ this.container.addEventListener('pointerdown', this.handlePointerDown);
+ window.addEventListener('pointermove', this.handlePointerMove);
+ window.addEventListener('pointerup', this.handlePointerUp);
+ window.addEventListener('pointercancel', this.handlePointerUp);
}
protected unbindEvents(): void {
+ // todo: почему то отличается от базового класса
if (!this.isBound) {
return;
}
+ this.isBound = false;
+
this.chart.unsubscribeClick(this.clickHandler);
- super.unbindEvents();
+
+ this.container.removeEventListener('dblclick', this.handleDoubleClick);
+ this.container.removeEventListener('pointerdown', this.handlePointerDown);
+ window.removeEventListener('pointermove', this.handlePointerMove);
+ window.removeEventListener('pointerup', this.handlePointerUp);
+ window.removeEventListener('pointercancel', this.handlePointerUp);
}
protected handleDoubleClick = (event: MouseEvent): void => {
- if (this.hidden || this.mode !== 'ready' || !this.isSelected()) {
+ if (this.hidden || this.mode !== 'ready' || !this.isActive.value) {
return;
}
- const point = this.getEventPoint(event as PointerEvent);
+ const point = this.getLocalPoint(event as PointerEvent);
if (!this.containsPoint(point)) {
return;
@@ -630,59 +634,57 @@ export class SliderPosition extends SeriesDrawingBase<SliderPositionSettings> im
};
private handleChartClick(params: MouseEventParams<Time>): void {
- if (this.hidden || !params.point || this.mode !== 'idle') {
+ if (this.ignoreNextChartClick) {
+ this.ignoreNextChartClick = false;
return;
}
- const anchor = this.createAnchor(params);
-
- if (!anchor) {
+ if (this.hidden || !params.point) {
return;
}
- const distance = this.getInitialZoneDistance(anchor.price);
- const stopDirection = this.side === 'long' ? -1 : 1;
- const targetDirection = -stopDirection;
+ if (this.mode === 'idle') {
+ const anchor = this.createAnchor(params);
- 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, distance);
- this.targetPrice = this.normalizeTarget(
- anchor.price,
- anchor.price + distance * targetDirection * this.defaultRiskRewardRatio,
- distance,
- );
+ if (!anchor) {
+ return;
+ }
- this.mode = 'ready';
- this.resolveReady?.();
+ const distance = this.getInitialZoneDistance(anchor.price);
+ 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, distance);
+ this.targetPrice = this.normalizeTarget(
+ anchor.price,
+ anchor.price + distance * targetDirection * this.defaultRiskRewardRatio,
+ distance,
+ );
- this.render();
- }
+ this.isActive.next(true);
+ this.mode = 'ready';
+ this.resolveReady?.();
- protected handlePointerDown = (event: PointerEvent): void => {
- if (this.hidden || this.mode !== 'ready' || event.button !== 0) {
+ this.render();
return;
}
- const point = this.getEventPoint(event);
-
- if (!this.isSelected()) {
- if (!this.containsPoint(point)) {
- return;
- }
-
- event.preventDefault();
- event.stopPropagation();
+ this.isActive.next(this.containsPoint({ x: params.point.x, y: params.point.y }));
+ this.render();
+ }
- this.select();
+ protected handlePointerDown = (event: PointerEvent): void => {
+ if (this.hidden || this.mode !== 'ready' || !this.isActive.value) {
return;
}
+ const point = this.getLocalPoint(event);
const dragTarget = this.getHandleTarget(point);
if (!dragTarget) {
- this.deselect();
return;
}
@@ -693,6 +695,7 @@ export class SliderPosition extends SeriesDrawingBase<SliderPositionSettings> im
this.dragPointerId = event.pointerId;
this.dragStartPoint = point;
this.dragStateSnapshot = this.getState();
+ this.didDrag = false;
this.mode = 'dragging';
};
@@ -703,7 +706,8 @@ export class SliderPosition extends SeriesDrawingBase<SliderPositionSettings> im
event.preventDefault();
- this.applyDrag(this.getEventPoint(event));
+ this.didDrag = true;
+ this.applyDrag(this.getLocalPoint(event));
this.render();
};
@@ -712,11 +716,13 @@ export class SliderPosition extends SeriesDrawingBase<SliderPositionSettings> im
return;
}
+ this.ignoreNextChartClick = this.didDrag;
+
this.activeDragTarget = null;
this.dragPointerId = null;
this.dragStartPoint = null;
this.dragStateSnapshot = null;
-
+ this.didDrag = false;
this.mode = 'ready';
this.resolveReady?.();
@@ -808,7 +814,6 @@ export class SliderPosition extends SeriesDrawingBase<SliderPositionSettings> im
if (minValue < range.min) {
const shift = range.min - minValue;
-
nextEntryPrice += shift;
nextStopPrice += shift;
nextTargetPrice += shift;
@@ -816,7 +821,6 @@ export class SliderPosition extends SeriesDrawingBase<SliderPositionSettings> im
if (maxValue > range.max) {
const shift = maxValue - range.max;
-
nextEntryPrice -= shift;
nextStopPrice -= shift;
nextTargetPrice -= shift;
@@ -1148,4 +1152,8 @@ export class SliderPosition extends SeriesDrawingBase<SliderPositionSettings> im
return isPointInBounds(point, bounds, HIT_TOLERANCE);
}
+
+ private getLocalPoint(event: PointerEvent): Point {
+ return getPointerPointFromEvent(this.container, event);
+ }
}
diff --git a/src/core/Drawings/text/paneRenderer.ts b/src/core/Drawings/text/paneRenderer.ts
index 5576be4..3f196b6 100644
--- a/src/core/Drawings/text/paneRenderer.ts
+++ b/src/core/Drawings/text/paneRenderer.ts
@@ -6,10 +6,11 @@ import { getThemeStore } from '@src/theme';
import { Text } from './text';
const UI = {
+ handleRadius: 5,
+ handleBorderWidth: 2,
borderWidth: 1,
borderRadius: 4,
padding: 6,
- selectionBorderWidth: 1,
};
export class TextPaneRenderer implements IPrimitivePaneRenderer {
@@ -28,22 +29,25 @@ export class TextPaneRenderer implements IPrimitivePaneRenderer {
target.useBitmapCoordinateSpace(({ context, horizontalPixelRatio, verticalPixelRatio }) => {
const pixelRatio = Math.max(horizontalPixelRatio, verticalPixelRatio);
+ const handleRadius = UI.handleRadius * pixelRatio;
+ const handleBorderWidth = UI.handleBorderWidth * pixelRatio;
+ const borderWidth = UI.borderWidth * pixelRatio;
+
const left = data.left * horizontalPixelRatio;
const top = data.top * verticalPixelRatio;
const width = data.width * horizontalPixelRatio;
const height = data.height * verticalPixelRatio;
const paddingX = UI.padding * horizontalPixelRatio;
const paddingY = UI.padding * verticalPixelRatio;
- const borderRadius = UI.borderRadius * pixelRatio;
context.save();
context.fillStyle = data.backgroundColor;
- fillRoundedRect(context, left, top, width, height, borderRadius);
+ fillRoundedRect(context, left, top, width, height, UI.borderRadius * pixelRatio);
context.strokeStyle = data.borderColor;
- context.lineWidth = UI.borderWidth * pixelRatio;
- strokeRoundedRect(context, left, top, width, height, borderRadius);
+ context.lineWidth = borderWidth;
+ strokeRoundedRect(context, left, top, width, height, UI.borderRadius * pixelRatio);
context.save();
context.beginPath();
@@ -61,8 +65,15 @@ export class TextPaneRenderer implements IPrimitivePaneRenderer {
context.restore();
- if (data.showSelectionBorder) {
- drawSelectionBorder(context, left, top, width, height, borderRadius, pixelRatio);
+ if (data.showHandles) {
+ const { colors } = getThemeStore();
+
+ context.save();
+ context.fillStyle = colors.chartBackground;
+ context.strokeStyle = colors.chartLineColor;
+ context.lineWidth = handleBorderWidth;
+ drawHandle(context, left, top, handleRadius);
+ context.restore();
}
context.restore();
@@ -70,24 +81,11 @@ export class TextPaneRenderer implements IPrimitivePaneRenderer {
}
}
-function drawSelectionBorder(
- context: CanvasRenderingContext2D,
- x: number,
- y: number,
- width: number,
- height: number,
- radius: number,
- pixelRatio: number,
-): void {
- const { colors } = getThemeStore();
-
- context.save();
- context.strokeStyle = colors.chartLineColor;
- context.lineWidth = UI.selectionBorderWidth * pixelRatio;
-
- strokeRoundedRect(context, x, y, width, height, radius);
-
- context.restore();
+function drawHandle(context: CanvasRenderingContext2D, x: number, y: number, radius: number): void {
+ context.beginPath();
+ context.arc(x, y, radius, 0, Math.PI * 2);
+ context.fill();
+ context.stroke();
}
function fillRoundedRect(
diff --git a/src/core/Drawings/text/settings.ts b/src/core/Drawings/text/settings.ts
index f99b540..ae6fb84 100644
--- a/src/core/Drawings/text/settings.ts
+++ b/src/core/Drawings/text/settings.ts
@@ -1,7 +1,7 @@
import { getThemeStore } from '@src/theme';
import { t } from '@src/translations';
-import type { SettingField, SettingsTab, SettingsValues } from '@src/types';
+import { SettingField, SettingsTab, SettingsValues } from '@src/types';
export interface TextStyle {
backgroundColor: string;
@@ -35,16 +35,16 @@ export function createDefaultSettings(): TextSettings {
export function getTextSettingsTabs(settings: TextSettings): SettingsTab[] {
const styleFields: SettingField[] = [
{
- key: 'borderColor',
- label: t('Border'),
+ key: 'backgroundColor',
+ label: t('Background color'),
type: 'color',
- defaultValue: settings.borderColor,
+ defaultValue: settings.backgroundColor,
},
{
- key: 'backgroundColor',
- label: t('Background'),
+ key: 'borderColor',
+ label: t('Border color'),
type: 'color',
- defaultValue: settings.backgroundColor,
+ defaultValue: settings.borderColor,
},
];
@@ -72,19 +72,15 @@ export function getTextSettingsTabs(settings: TextSettings): SettingsTab[] {
},
{
key: 'isItalic',
- label: t('Italic'),
+ label: t('Italics'),
type: 'boolean',
defaultValue: settings.isItalic,
},
{
key: 'textColor',
- label: t('Text'),
+ label: t('Text color'),
type: 'color',
defaultValue: settings.textColor,
- toolbar: {
- control: 'color',
- role: 'text',
- },
},
];
diff --git a/src/core/Drawings/text/text.ts b/src/core/Drawings/text/text.ts
index 9cab451..428dad1 100644
--- a/src/core/Drawings/text/text.ts
+++ b/src/core/Drawings/text/text.ts
@@ -1,10 +1,10 @@
import { IChartApi, IPrimitivePaneView, PrimitiveHoveredItem, UTCTimestamp } from 'lightweight-charts';
-import { clamp } from 'lodash-es';
import { Observable } from 'rxjs';
import { CustomPriceAxisView, CustomTimeAxisView } from '@core/Drawings/axis';
import { SeriesDrawingBase } from '@core/Drawings/common';
import {
+ clamp,
clampPointToContainer as clampPointToContainerInElement,
getAnchorFromPoint,
getContainerSize as getElementContainerSize,
@@ -17,13 +17,15 @@ import { updateViews } from '@core/Drawings/utils';
import { getThemeStore } from '@src/theme';
import { SettingsTab } from '@src/types';
+
import { Defaults } from '@src/types/defaults';
+
import { formatDate, formatPrice } from '@src/utils';
import { TextPaneView } from './paneView';
import { createDefaultSettings, getTextSettingsTabs, TextContentStyle, TextSettings, TextStyle } from './settings';
-import type { DrawingInteraction, ISeriesDrawing } from '@core/Drawings/common';
+import type { ISeriesDrawing } from '@core/Drawings/common';
import type { Anchor, AxisLabel, Point, SeriesApi } from '@core/Drawings/types';
import type { ChartOptionsModel } from '@src/types';
@@ -31,6 +33,7 @@ type TextMode = 'idle' | 'dragging' | 'ready';
export interface TextState {
hidden: boolean;
+ isActive: boolean;
mode: TextMode;
point: Anchor | null;
settings: TextSettings;
@@ -50,12 +53,11 @@ interface TextGeometry {
}
export interface TextRenderData extends TextGeometry, TextStyle, TextContentStyle {
- showSelectionBorder: boolean;
+ showHandles: boolean;
}
interface TextParams {
container: HTMLElement;
- interaction: DrawingInteraction;
formatObservable?: Observable<ChartOptionsModel>;
removeSelf?: () => void;
openSettings?: () => void;
@@ -63,6 +65,7 @@ interface TextParams {
const UI = {
padding: 6,
+ lineHeightMultiplier: 1.2,
};
let measureCanvas: HTMLCanvasElement | null = null;
@@ -91,13 +94,7 @@ export class Text extends SeriesDrawingBase<TextSettings> implements ISeriesDraw
private readonly priceAxisView: CustomPriceAxisView;
constructor(chart: IChartApi, series: SeriesApi, params: TextParams) {
- super({
- chart,
- series,
- container: params.container,
- interaction: params.interaction,
- });
-
+ super({ chart, series, container: params.container });
const { formatObservable, removeSelf, openSettings } = params;
this.removeSelf = removeSelf;
@@ -138,6 +135,7 @@ export class Text extends SeriesDrawingBase<TextSettings> implements ISeriesDraw
public getState(): TextState {
return {
hidden: this.hidden,
+ isActive: this.isActive.value,
mode: this.mode,
point: this.point,
settings: { ...this.settings },
@@ -152,6 +150,9 @@ export class Text extends SeriesDrawingBase<TextSettings> implements ISeriesDraw
const nextState = state as Partial<TextState>;
this.hidden = typeof nextState.hidden === 'boolean' ? nextState.hidden : this.hidden;
+ if (typeof nextState.isActive === 'boolean') {
+ this.isActive.next(nextState.isActive);
+ }
this.mode = nextState.mode ?? this.mode;
this.point = nextState.point ?? this.point;
@@ -203,11 +204,11 @@ export class Text extends SeriesDrawingBase<TextSettings> implements ISeriesDraw
return {
...geometry,
...this.settings,
- showSelectionBorder: this.isSelected(),
+ showHandles: this.isActive.value,
};
}
- protected getHoveredItem(x: number, y: number): PrimitiveHoveredItem | null {
+ public hitTest(x: number, y: number): PrimitiveHoveredItem | null {
if (this.hidden || this.mode === 'idle') {
return null;
}
@@ -224,7 +225,7 @@ 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 (kind !== 'main' || !this.isActive.value || !this.point || typeof this.point.time !== 'number') {
return null;
}
@@ -250,7 +251,7 @@ export class Text extends SeriesDrawingBase<TextSettings> implements ISeriesDraw
}
protected getPriceAxisLabel(kind: string): AxisLabel | null {
- if (kind !== 'main' || !this.isSelected() || !this.point) {
+ if (kind !== 'main' || !this.isActive.value || !this.point) {
return null;
}
@@ -299,7 +300,7 @@ export class Text extends SeriesDrawingBase<TextSettings> implements ISeriesDraw
const containsPoint = this.containsPoint(point);
- if (!this.isSelected()) {
+ if (!this.isActive.value) {
if (!containsPoint) {
return;
}
@@ -307,7 +308,8 @@ export class Text extends SeriesDrawingBase<TextSettings> implements ISeriesDraw
event.preventDefault();
event.stopPropagation();
- this.select();
+ this.isActive.next(true);
+ this.render();
return;
}
@@ -319,11 +321,12 @@ export class Text extends SeriesDrawingBase<TextSettings> implements ISeriesDraw
return;
}
- this.deselect();
+ this.isActive.next(false);
+ this.render();
};
protected handleDoubleClick = (event: MouseEvent): void => {
- if (this.hidden || this.mode !== 'ready' || !this.isSelected()) {
+ if (this.hidden || this.mode !== 'ready' || !this.isActive.value) {
return;
}
@@ -368,6 +371,7 @@ export class Text extends SeriesDrawingBase<TextSettings> implements ISeriesDraw
}
this.point = anchor;
+ this.isActive.next(true);
this.mode = 'ready';
this.resolveReady?.();
@@ -412,10 +416,7 @@ export class Text extends SeriesDrawingBase<TextSettings> implements ISeriesDraw
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({
- x: nextLeft,
- y: nextTop,
- });
+ const anchor = this.createAnchor({ x: nextLeft, y: nextTop });
if (!anchor) {
return;
@@ -449,7 +450,7 @@ export class Text extends SeriesDrawingBase<TextSettings> implements ISeriesDraw
const lines = getTextLines(this.settings.text);
const font = getFont(this.settings);
- const lineHeight = this.settings.fontSize;
+ const lineHeight = Math.round(this.settings.fontSize * UI.lineHeightMultiplier);
const measured = measureTextBlock(lines, font, lineHeight);
const width = Math.min(containerWidth, measured.width + UI.padding * 2);
diff --git a/src/core/Drawings/traectory/settings.ts b/src/core/Drawings/traectory/settings.ts
index 99323e2..61949c4 100644
--- a/src/core/Drawings/traectory/settings.ts
+++ b/src/core/Drawings/traectory/settings.ts
@@ -20,13 +20,9 @@ export function getTraectorySettingsTabs(settings: TraectorySettings): SettingsT
const fields: SettingField[] = [
{
key: 'lineColor',
- label: t('Line'),
+ label: t('Line color'),
type: 'color',
defaultValue: settings.lineColor,
- toolbar: {
- control: 'color',
- role: 'line',
- },
},
];
diff --git a/src/core/Drawings/traectory/traectory.ts b/src/core/Drawings/traectory/traectory.ts
index 54133aa..fd458ae 100644
--- a/src/core/Drawings/traectory/traectory.ts
+++ b/src/core/Drawings/traectory/traectory.ts
@@ -1,10 +1,10 @@
import { IChartApi, IPrimitivePaneView, PrimitiveHoveredItem } from 'lightweight-charts';
-import { clamp } from 'lodash-es';
import { Observable } from 'rxjs';
import { CustomPriceAxisPaneView, CustomTimeAxisPaneView } from '@core/Drawings/axis';
import { SeriesDrawingBase } from '@core/Drawings/common';
import {
+ clamp,
clampPointToContainer as clampPointToContainerInElement,
getAnchorFromPoint,
getContainerSize as getElementContainerSize,
@@ -21,7 +21,7 @@ import { TraectoryPaneView } from './paneView';
import { createDefaultSettings, getTraectorySettingsTabs, TraectorySettings, TraectoryStyle } from './settings';
-import type { DrawingInteraction, ISeriesDrawing } from '@core/Drawings/common';
+import type { ISeriesDrawing } from '@core/Drawings/common';
import type { Anchor, AxisSegment, Point, SeriesApi } from '@core/Drawings/types';
import type { ChartOptionsModel, SettingsTab } from '@src/types';
@@ -29,7 +29,6 @@ type TraectoryMode = 'idle' | 'drawing' | 'ready' | 'dragging-point' | 'dragging
interface TraectoryParams {
container: HTMLElement;
- interaction: DrawingInteraction;
formatObservable?: Observable<ChartOptionsModel>;
removeSelf?: () => void;
openSettings?: () => void;
@@ -37,6 +36,7 @@ interface TraectoryParams {
export interface TraectoryState {
hidden: boolean;
+ isActive: boolean;
mode: TraectoryMode;
points: Anchor[];
settings: TraectorySettings;
@@ -81,13 +81,7 @@ export class Traectory extends SeriesDrawingBase<TraectorySettings> implements I
private readonly priceAxisPaneView: CustomPriceAxisPaneView;
constructor(chart: IChartApi, series: SeriesApi, params: TraectoryParams) {
- super({
- chart,
- series,
- container: params.container,
- interaction: params.interaction,
- });
-
+ super({ chart, series, container: params.container });
const { removeSelf, openSettings } = params;
this.removeSelf = removeSelf;
@@ -113,6 +107,7 @@ export class Traectory extends SeriesDrawingBase<TraectorySettings> implements I
public getState(): TraectoryState {
return {
hidden: this.hidden,
+ isActive: this.isActive.value,
mode: this.mode,
points: this.points,
settings: { ...this.settings },
@@ -127,7 +122,11 @@ export class Traectory extends SeriesDrawingBase<TraectorySettings> implements I
const nextState = state as Partial<TraectoryState>;
this.hidden = typeof nextState.hidden === 'boolean' ? nextState.hidden : this.hidden;
+ if (typeof nextState.isActive === 'boolean') {
+ this.isActive.next(nextState.isActive);
+ }
this.mode = nextState.mode ?? this.mode;
+
this.points = Array.isArray(nextState.points) ? nextState.points : this.points;
if ('settings' in nextState && nextState.settings) {
@@ -182,13 +181,13 @@ export class Traectory extends SeriesDrawingBase<TraectorySettings> implements I
return {
...geometry,
previewPoint: this.getPreviewPoint(),
- showHandles: this.shouldShowHandles(),
+ showHandles: this.mode === 'drawing' || this.isActive.value,
showArrow: this.mode !== 'drawing' && geometry.points.length > 1,
...this.settings,
};
}
- protected getHoveredItem(x: number, y: number): PrimitiveHoveredItem | null {
+ public hitTest(x: number, y: number): PrimitiveHoveredItem | null {
if (this.hidden || this.mode === 'idle' || this.mode === 'drawing') {
return null;
}
@@ -216,7 +215,7 @@ export class Traectory extends SeriesDrawingBase<TraectorySettings> implements I
}
protected getTimeAxisSegments(): AxisSegment[] {
- if (!this.isSelected() && !this.isCreationPending()) {
+ if (!this.isActive.value) {
return [];
}
@@ -238,7 +237,7 @@ export class Traectory extends SeriesDrawingBase<TraectorySettings> implements I
}
protected getPriceAxisSegments(): AxisSegment[] {
- if (!this.isSelected() && !this.isCreationPending()) {
+ if (!this.isActive.value) {
return [];
}
@@ -283,7 +282,7 @@ export class Traectory extends SeriesDrawingBase<TraectorySettings> implements I
return;
}
- if (this.mode !== 'ready' || !this.isSelected()) {
+ if (this.mode !== 'ready' || !this.isActive.value) {
return;
}
@@ -344,7 +343,7 @@ export class Traectory extends SeriesDrawingBase<TraectorySettings> implements I
const pointIndex = this.getPointIndexAt(point);
const isInsideTraectory = pointIndex !== null || this.isPointNearTraectory(point);
- if (!this.isSelected()) {
+ if (!this.isActive.value) {
if (!isInsideTraectory) {
return;
}
@@ -352,7 +351,8 @@ export class Traectory extends SeriesDrawingBase<TraectorySettings> implements I
event.preventDefault();
event.stopPropagation();
- this.select();
+ this.isActive.next(true);
+ this.render();
return;
}
@@ -365,7 +365,8 @@ export class Traectory extends SeriesDrawingBase<TraectorySettings> implements I
}
if (!this.isPointNearTraectory(point)) {
- this.deselect();
+ this.isActive.next(false);
+ this.render();
return;
}
@@ -420,6 +421,7 @@ export class Traectory extends SeriesDrawingBase<TraectorySettings> implements I
this.points = [anchor];
this.previewAnchor = anchor;
+ this.isActive.next(true);
this.mode = 'drawing';
this.render();
@@ -468,6 +470,7 @@ export class Traectory extends SeriesDrawingBase<TraectorySettings> implements I
}
this.previewAnchor = null;
+ this.isActive.next(true);
this.mode = 'ready';
this.resolveReady?.();
@@ -512,6 +515,7 @@ export class Traectory extends SeriesDrawingBase<TraectorySettings> implements I
}
private resetToIdle(): void {
+ this.isActive.next(false);
this.mode = 'idle';
this.points = [];
diff --git a/src/core/Drawings/trendLine/settings.ts b/src/core/Drawings/trendLine/settings.ts
index b615319..31393a9 100644
--- a/src/core/Drawings/trendLine/settings.ts
+++ b/src/core/Drawings/trendLine/settings.ts
@@ -34,13 +34,9 @@ export function getTrendLineSettingsTabs(settings: TrendLineSettings): SettingsT
const styleFields: SettingField[] = [
{
key: 'lineColor',
- label: t('Line'),
+ label: t('Line color'),
type: 'color',
defaultValue: settings.lineColor,
- toolbar: {
- control: 'color',
- role: 'line',
- },
},
];
@@ -68,19 +64,15 @@ export function getTrendLineSettingsTabs(settings: TrendLineSettings): SettingsT
},
{
key: 'isItalic',
- label: t('Italic'),
+ label: t('Italics'),
type: 'boolean',
defaultValue: settings.isItalic,
},
{
key: 'textColor',
- label: t('Text'),
+ label: t('Text color'),
type: 'color',
defaultValue: settings.textColor,
- toolbar: {
- control: 'color',
- role: 'text',
- },
},
];
diff --git a/src/core/Drawings/trendLine/trendLine.ts b/src/core/Drawings/trendLine/trendLine.ts
index f6a6c78..fc3663c 100644
--- a/src/core/Drawings/trendLine/trendLine.ts
+++ b/src/core/Drawings/trendLine/trendLine.ts
@@ -33,7 +33,7 @@ import {
TrendLineTextStyle,
} from './settings';
-import type { DrawingInteraction, ISeriesDrawing } from '@core/Drawings/common';
+import type { ISeriesDrawing } from '@core/Drawings/common';
import type { Anchor, AxisLabel, AxisSegment, Point, SeriesApi } from '@core/Drawings/types';
import type { ChartOptionsModel, SettingsTab } from '@src/types';
@@ -43,7 +43,6 @@ type PriceLabelKind = 'start' | 'end';
interface TrendLineParams {
container: HTMLElement;
- interaction: DrawingInteraction;
formatObservable?: Observable<ChartOptionsModel>;
removeSelf?: () => void;
openSettings?: () => void;
@@ -51,6 +50,7 @@ interface TrendLineParams {
interface TrendLineState {
hidden: boolean;
+ isActive: boolean;
mode: TrendLineMode;
startAnchor: Anchor | null;
endAnchor: Anchor | null;
@@ -78,6 +78,7 @@ export class TrendLine extends SeriesDrawingBase<TrendLineSettings> implements I
private openSettings?: () => void;
protected settings: TrendLineSettings = createDefaultSettings();
+
protected mode: TrendLineMode = 'idle';
private startAnchor: Anchor | null = null;
@@ -104,10 +105,9 @@ export class TrendLine extends SeriesDrawingBase<TrendLineSettings> implements I
constructor(
chart: IChartApi,
series: SeriesApi,
- { container, interaction, formatObservable, removeSelf, openSettings }: TrendLineParams,
+ { container, formatObservable, removeSelf, openSettings }: TrendLineParams,
) {
- super({ chart, series, container, interaction });
-
+ super({ chart, series, container });
this.removeSelf = removeSelf;
this.openSettings = openSettings;
@@ -160,6 +160,7 @@ export class TrendLine extends SeriesDrawingBase<TrendLineSettings> implements I
public getState(): TrendLineState {
return {
hidden: this.hidden,
+ isActive: this.isActive.value,
mode: this.mode,
startAnchor: this.startAnchor,
endAnchor: this.endAnchor,
@@ -168,16 +169,16 @@ export class TrendLine extends SeriesDrawingBase<TrendLineSettings> implements I
}
public setState(state: unknown): void {
- if (!state || typeof state !== 'object') {
- return;
- }
-
const nextState = state as Partial<TrendLineState>;
if ('hidden' in nextState && typeof nextState.hidden === 'boolean') {
this.hidden = nextState.hidden;
}
+ if ('isActive' in nextState && typeof nextState.isActive === 'boolean') {
+ this.isActive.next(nextState.isActive);
+ }
+
if ('mode' in nextState && nextState.mode) {
this.mode = nextState.mode;
}
@@ -249,12 +250,12 @@ export class TrendLine extends SeriesDrawingBase<TrendLineSettings> implements I
return {
...geometry,
- showHandles: this.shouldShowHandles(),
+ showHandles: this.isActive.value,
...this.settings,
};
}
- protected getHoveredItem(x: number, y: number): PrimitiveHoveredItem | null {
+ public hitTest(x: number, y: number): PrimitiveHoveredItem | null {
if (this.hidden || this.mode === 'idle' || this.mode === 'drawing') {
return null;
}
@@ -281,7 +282,7 @@ export class TrendLine extends SeriesDrawingBase<TrendLineSettings> implements I
}
protected getTimeAxisSegments(): AxisSegment[] {
- if (!this.isSelected() && !this.isCreationPending()) {
+ if (!this.isActive.value) {
return [];
}
@@ -303,7 +304,7 @@ export class TrendLine extends SeriesDrawingBase<TrendLineSettings> implements I
}
protected getPriceAxisSegments(): AxisSegment[] {
- if (!this.isSelected() && !this.isCreationPending()) {
+ if (!this.isActive.value) {
return [];
}
@@ -325,7 +326,7 @@ export class TrendLine extends SeriesDrawingBase<TrendLineSettings> implements I
}
protected getTimeAxisLabel(kind: string): AxisLabel | null {
- if ((!this.isSelected() && !this.isCreationPending()) || (kind !== 'start' && kind !== 'end')) {
+ if (!this.isActive.value || (kind !== 'start' && kind !== 'end')) {
return null;
}
@@ -347,7 +348,7 @@ export class TrendLine extends SeriesDrawingBase<TrendLineSettings> implements I
}
protected getPriceAxisLabel(kind: string): AxisLabel | null {
- if ((!this.isSelected() && !this.isCreationPending()) || (kind !== 'start' && kind !== 'end')) {
+ if (!this.isActive.value || (kind !== 'start' && kind !== 'end')) {
return null;
}
@@ -418,18 +419,17 @@ export class TrendLine extends SeriesDrawingBase<TrendLineSettings> implements I
}
const pointTarget = this.getPointTarget(point);
- const isNearLine = this.isPointNearLine(point);
- const isDrawingHit = pointTarget !== null || isNearLine;
- if (!this.isSelected()) {
- if (!isDrawingHit) {
+ if (!this.isActive.value) {
+ if (!pointTarget && !this.isPointNearLine(point)) {
return;
}
event.preventDefault();
event.stopPropagation();
- this.select();
+ this.isActive.next(true);
+ this.render();
return;
}
@@ -449,7 +449,7 @@ export class TrendLine extends SeriesDrawingBase<TrendLineSettings> implements I
return;
}
- if (isNearLine) {
+ if (this.isPointNearLine(point)) {
event.preventDefault();
event.stopPropagation();
@@ -457,7 +457,8 @@ export class TrendLine extends SeriesDrawingBase<TrendLineSettings> implements I
return;
}
- this.deselect();
+ this.isActive.next(false);
+ this.render();
};
protected handlePointerMove = (event: PointerEvent): void => {
@@ -509,6 +510,7 @@ export class TrendLine extends SeriesDrawingBase<TrendLineSettings> implements I
this.startAnchor = anchor;
this.endAnchor = anchor;
+ this.isActive.next(true);
this.mode = 'drawing';
this.render();
diff --git a/src/core/Drawings/volumeProfile/settings.ts b/src/core/Drawings/volumeProfile/settings.ts
index 0af1f69..ab8289b 100644
--- a/src/core/Drawings/volumeProfile/settings.ts
+++ b/src/core/Drawings/volumeProfile/settings.ts
@@ -26,25 +26,25 @@ export function getVolumeProfileSettingsTabs(settings: VolumeProfileSettings): S
const fields: SettingField[] = [
{
key: 'areaFillColor',
- label: t('Background'),
+ label: t('Area color'),
type: 'color',
defaultValue: settings.areaFillColor,
},
{
key: 'buyFillColor',
- label: t('Growing volume'),
+ label: t('Growing volume color'),
type: 'color',
defaultValue: settings.buyFillColor,
},
{
key: 'sellFillColor',
- label: t('Decreasing volume'),
+ label: t('Decreasing volume color'),
type: 'color',
defaultValue: settings.sellFillColor,
},
{
key: 'pocLineColor',
- label: t('POC-line'),
+ label: t('POC-line color'),
type: 'color',
defaultValue: settings.pocLineColor,
},
diff --git a/src/core/Drawings/volumeProfile/volumeProfile.ts b/src/core/Drawings/volumeProfile/volumeProfile.ts
index d254f7d..54240b8 100644
--- a/src/core/Drawings/volumeProfile/volumeProfile.ts
+++ b/src/core/Drawings/volumeProfile/volumeProfile.ts
@@ -1,5 +1,5 @@
import { IChartApi, IPrimitivePaneView, PrimitiveHoveredItem, Time, UTCTimestamp } from 'lightweight-charts';
-import { clamp } from 'lodash-es';
+
import { Observable } from 'rxjs';
import {
@@ -10,6 +10,7 @@ import {
} from '@core/Drawings/axis';
import { SeriesDrawingBase } from '@core/Drawings/common';
import {
+ clamp,
clampPointToContainer as clampPointToContainerInElement,
getAnchorFromPoint,
getContainerSize as getElementContainerSize,
@@ -19,8 +20,8 @@ import {
isPointInBounds,
} from '@core/Drawings/helpers';
import { updateViews } from '@core/Drawings/utils';
-
import { getThemeStore } from '@src/theme';
+
import { SettingsTab } from '@src/types';
import { Defaults } from '@src/types/defaults';
import { formatPrice } from '@src/utils';
@@ -35,7 +36,7 @@ import {
VolumeProfileStyle,
} from './settings';
-import type { DrawingInteraction, ISeriesDrawing } from '@core/Drawings/common';
+import type { ISeriesDrawing } from '@core/Drawings/common';
import type { Anchor, AxisLabel, AxisSegment, Bounds, Point, SeriesApi } from '@core/Drawings/types';
import type { ChartOptionsModel } from '@src/types';
@@ -46,7 +47,6 @@ type DragTarget = 'body' | 'poc' | 'start' | 'end' | null;
interface VolumeProfileParams {
container: HTMLElement;
- interaction: DrawingInteraction;
profileKind?: VolumeProfileKind;
formatObservable?: Observable<ChartOptionsModel>;
removeSelf?: () => void;
@@ -73,6 +73,7 @@ interface SeriesCandleData {
export interface VolumeProfileState {
hidden: boolean;
+ isActive: boolean;
mode: VolumeProfileMode;
startAnchor: Anchor | null;
endAnchor: Anchor | null;
@@ -121,6 +122,7 @@ export class VolumeProfile extends SeriesDrawingBase<VolumeProfileSettings> impl
private readonly profileKind: VolumeProfileKind;
protected settings: VolumeProfileSettings = createDefaultSettings();
+
protected mode: VolumeProfileMode = 'idle';
private startAnchor: Anchor | null = null;
@@ -154,23 +156,16 @@ export class VolumeProfile extends SeriesDrawingBase<VolumeProfileSettings> impl
constructor(
chart: IChartApi,
series: SeriesApi,
- {
- container,
- interaction,
- profileKind = 'fixedRange',
- formatObservable,
- removeSelf,
- openSettings,
- }: VolumeProfileParams,
+ { container, profileKind = 'fixedRange', formatObservable, removeSelf, openSettings }: VolumeProfileParams,
) {
- super({ chart, series, container, interaction });
-
+ super({ chart, series, container });
this.profileKind = profileKind;
this.removeSelf = removeSelf;
this.openSettings = openSettings;
if (this.profileKind === 'visibleRange') {
this.mode = 'ready';
+ this.isActive.next(true);
}
this.paneView = new VolumeProfilePaneView(this);
@@ -224,7 +219,6 @@ export class VolumeProfile extends SeriesDrawingBase<VolumeProfileSettings> impl
if (this.profileKind === 'visibleRange') {
this.chart.timeScale().unsubscribeVisibleLogicalRangeChange(this.handleVisibleLogicalRangeChange);
}
-
super.destroy();
}
@@ -247,6 +241,7 @@ export class VolumeProfile extends SeriesDrawingBase<VolumeProfileSettings> impl
public getState(): VolumeProfileState {
return {
hidden: this.hidden,
+ isActive: this.isActive.value,
mode: this.mode,
startAnchor: this.startAnchor,
endAnchor: this.endAnchor,
@@ -263,6 +258,9 @@ export class VolumeProfile extends SeriesDrawingBase<VolumeProfileSettings> impl
const nextState = state as Partial<VolumeProfileState>;
this.hidden = typeof nextState.hidden === 'boolean' ? nextState.hidden : this.hidden;
+ if (typeof nextState.isActive === 'boolean') {
+ this.isActive.next(nextState.isActive);
+ }
if (this.profileKind === 'fixedRange') {
this.mode = nextState.mode ?? this.mode;
@@ -360,13 +358,13 @@ export class VolumeProfile extends SeriesDrawingBase<VolumeProfileSettings> impl
profileKind: this.profileKind,
rows,
pocY,
- showHandles: this.shouldShowHandles(),
+ showHandles: this.isActive.value || this.mode === 'drawing',
...this.settings,
};
}
protected getTimeAxisSegments(): AxisSegment[] {
- if (this.profileKind === 'visibleRange' || (!this.isSelected() && !this.isCreationPending())) {
+ if (this.profileKind === 'visibleRange' || !this.isActive.value) {
return [];
}
@@ -388,7 +386,7 @@ export class VolumeProfile extends SeriesDrawingBase<VolumeProfileSettings> impl
}
protected getPriceAxisSegments(): AxisSegment[] {
- if (this.profileKind === 'visibleRange' || (!this.isSelected() && !this.isCreationPending())) {
+ if (this.profileKind === 'visibleRange' || !this.isActive.value) {
return [];
}
@@ -409,7 +407,7 @@ export class VolumeProfile extends SeriesDrawingBase<VolumeProfileSettings> impl
];
}
- protected getHoveredItem(x: number, y: number): PrimitiveHoveredItem | null {
+ public hitTest(x: number, y: number): PrimitiveHoveredItem | null {
if (this.hidden || this.mode === 'idle' || this.mode === 'drawing') {
return null;
}
@@ -429,7 +427,7 @@ export class VolumeProfile extends SeriesDrawingBase<VolumeProfileSettings> impl
}
return {
- cursorStyle: this.isSelected() ? 'grab' : 'pointer',
+ cursorStyle: this.isActive.value ? 'grab' : 'pointer',
externalId: 'volume-profile',
zOrder: 'top',
};
@@ -440,7 +438,7 @@ export class VolumeProfile extends SeriesDrawingBase<VolumeProfileSettings> impl
return null;
}
- if ((!this.isSelected() && !this.isCreationPending()) || (kind !== 'start' && kind !== 'end')) {
+ if (!this.isActive.value || (kind !== 'start' && kind !== 'end')) {
return null;
}
@@ -476,7 +474,7 @@ export class VolumeProfile extends SeriesDrawingBase<VolumeProfileSettings> impl
return null;
}
- if ((!this.isSelected() && !this.isCreationPending()) || (kind !== 'start' && kind !== 'end')) {
+ if (!this.isActive.value || (kind !== 'start' && kind !== 'end')) {
return null;
}
@@ -513,7 +511,7 @@ export class VolumeProfile extends SeriesDrawingBase<VolumeProfileSettings> impl
};
protected handleDoubleClick = (event: MouseEvent): void => {
- if (this.hidden || this.mode !== 'ready' || !this.isSelected()) {
+ if (this.hidden || this.mode !== 'ready' || !this.isActive.value) {
return;
}
@@ -548,21 +546,18 @@ export class VolumeProfile extends SeriesDrawingBase<VolumeProfileSettings> impl
const dragTarget = this.getVisibleRangeDragTarget(point);
if (!dragTarget) {
- if (this.isSelected()) {
- this.deselect();
- }
-
+ this.isActive.next(false);
+ this.render();
return;
}
event.preventDefault();
event.stopPropagation();
- if (!this.isSelected()) {
- this.select();
- }
+ this.isActive.next(true);
if (dragTarget !== 'poc') {
+ this.render();
return;
}
@@ -598,7 +593,7 @@ export class VolumeProfile extends SeriesDrawingBase<VolumeProfileSettings> impl
const dragTarget = this.getFixedRangeDragTarget(point);
- if (!this.isSelected()) {
+ if (!this.isActive.value) {
if (!dragTarget) {
return;
}
@@ -606,12 +601,14 @@ export class VolumeProfile extends SeriesDrawingBase<VolumeProfileSettings> impl
event.preventDefault();
event.stopPropagation();
- this.select();
+ this.isActive.next(true);
+ this.render();
return;
}
if (!dragTarget) {
- this.deselect();
+ this.isActive.next(false);
+ this.render();
return;
}
@@ -683,6 +680,7 @@ export class VolumeProfile extends SeriesDrawingBase<VolumeProfileSettings> impl
this.startAnchor = anchor;
this.endAnchor = anchor;
+ this.isActive.next(true);
this.mode = 'drawing';
this.calculateProfile();
@@ -751,6 +749,7 @@ export class VolumeProfile extends SeriesDrawingBase<VolumeProfileSettings> impl
private resetToIdle(): void {
this.hidden = false;
+ this.isActive.next(false);
this.mode = 'idle';
this.startAnchor = null;
@@ -1222,19 +1221,13 @@ export class VolumeProfile extends SeriesDrawingBase<VolumeProfileSettings> impl
pocY: number | null;
} {
if (!this.profileRows.length) {
- return {
- rows: [],
- pocY: null,
- };
+ return { rows: [], pocY: null };
}
const maxVolume = this.profileRows.reduce((max, row) => Math.max(max, row.totalVolume), 0);
if (maxVolume <= 0) {
- return {
- rows: [],
- pocY: null,
- };
+ return { rows: [], pocY: null };
}
let pocY: number | null = null;
@@ -1266,10 +1259,7 @@ export class VolumeProfile extends SeriesDrawingBase<VolumeProfileSettings> impl
})
.filter((row): row is VolumeProfileRenderRow => row !== null);
- return {
- rows,
- pocY,
- };
+ return { rows, pocY };
}
private createAnchor(point: Point): Anchor | null {
diff --git a/src/core/DrawingsManager.tsx b/src/core/DrawingsManager.tsx
index 569c8ee..e4f85b6 100644
--- a/src/core/DrawingsManager.tsx
+++ b/src/core/DrawingsManager.tsx
@@ -1,5 +1,5 @@
import { IChartApi, ISeriesApi, SeriesType } from 'lightweight-charts';
-import { BehaviorSubject, distinctUntilChanged, map, Observable, Subscription } from 'rxjs';
+import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { EventManager } from '@core';
import { DOMModel } from '@core/DOMModel';
@@ -12,8 +12,6 @@ import { ModalRenderer } from '@src/core/ModalRenderer';
import { SeriesStrategies } from '@src/modules/series-strategies/SeriesFactory';
import { ActiveDrawingTool } from '@src/types';
-import type { DrawingInteraction } from '@src/core/Drawings/common';
-
interface DrawingsManagerParams {
eventManager: EventManager;
mainSeries$: Observable<SeriesStrategies | null>;
@@ -29,13 +27,11 @@ export interface DrawingSnapshotItem {
id: string;
drawingName: DrawingsNames;
state: unknown;
- isLocked?: boolean;
}
interface CreateDrawingOptions {
id?: string;
state?: unknown;
- isLocked?: boolean;
shouldUpdateDrawingsList?: boolean;
}
@@ -51,7 +47,6 @@ export class DrawingsManager {
private mainSeries: SeriesStrategies | null = null;
private subscriptions = new Subscription();
private drawings$ = new BehaviorSubject<Drawing[]>([]);
- private selectedDrawing$ = new BehaviorSubject<Drawing | null>(null);
private activeTool$ = new BehaviorSubject<ActiveDrawingTool>('crosshair');
private endlessMode$ = new BehaviorSubject(false);
private recreateScheduled = false;
@@ -215,12 +210,6 @@ export class DrawingsManager {
return;
}
- const selectedDrawing = this.selectedDrawing$.value;
-
- if (selectedDrawing && drawingsToRemove.includes(selectedDrawing)) {
- this.selectedDrawing$.next(null);
- }
-
drawingsToRemove.forEach((drawing) => {
drawing.destroy();
this.DOM.removeEntity(drawing);
@@ -254,24 +243,14 @@ export class DrawingsManager {
throw new Error('[Drawings] main series is not defined');
}
- const { id, state, isLocked = false, shouldUpdateDrawingsList = true } = options;
- const shouldSelectAfterCreation = state === undefined;
-
- if (shouldSelectAfterCreation && this.selectedDrawing$.value) {
- this.selectedDrawing$.next(null);
- }
+ const { id, state, shouldUpdateDrawingsList = true } = options;
const config = drawingsMap[name];
const drawingId = id ?? crypto.randomUUID();
let createdDrawing: Drawing | null = null;
- const selected$ = this.selectedDrawing$.pipe(
- map((drawing) => drawing?.id === drawingId),
- distinctUntilChanged(),
- );
-
- const construct = (chart: IChartApi, series: ISeriesApi<SeriesType>, interaction: DrawingInteraction) => {
+ const construct = (chart: IChartApi, series: ISeriesApi<SeriesType>) => {
const paneElement = series.getPane().getHTMLElement();
if (!paneElement) {
@@ -286,7 +265,6 @@ export class DrawingsManager {
series,
eventManager: this.eventManager,
container: canvasElement,
- interaction,
removeSelf: () => this.removeDrawing(drawingId),
openSettings: () => {
if (createdDrawing) {
@@ -308,23 +286,6 @@ export class DrawingsManager {
moveDown,
moveUp,
construct,
- selected$,
- isSelected: () => this.selectedDrawing$.value?.id === drawingId,
- select: () => {
- if (!createdDrawing || createdDrawing.isCreationPending() || this.selectedDrawing$.value === createdDrawing) {
- return;
- }
-
- this.selectedDrawing$.next(createdDrawing);
- },
- deselect: () => {
- if (!createdDrawing || this.selectedDrawing$.value !== createdDrawing) {
- return;
- }
-
- this.selectedDrawing$.next(null);
- },
- isLocked,
paneId: this.paneId,
hotkeys: this.hotkeys,
setCopyPasteBuffer: (copyPasteBuffer) => {
@@ -346,18 +307,6 @@ export class DrawingsManager {
this.drawings$.next([...this.drawings$.value, entity]);
}
- if (shouldSelectAfterCreation) {
- entity.waitForCreation().then(() => {
- if (!this.drawings$.value.includes(entity)) {
- return;
- }
-
- this.selectedDrawing$.next(entity);
- this.updateActiveTool();
- this.DOM.refreshEntities();
- });
- }
-
return entity;
}
@@ -368,7 +317,6 @@ export class DrawingsManager {
id: drawing.id,
drawingName: drawing.getDrawingName(),
state: drawing.getState(),
- isLocked: drawing.isLocked(),
}));
}
@@ -390,12 +338,7 @@ export class DrawingsManager {
}
drawings.push(
- this.createDrawing(item.drawingName, {
- id: item.id,
- state: item.state,
- isLocked: item.isLocked,
- shouldUpdateDrawingsList: false,
- }),
+ this.createDrawing(item.drawingName, { id: item.id, state: item.state, shouldUpdateDrawingsList: false }),
);
return drawings;
@@ -419,10 +362,7 @@ export class DrawingsManager {
keys: [Keys.escape],
hash: this.escapeUnregisterHash,
});
-
- this.escapeUnregisterHash = null;
}
-
this.endlessMode$.next(value);
};
@@ -444,35 +384,7 @@ export class DrawingsManager {
return this.drawings$.asObservable();
}
- public selectedDrawing(): Observable<Drawing | null> {
- return this.selectedDrawing$.asObservable();
- }
-
- public openSelectedDrawingSettings(): void {
- const drawing = this.selectedDrawing$.value;
-
- if (!drawing) {
- return;
- }
-
- this.openSettings(drawing);
- }
-
- public deleteSelectedDrawing(): void {
- const drawing = this.selectedDrawing$.value;
-
- if (!drawing) {
- return;
- }
-
- this.removeDrawing(drawing.id);
- }
-
- public toggleSelectedDrawingLock(): void {
- this.selectedDrawing$.value?.toggleLock();
- }
-
- private openSettings = (drawing: Drawing): void => {
+ private openSettings = (drawing: Drawing) => {
const tabs = drawing.getSettingsTabs();
if (!tabs.length || tabs.every((tab) => tab.fields.length === 0)) {
@@ -503,10 +415,6 @@ export class DrawingsManager {
}
public hideAll(): void {
- if (this.selectedDrawing$.value) {
- this.selectedDrawing$.next(null);
- }
-
this.drawings$.value.forEach((drawing) => drawing.hide());
this.DOM.refreshEntities();
}
@@ -516,7 +424,6 @@ export class DrawingsManager {
keys: [Keys.escape],
hash: this.escapeUnregisterHash,
});
-
window.removeEventListener('pointerup', this.handlePointerUp);
this.container.removeEventListener('click', this.handleClick);
this.container.removeEventListener('pointerdown', this.handlePointerDown);
@@ -525,7 +432,6 @@ export class DrawingsManager {
this.subscriptions.unsubscribe();
this.drawings$.complete();
- this.selectedDrawing$.complete();
this.activeTool$.complete();
this.endlessMode$.complete();
}
diff --git a/src/core/Indicator.ts b/src/core/Indicator.ts
index ddb29db..2b49366 100644
--- a/src/core/Indicator.ts
+++ b/src/core/Indicator.ts
@@ -71,6 +71,16 @@ export class Indicator extends DOMObject implements ISerializable<IndicatorSnaps
this.associatedPane.setIndicator(this.id, this);
}
+ public recreateSeries() {
+ this.series.forEach((s) => {
+ s.destroy();
+ });
+
+ this.series = [];
+ this.seriesMap.clear();
+ this.createSeries();
+ }
+
public subscribeDataChange(handler: () => void): Subscription {
this.dataChangeHandlers.add(handler);
diff --git a/src/core/Indicators/ema.ts b/src/core/Indicators/ema.ts
index ee55b0b..d4ebc33 100644
--- a/src/core/Indicators/ema.ts
+++ b/src/core/Indicators/ema.ts
@@ -18,6 +18,12 @@ export function emaIndicator(
const sourceData: LineCandle[] = mainSeriesData.map((point) => {
const values = point.customValues as unknown as Record<string, unknown>;
+ if (!values) {
+ return {
+ time: point.time as Time,
+ } as unknown as LineCandle;
+ }
+
const sourceValue = values[source];
const fallbackValue = values.close ?? values.value;
@@ -62,7 +68,7 @@ export function emaIndicator(
}
if (!selfData) {
- return [{ time: candle.time as Time, value: 0 }];
+ return [{ time: candle.time as Time }];
}
return [calculatePreciseEMASeriesData(sourceData, selfData, candle, length)];
@@ -84,9 +90,8 @@ export function calculatePreciseEMASeriesData(
};
}
- const prevCandleIndex = currentIndicatorData.length - 2;
-
- const prevCandle = currentIndicatorData[prevCandleIndex];
+ const prevCandleIndex = currentIndicatorData.findIndex((c) => c.time === candle.time);
+ const prevCandle = currentIndicatorData[prevCandleIndex - 1];
const smoothing = 2;
const k = smoothing / (maLength + 1);
@@ -111,9 +116,9 @@ export function calculateEMASeriesData(
const k = smoothing / (maLength + 1);
for (let i = 0; i < candleData.length; i++) {
- if (i < maLength - 1) {
+ if (i < maLength - 1 || !candleData[i].value) {
// Provide whitespace data points until the MA can be calculated
- maData.push({ time: candleData[i].time as Time, value: 0 });
+ maData.push({ time: candleData[i].time as Time } as LineData<Time>);
} else if (i === maLength - 1) {
let sum = 0;
for (let j = 0; j < maLength; j++) {
diff --git a/src/core/Indicators/sma.ts b/src/core/Indicators/sma.ts
index 10b5294..84a4fcf 100644
--- a/src/core/Indicators/sma.ts
+++ b/src/core/Indicators/sma.ts
@@ -76,16 +76,18 @@ export function calculatePreciseMASeriesData(
};
}
- let sum = 0;
+ let sum;
for (let i = candleIndexToCalculate; i > candleIndexToCalculate - maLength; i--) {
if (i < 0) break;
+ sum = sum ?? 0;
+
sum += candleData[i].value;
}
return {
time: candle.time as Time,
- value: sum / maLength,
+ value: sum && sum / maLength,
};
}
diff --git a/src/core/Indicators/volume.ts b/src/core/Indicators/volume.ts
index e79a696..4cb4935 100644
--- a/src/core/Indicators/volume.ts
+++ b/src/core/Indicators/volume.ts
@@ -11,6 +11,13 @@ export function volume({
if (!candle) {
return mainSeriesData.map((d) => {
const cv = d.customValues;
+
+ if (!cv) {
+ return {
+ time: d.time as Time,
+ };
+ }
+
return {
time: d.time as Time,
value: cv.volume,
diff --git a/src/core/Legend.ts b/src/core/Legend.ts
index b4aa179..408a2ec 100644
--- a/src/core/Legend.ts
+++ b/src/core/Legend.ts
@@ -244,6 +244,11 @@ export class Legend {
return;
}
+ if (param.seriesData.size === 0) {
+ this.updateWithLastCandle();
+ return;
+ }
+
if (this.paneIndex() === param.paneIndex) {
this.tooltipVisability.next(true);
} else {
diff --git a/src/core/MoexChart.tsx b/src/core/MoexChart.tsx
index 2f46df1..611e81f 100644
--- a/src/core/MoexChart.tsx
+++ b/src/core/MoexChart.tsx
@@ -7,7 +7,6 @@ import { Header } from '@components/Header';
import { DataSource, DataSourceParams } from '@core/DataSource';
import { Hotkeys } from '@core/Hotkeys';
import { ModalRenderer } from '@core/ModalRenderer';
-import { FloatingDrawingToolbar } from '@src/components/FloatingToolbar';
import { SettingsModal } from '@src/components/SettingsModal';
import Toolbar from '@src/components/Toolbar';
@@ -111,7 +110,6 @@ export class MoexChart implements ISerializable<MoexChartSnapshot> {
private toolbarRenderer: UIRenderer | undefined;
private controlBarRenderer?: UIRenderer;
private footerRenderer?: UIRenderer;
- private drawingToolbarRenderer!: UIRenderer;
private timeScaleHoverController!: TimeScaleHoverController;
private dataSource!: DataSource;
@@ -170,7 +168,6 @@ export class MoexChart implements ISerializable<MoexChartSnapshot> {
headerContainer,
modalContainer,
controlBarContainer,
- drawingToolbarContainer,
footerContainer,
toggleToolbar, // todo: move this function to toolbarModel
} = ContainerManager.createContainers({
@@ -223,7 +220,6 @@ export class MoexChart implements ISerializable<MoexChartSnapshot> {
this.headerRenderer = new ReactRenderer(headerContainer);
this.toolbarRenderer = new ReactRenderer(toolBarContainer);
- this.drawingToolbarRenderer = new ReactRenderer(drawingToolbarContainer);
if (config.chartCollectionPreset.showControlBar) {
this.controlBarRenderer = new ReactRenderer(controlBarContainer);
@@ -286,17 +282,6 @@ export class MoexChart implements ISerializable<MoexChartSnapshot> {
}
private renderAttachments(config: IMoexChart, toggleToolbar: () => boolean) {
- const drawingsManager = this.chart.getDrawingsManager();
-
- this.drawingToolbarRenderer.renderComponent(
- <FloatingDrawingToolbar
- selectedDrawing$={drawingsManager.selectedDrawing()}
- onToggleLock={() => drawingsManager.toggleSelectedDrawingLock()}
- onOpenSettings={() => drawingsManager.openSelectedDrawingSettings()}
- onDelete={() => drawingsManager.deleteSelectedDrawing()}
- />,
- );
-
this.headerRenderer.renderComponent(
<Header
timeframes={config.chartCollectionPreset.supportedTimeframes}
@@ -383,7 +368,6 @@ export class MoexChart implements ISerializable<MoexChartSnapshot> {
*/
destroy(): void {
this.headerRenderer.destroy();
- this.drawingToolbarRenderer.destroy();
this.subscriptions.unsubscribe();
this.timeScaleHoverController.destroy();
@@ -408,10 +392,6 @@ export class MoexChart implements ISerializable<MoexChartSnapshot> {
this.eventManager.destroy();
}
- if (this.toolbarRenderer) {
- this.toolbarRenderer.destroy();
- }
-
this.dataSource.destroy();
ContainerManager.clearContainers(this.rootContainer);
diff --git a/src/core/Pane.tsx b/src/core/Pane.tsx
index 147d21f..25cf268 100644
--- a/src/core/Pane.tsx
+++ b/src/core/Pane.tsx
@@ -156,6 +156,9 @@ export class Pane implements ISerializable<PaneSnapshot> {
this.initializeMainSerie({ lwcChart, dataSource });
} else if (basedOn) {
this.mainSeries = basedOn.getMainSerie();
+ this.mainSeries.subscribe(() => {
+ this.rebindIndicators();
+ });
} else {
console.error('[Pane]: There is no any mainSerie for new pane');
}
@@ -395,6 +398,12 @@ export class Pane implements ISerializable<PaneSnapshot> {
);
}
+ private rebindIndicators(): void {
+ for (const indicator of this.indicatorsMap.value.values()) {
+ indicator.recreateSeries();
+ }
+ }
+
private initializeMainSerie({ lwcChart, dataSource }: { lwcChart: IChartApi; dataSource: DataSource }): void {
this.mainSerieSub = this.eventManager.subscribeSeriesSelected((nextSeries) => {
this.mainSeries.value?.destroy();
@@ -408,6 +417,8 @@ export class Pane implements ISerializable<PaneSnapshot> {
});
this.mainSeries.next(next);
+ this.rebindIndicators();
+
this.priceScaleControls.refresh();
});
}
diff --git a/src/core/PaneManager.ts b/src/core/PaneManager.ts
index 45a0c0c..131ceb7 100644
--- a/src/core/PaneManager.ts
+++ b/src/core/PaneManager.ts
@@ -24,7 +24,7 @@ interface PaneManagerParams
panesSnapshot: PaneSnapshot[];
}
-interface PriceAxisLabelsSources {
+interface PaneManagerStartParams {
compareEntities$: Observable<Indicator[]>;
indicatorEntities$: Observable<Indicator[]>;
}
@@ -90,7 +90,7 @@ export class PaneManager implements ISerializable<PaneSnapshot[]> {
this.syncPaneContainers();
}
- public start({ compareEntities$, indicatorEntities$ }: PriceAxisLabelsSources): void {
+ public start({ compareEntities$, indicatorEntities$ }: PaneManagerStartParams): void {
this.priceAxisLabels?.destroy();
this.priceAxisLabels = new PriceAxisLabels({
diff --git a/src/core/PriceScale/PriceScaleControls.tsx b/src/core/PriceScale/PriceScaleControls.tsx
index 532c5a8..72fa3c6 100644
--- a/src/core/PriceScale/PriceScaleControls.tsx
+++ b/src/core/PriceScale/PriceScaleControls.tsx
@@ -2,7 +2,6 @@ import { PriceScaleMode } from 'lightweight-charts';
import { PriceScaleControls as PriceScaleControlsView } from '@components/PriceScaleControls';
import { ReactRenderer } from '@core/ReactRenderer';
-import { CHART_PRICE_SCALE_CONTROLS, CHART_PRICE_SCALE_CONTROLS_VISIBLE } from '@src/constants';
import { Direction } from '@src/types';
import { PriceScale } from './PriceScale';
@@ -37,7 +36,7 @@ export class PriceScaleControls {
this.onPriceScaleChange = onPriceScaleChange;
this.container = document.createElement('div');
- this.container.classList.add(CHART_PRICE_SCALE_CONTROLS);
+ this.container.className = 'moex-price-scale-controls';
this.renderer = new ReactRenderer(this.container);
}
@@ -139,12 +138,12 @@ export class PriceScaleControls {
/>,
);
- this.container.classList.add(CHART_PRICE_SCALE_CONTROLS_VISIBLE);
+ this.container.classList.add('moex-price-scale-controls_visible');
}
private hide(): void {
this.renderedAutoScaleEnabled = null;
- this.container.classList.remove(CHART_PRICE_SCALE_CONTROLS_VISIBLE);
+ this.container.classList.remove('moex-price-scale-controls_visible');
}
private getHoveredPriceScale(clientX: number): PriceScale | null {
diff --git a/src/core/Series/BarSeriesStrategy.ts b/src/core/Series/BarSeriesStrategy.ts
index 39a084b..93e754e 100644
--- a/src/core/Series/BarSeriesStrategy.ts
+++ b/src/core/Series/BarSeriesStrategy.ts
@@ -48,14 +48,7 @@ export class BarSeriesStrategy extends BaseSeries<'Bar'> implements ISeries<'Bar
return false;
}
- return data.every(
- (point) =>
- typeof point.time === 'number' &&
- typeof point.open === 'number' &&
- typeof point.high === 'number' &&
- typeof point.low === 'number' &&
- typeof point.close === 'number',
- );
+ return data.every((point) => typeof point.time === 'number');
}
protected dataSourceSubscription = (dataToSet: Candle[]): void => {
@@ -74,7 +67,7 @@ export class BarSeriesStrategy extends BaseSeries<'Bar'> implements ISeries<'Bar
}
const formattedData = this.formatData([dataToSet]);
- this.update(formattedData[0]);
+ this.update(formattedData[0], true);
};
protected formatMainSerie(inputData: Candle[]): SeriesDataItemTypeMap<Time>['Bar'][] {
diff --git a/src/core/Series/CandlestickSeriesStrategy.ts b/src/core/Series/CandlestickSeriesStrategy.ts
index 4089ab2..52d41c1 100644
--- a/src/core/Series/CandlestickSeriesStrategy.ts
+++ b/src/core/Series/CandlestickSeriesStrategy.ts
@@ -55,12 +55,17 @@ export class CandlestickSeriesStrategy extends BaseSeries<'Candlestick'> impleme
return false;
}
// Проверяем обязательные поля
- if (typeof point.time !== 'number' || typeof point.close !== 'number') {
+ if (typeof point.time !== 'number') {
return false;
}
// Если указаны OHLC, проверяем их корректность
- if (point.open !== undefined && point.high !== undefined && point.low !== undefined) {
+ if (
+ point.open !== undefined &&
+ point.high !== undefined &&
+ point.low !== undefined &&
+ point.close !== undefined
+ ) {
return point.high >= Math.max(point.open, point.close) && point.low <= Math.min(point.open, point.close);
}
@@ -88,7 +93,7 @@ export class CandlestickSeriesStrategy extends BaseSeries<'Candlestick'> impleme
}
const formattedData = this.formatData([dataToSet]);
- this.update(formattedData[0]);
+ this.update(formattedData[0], true);
};
protected formatMainSerie(inputData: Candle[]): SeriesDataItemTypeMap<Time>['Candlestick'][] {
diff --git a/src/core/Series/HistogramSeriesStrategy.ts b/src/core/Series/HistogramSeriesStrategy.ts
index 9060606..c877ad6 100644
--- a/src/core/Series/HistogramSeriesStrategy.ts
+++ b/src/core/Series/HistogramSeriesStrategy.ts
@@ -39,9 +39,7 @@ export class HistogramSeriesStrategy extends BaseSeries<'Histogram'> implements
return false;
}
- return data.every(
- (point) => typeof point.time === 'number' && typeof point.volume === 'number' && !Number.isNaN(point.volume),
- );
+ return data.every((point) => typeof point.time === 'number');
}
public getTypeName(): string {
@@ -64,7 +62,7 @@ export class HistogramSeriesStrategy extends BaseSeries<'Histogram'> implements
}
const formattedData = this.formatData([dataToSet]);
- this.update(formattedData[0]);
+ this.update(formattedData[0], true);
};
protected formatMainSerie(inputData: Candle[]): SeriesDataItemTypeMap<Time>['Histogram'][] {
diff --git a/src/core/Series/LineSeriesStrategy.ts b/src/core/Series/LineSeriesStrategy.ts
index 8c655c9..74ae054 100644
--- a/src/core/Series/LineSeriesStrategy.ts
+++ b/src/core/Series/LineSeriesStrategy.ts
@@ -48,9 +48,7 @@ export class LineSeriesStrategy extends BaseSeries<'Line'> implements ISeries<'L
return false;
}
- return data.every(
- (point) => typeof point.time === 'number' && typeof point.close === 'number' && !Number.isNaN(point.close),
- );
+ return data.every((point) => typeof point.time === 'number');
}
public getTypeName(): string {
@@ -73,7 +71,7 @@ export class LineSeriesStrategy extends BaseSeries<'Line'> implements ISeries<'L
}
const formattedData = this.formatData([dataToSet]);
- this.update(formattedData[0]);
+ this.update(formattedData[0], true);
};
protected formatMainSerie(inputData: Candle[]): SeriesDataItemTypeMap<Time>['Line'][] {
diff --git a/src/core/SymbolSource.ts b/src/core/SymbolSource.ts
index 638b314..ea51b71 100644
--- a/src/core/SymbolSource.ts
+++ b/src/core/SymbolSource.ts
@@ -1,4 +1,5 @@
import dayjs from 'dayjs';
+import { clone } from 'lodash-es';
import { BehaviorSubject, firstValueFrom, Observable, Subject } from 'rxjs';
import { filter, take } from 'rxjs/operators';
@@ -74,11 +75,6 @@ export class SymbolSource {
return this.lastCandleSubject.value;
}
- public async ready(): Promise<void> {
- if (this.isInitializedSubject.value) return;
- await firstValueFrom(this.isInitializedSubject.pipe(filter(Boolean), take(1)));
- }
-
public destroy(): void {
this.loadSeq += 1;
this.loadingPromise = null;
@@ -118,8 +114,7 @@ export class SymbolSource {
const tf = this.getTimeframe();
this.realtimeCache = this.normalizeList(tf, this.realtimeCache);
-
- const left = this.currentDataSubject.value;
+ const left = this.currentDataSubject.value.filter((v) => v.open !== undefined);
const right = this.realtimeCache;
if (left.length === 0 || right.length === 0) {
@@ -139,8 +134,10 @@ export class SymbolSource {
: left.concat(right);
const normalizedNext = this.normalizeList(tf, next);
+ const extendedNormalizedNext = applyWhitespacesToFuture(normalizedNext);
+
+ this.currentDataSubject.next(extendedNormalizedNext);
- this.currentDataSubject.next(normalizedNext);
this.newestCandle = normalizedNext[normalizedNext.length - 1] ?? null;
this.realtimeCache = [];
@@ -166,7 +163,6 @@ export class SymbolSource {
this.isLoadingSubject.next(true);
try {
- this.saveRealtimeCache();
if (!this.oldestCandle) return;
const olderData = await this.getData(tf, this.symbol, this.oldestCandle);
@@ -193,6 +189,7 @@ export class SymbolSource {
this.currentDataSubject.next(combined);
this.lastCandleSubject.next(this.newestCandle);
+ this.saveRealtimeCache();
} catch (error) {
console.error('[DataSource] Ошибка при догрузке истории:', error);
} finally {
@@ -227,8 +224,6 @@ export class SymbolSource {
this.isLoadingSubject.next(true);
try {
- this.saveRealtimeCache();
-
let current = this.currentDataSubject.value;
let oldest = current[0] ?? null;
@@ -328,12 +323,16 @@ export class SymbolSource {
const loaded = (await this.getData(tf, this.symbol)) ?? [];
if (seq !== this.loadSeq) return;
- const normalized = this.normalizeList(tf, loaded);
+ let normalized = this.normalizeList(tf, loaded);
- this.oldestCandle = normalized[0] ?? null;
this.newestCandle = normalized[normalized.length - 1] ?? null;
+ normalized = applyWhitespacesToFuture(normalized);
+
+ this.oldestCandle = normalized[0] ?? null;
+
this.currentDataSubject.next(normalized);
+
this.lastCandleSubject.next(this.newestCandle);
this.isInitializedSubject.next(true);
@@ -354,6 +353,11 @@ export class SymbolSource {
await task;
}
+ private async ready(): Promise<void> {
+ if (this.isInitializedSubject.value) return;
+ await firstValueFrom(this.isInitializedSubject.pipe(filter(Boolean), take(1)));
+ }
+
private flushRealtimeBuffer(): void {
if (this.realtimeBuffer.length === 0) return;
@@ -444,3 +448,20 @@ export class SymbolSource {
};
}
}
+
+export function applyWhitespacesToFuture(data: Candle[]): Candle[] {
+ if (data.length < 2) {
+ return [];
+ }
+ const lastTime = data[data.length - 1].time;
+ const timeDiff = data[data.length - 1].time - data[data.length - 2].time;
+ const whitespacesNeeded = 5000;
+ const res = clone(data);
+ let i = 0;
+ while (i < whitespacesNeeded) {
+ i++;
+ res.push({ time: lastTime + timeDiff * i } as Candle);
+ }
+
+ return res;
+}
diff --git a/src/core/styles.module.scss b/src/core/styles.module.scss
index 16d5de6..7de2375 100644
--- a/src/core/styles.module.scss
+++ b/src/core/styles.module.scss
@@ -1,11 +1,12 @@
.scrollableBox {
- scrollbar-width: thin;
- scrollbar-color: var(--neutral-6) #00000000;
-
&::-webkit-scrollbar {
height: 4px;
}
+ &::-webkit-scrollbar-button {
+ display: none;
+ }
+
&::-webkit-scrollbar-track {
background: #00000000;
border-radius: 4px;
@@ -21,6 +22,13 @@
}
}
+@supports not selector(::-webkit-scrollbar) {
+ .scrollableBox {
+ scrollbar-width: thin;
+ scrollbar-color: var(--neutral-6) #00000000;
+ }
+}
+
.safariFullscreen {
position: fixed;
top: 0;
diff --git a/src/styles/global.scss b/src/styles/global.scss
index 1ff1616..ad77c8c 100644
--- a/src/styles/global.scss
+++ b/src/styles/global.scss
@@ -8,7 +8,7 @@
position: fixed;
inset: 0;
pointer-events: none;
- z-index: 1100;
+ z-index: 900;
}
.moex-chart-portal-host > * {
diff --git a/src/translations/russianDict.ts b/src/translations/russianDict.ts
index 1812d10..b3a4e59 100644
--- a/src/translations/russianDict.ts
+++ b/src/translations/russianDict.ts
@@ -75,16 +75,13 @@ export const russian = {
Time: 'Время',
Arguments: 'Аргументы',
// settings
- Area: 'Область',
- Border: 'Граница',
+ 'Area color': 'Цвет области',
+ 'Border color': 'Цвет границы',
'Font size': 'Размер текста',
Bold: 'Жирный',
- Italic: 'Курсив',
+ Italics: 'Курсив',
'Text color': 'Цвет текста',
'Background color': 'Цвет фона',
- Lock: 'Заблокировать',
- Unlock: 'Разблокировать',
- Remove: 'Удалить',
Style: 'Стиль',
'Background transparency': 'Прозрачность фона',
Background: 'Фон',
@@ -94,18 +91,15 @@ export const russian = {
'Show level values': 'Отображать уровень',
Color: 'Цвет',
Levels: 'Уровни',
- Level: 'Уровень',
Left: 'Слева',
Right: 'Справа',
'Enter text': 'Введите текст',
- 'Profit zone': 'Зона прибыли',
- 'Loss zone': 'Зона убытка',
+ 'Profit zone color': 'Цвет зоны прибыли',
+ 'Loss zone color': 'Цвет зоны убытка',
'Line color': 'Цвет линии',
- 'Growing volume': 'Растущий объём',
- 'Decreasing volume': 'Снижающий объём',
- 'POC-line': 'POC-линия',
- 'Parallel channel': 'Параллельный канал',
- 'Middle line': 'Средняя линия',
+ 'Growing volume color': 'Цвет растущего объёма',
+ 'Decreasing volume color': 'Цвет снижающего объёма',
+ 'POC-line color': 'Цвет POC-линии',
Length: 'Длинна',
Data: 'Данные',
'Open price': 'Цена открытия',
diff --git a/src/types/drawing.ts b/src/types/drawing.ts
index b8f9dea..71934d0 100644
--- a/src/types/drawing.ts
+++ b/src/types/drawing.ts
@@ -2,8 +2,7 @@ import { IChartApi, ISeriesApi, SeriesType } from 'lightweight-charts';
import { DrawingsNames } from '@src/constants';
import { EventManager } from '@src/core';
-
-import type { DrawingInteraction, ISeriesDrawing } from '@src/core/Drawings/common';
+import { ISeriesDrawing } from '@src/core/Drawings/common';
export type ActiveDrawingTool = DrawingsNames | 'crosshair';
@@ -12,11 +11,9 @@ interface DrawingParams {
series: ISeriesApi<SeriesType>;
eventManager: EventManager;
container: HTMLElement;
- interaction: DrawingInteraction;
removeSelf: () => void;
openSettings: () => void;
}
-
export interface DrawingConfig {
singleInstance?: boolean;
construct: (params: DrawingParams) => ISeriesDrawing;
diff --git a/src/types/settings.ts b/src/types/settings.ts
index 62572c4..5904562 100644
--- a/src/types/settings.ts
+++ b/src/types/settings.ts
@@ -1,19 +1,9 @@
export type SettingValue = string | number | boolean | any;
export type SettingsValues = Record<string, SettingValue>;
-export type ToolbarColorRole = 'line' | 'fill' | 'text';
-
-export interface ToolbarColorControlConfig {
- control: 'color';
- role: ToolbarColorRole;
-}
-
-export type ToolbarControlConfig = ToolbarColorControlConfig;
-
interface BaseSettingField {
key: string;
label: string;
- toolbar?: ToolbarControlConfig;
}
export interface NumberSettingField extends BaseSettingField {
@@ -70,10 +60,6 @@ export type SettingField =
| TextAreaSettingField
| BooleanSettingField;
-export type ToolbarSettingField = SettingField & {
- toolbar: ToolbarControlConfig;
-};
-
export interface SettingsTab<TField extends SettingField = SettingField> {
key: string;
label: string;
diff --git a/stories/MB/MB.stories.tsx b/stories/MB/MB.stories.tsx
index 098f8de..e5e893f 100644
--- a/stories/MB/MB.stories.tsx
+++ b/stories/MB/MB.stories.tsx
@@ -9,6 +9,8 @@ import { SymbolInfoInput } from '@lib/types';
import { dataSourceProvider } from '../common';
+import { activate } from '../worker';
+
import type { Meta, StoryObj } from '@storybook/react';
/**
@@ -16,6 +18,8 @@ import type { Meta, StoryObj } from '@storybook/react';
* describes an entry point of MoexChart for MB user
*/
+activate()
+
type MBProps = Omit<IMoexChart, 'container'>;
// todo: переписат ьпод новую сигнатуру
diff --git a/stories/MXT/MXT.stories.tsx b/stories/MXT/MXT.stories.tsx
index 874c426..176bf98 100644
--- a/stories/MXT/MXT.stories.tsx
+++ b/stories/MXT/MXT.stories.tsx
@@ -8,6 +8,8 @@ import { IndicatorsIds } from '@lib/constants';
import { dataSourceProvider } from '../common';
+import { activate } from '../worker';
+
import type { Meta, StoryObj } from '@storybook/react';
/**
@@ -15,6 +17,8 @@ import type { Meta, StoryObj } from '@storybook/react';
* describes an entry point of MoexChart for MXT user
*/
+activate()
+
export type MXTProps = Omit<IMoexChart, 'container'>;
// todo: переписат ьпод новую сигнатуру
const MXTEntry = (props: MXTProps) => {
diff --git a/stories/TradeRadar/TradeRadar.stories.tsx b/stories/TradeRadar/TradeRadar.stories.tsx
index 73f38a5..d097064 100644
--- a/stories/TradeRadar/TradeRadar.stories.tsx
+++ b/stories/TradeRadar/TradeRadar.stories.tsx
@@ -11,6 +11,8 @@ import { CompareMode, SymbolInfoInput } from '@lib/types';
import { dataSourceProvider } from '../common';
+import { activate } from '../worker';
+
import type { Meta, StoryObj } from '@storybook/react';
/**
@@ -19,7 +21,7 @@ import type { Meta, StoryObj } from '@storybook/react';
*/
type TRProps = Omit<IMoexChart, 'container'>;
-
+activate()
const TREntry = (props: TRProps) => {
const [isCompareOpen, setIsCompareOpen] = useState(false);
const [moexChart, setMoexChart] = useState<MoexChart | undefined>();
diff --git a/stories/common.ts b/stories/common.ts
index 4e2b743..1ecc414 100644
--- a/stories/common.ts
+++ b/stories/common.ts
@@ -37,9 +37,9 @@ class DataSourceProvider {
update: (symbol: string, candle: Candle) => void,
periodMs = 500,
): () => void {
- if (this.realtimeTimer) clearInterval(this.realtimeTimer);
+ if (this.realtimeTimer) window.clearIntervalReliable(this.realtimeTimer);
- this.realtimeTimer = setInterval(() => {
+ this.realtimeTimer = window.setIntervalReliable(() => {
const tf = getTimeframe();
const symbols = getSymbols();
@@ -52,7 +52,7 @@ class DataSourceProvider {
}, periodMs);
return () => {
- if (this.realtimeTimer) clearInterval(this.realtimeTimer);
+ if (this.realtimeTimer) window.clearIntervalReliable(this.realtimeTimer);
this.realtimeTimer = null;
};
}
diff --git a/stories/worker.ts b/stories/worker.ts
new file mode 100644
index 0000000..679a4ef
--- /dev/null
+++ b/stories/worker.ts
@@ -0,0 +1,161 @@
+/* eslint-disable */
+declare global {
+ interface Window {
+ setIntervalReliable: typeof setInterval;
+ setTimeoutReliable: typeof setTimeout;
+ clearIntervalReliable: typeof clearInterval;
+ clearTimeoutReliable: typeof clearTimeout;
+ }
+}
+
+const LOG_PREFIX = '[HTIMER]: ';
+
+export const activate = () => {
+ let workerScript = '';
+ if (!/MSIE 10/i.test(navigator.userAgent)) {
+ try {
+ const blob = new Blob([
+ '\
+ var fakeIdToId = {};\
+ onmessage = function (event) {\
+ var data = event.data,\
+ name = data.name,\
+ fakeId = data.fakeId,\
+ time;\
+ if(data.hasOwnProperty(\'time\')) {\
+ time = data.time;\
+ }\
+ switch (name) {\
+ case \'setInterval\':\
+ fakeIdToId[fakeId] = setInterval(function () {\
+ postMessage({fakeId: fakeId});\
+ }, time);\
+ break;\
+ case \'clearInterval\':\
+ if (fakeIdToId.hasOwnProperty (fakeId)) {\
+ clearInterval(fakeIdToId[fakeId]);\
+ delete fakeIdToId[fakeId];\
+ }\
+ break;\
+ case \'setTimeout\':\
+ fakeIdToId[fakeId] = setTimeout(function () {\
+ postMessage({fakeId: fakeId});\
+ if (fakeIdToId.hasOwnProperty (fakeId)) {\
+ delete fakeIdToId[fakeId];\
+ }\
+ }, time);\
+ break;\
+ case \'clearTimeout\':\
+ if (fakeIdToId.hasOwnProperty (fakeId)) {\
+ clearTimeout(fakeIdToId[fakeId]);\
+ delete fakeIdToId[fakeId];\
+ }\
+ break;\
+ }\
+ }\
+ ',
+ ]);
+ // Obtain a blob URL reference to our worker 'file'.
+ workerScript = window.URL.createObjectURL(blob);
+ } catch (error) {
+ /* Blob is not supported, use external script instead */
+ }
+ }
+ let worker: Worker;
+ const fakeIdToCallback: Record<number, { callback: () => void; parameters: unknown[]; isTimeout?: boolean }> = {};
+ let lastFakeId = 0;
+ const maxFakeId = 0x7fffffff; // 2 ^ 31 - 1, 31 bit, positive values of signed 32 bit integer
+ if (typeof Worker !== 'undefined') {
+ function getFakeId() {
+ do {
+ if (lastFakeId == maxFakeId) {
+ lastFakeId = 0;
+ } else {
+ lastFakeId++;
+ }
+ } while (fakeIdToCallback.hasOwnProperty(lastFakeId));
+ return lastFakeId;
+ }
+ try {
+ worker = new Worker(workerScript);
+ window.setIntervalReliable = function (callback: (...args: any[]) => void, time?: number): number {
+ const fakeId = getFakeId();
+ fakeIdToCallback[fakeId] = {
+ callback,
+ parameters: Array.prototype.slice.call(arguments, 2),
+ };
+ worker.postMessage({
+ name: 'setInterval',
+ fakeId,
+ time,
+ });
+ return fakeId;
+ } as any;
+ window.clearIntervalReliable = function (fakeId: number) {
+ if (fakeIdToCallback.hasOwnProperty(fakeId)) {
+ delete fakeIdToCallback[fakeId];
+ worker.postMessage({
+ name: 'clearInterval',
+ fakeId,
+ });
+ }
+ } as any;
+ window.setTimeoutReliable = function (callback: (...args: any[]) => void, time?: number) {
+ const fakeId = getFakeId();
+ fakeIdToCallback[fakeId] = {
+ callback,
+ parameters: Array.prototype.slice.call(arguments, 2),
+ isTimeout: true,
+ };
+ worker.postMessage({
+ name: 'setTimeout',
+ fakeId,
+ time,
+ });
+ return fakeId;
+ } as any;
+ window.clearTimeoutReliable = function (fakeId: number) {
+ if (fakeIdToCallback.hasOwnProperty(fakeId)) {
+ delete fakeIdToCallback[fakeId];
+ worker.postMessage({
+ name: 'clearTimeout',
+ fakeId,
+ });
+ }
+ } as any;
+ worker.onmessage = function (event) {
+ const { data } = event;
+ const { fakeId } = data;
+ let request;
+ let parameters;
+ let callback;
+ if (fakeIdToCallback.hasOwnProperty(fakeId)) {
+ request = fakeIdToCallback[fakeId];
+ callback = request.callback;
+ parameters = request.parameters;
+ if (request.hasOwnProperty('isTimeout') && request.isTimeout) {
+ delete fakeIdToCallback[fakeId];
+ }
+ }
+ if (typeof callback === 'string') {
+ try {
+ callback = new Function(callback);
+ } catch (error) {
+ console.log(LOG_PREFIX, 'Error parsing callback code string: ', error);
+ }
+ }
+ if (typeof callback === 'function') {
+ callback.apply(window, parameters);
+ }
+ };
+ worker.onerror = function (event) {
+ console.log(LOG_PREFIX, event);
+ };
+ } catch (error) {
+ console.log(LOG_PREFIX, 'Initialisation failed');
+ console.error(LOG_PREFIX, error);
+ }
+ } else {
+ console.log(LOG_PREFIX, 'Initialisation failed - HTML5 Web Worker is not supported');
+ }
+};