diff --git a/src/constants/styles/colors.ts b/src/constants/styles/colors.ts index 7443864..7075c17 100644 --- a/src/constants/styles/colors.ts +++ b/src/constants/styles/colors.ts @@ -1,4 +1,12 @@ -import { AppColorMode } from '@/state/configs'; +import type { ThemeName } from 'public/tradingview/charting_library'; + +import { AppColorMode, AppTheme } from '@/state/configs'; + +export const THEME_NAMES: Record = { + [AppTheme.Classic]: 'Classic', + [AppTheme.Dark]: 'Dark', + [AppTheme.Light]: 'Light', +}; export type Theme = { [AppColorMode.GreenUp]: ThemeColorBase; diff --git a/src/constants/tvchart.ts b/src/constants/tvchart.ts new file mode 100644 index 0000000..62503ac --- /dev/null +++ b/src/constants/tvchart.ts @@ -0,0 +1,16 @@ +import { OrderSide } from '@dydxprotocol/v4-client-js'; + +import type { + IChartingLibraryWidget, + IOrderLineAdapter, + IPositionLineAdapter, +} from 'public/tradingview/charting_library'; + +export type TvWidget = IChartingLibraryWidget & { _id?: string; _ready?: boolean }; + +export type ChartLineType = OrderSide | 'position'; + +export type ChartLine = { + line: IOrderLineAdapter | IPositionLineAdapter; + chartLineType: ChartLineType; +}; diff --git a/src/hooks/tradingView/index.ts b/src/hooks/tradingView/index.ts index 13ecccc..5d26196 100644 --- a/src/hooks/tradingView/index.ts +++ b/src/hooks/tradingView/index.ts @@ -1,2 +1,4 @@ +export { useChartLines } from './useChartLines'; +export { useChartMarketAndResolution } from './useChartMarketAndResolution'; export { useTradingView } from './useTradingView'; export { useTradingViewTheme } from './useTradingViewTheme'; diff --git a/src/hooks/tradingView/useChartLines.ts b/src/hooks/tradingView/useChartLines.ts new file mode 100644 index 0000000..81620e1 --- /dev/null +++ b/src/hooks/tradingView/useChartLines.ts @@ -0,0 +1,209 @@ +import { useEffect, useState } from 'react'; + +import { shallowEqual, useSelector } from 'react-redux'; + +import { AbacusOrderStatus, ORDER_SIDES, SubaccountOrder } from '@/constants/abacus'; +import { STRING_KEYS } from '@/constants/localization'; +import { type OrderType, ORDER_TYPE_STRINGS } from '@/constants/trade'; +import type { ChartLine, TvWidget } from '@/constants/tvchart'; + +import { useStringGetter } from '@/hooks'; + +import { getCurrentMarketOrders, getCurrentMarketPositionData } from '@/state/accountSelectors'; +import { getAppTheme, getAppColorMode } from '@/state/configsSelectors'; + +import { MustBigNumber } from '@/lib/numbers'; +import { getChartLineColors } from '@/lib/tradingView/utils'; + +let chartLines: Record = {}; + +/** + * @description Hook to handle drawing chart lines + */ + +export const useChartLines = ({ + tvWidget, + displayButton, + isChartReady, +}: { + tvWidget: TvWidget | null; + displayButton: HTMLElement | null; + isChartReady?: boolean; +}) => { + const [showOrderLines, setShowOrderLines] = useState(false); + + const stringGetter = useStringGetter(); + + const appTheme = useSelector(getAppTheme); + const appColorMode = useSelector(getAppColorMode); + + const currentMarketPositionData = useSelector(getCurrentMarketPositionData, shallowEqual); + const currentMarketOrders: SubaccountOrder[] = useSelector(getCurrentMarketOrders, shallowEqual); + + useEffect(() => { + if (isChartReady && displayButton) { + displayButton.onclick = () => { + const newShowOrderLinesState = !showOrderLines; + if (newShowOrderLinesState) { + displayButton?.classList?.add('order-lines-active'); + } else { + displayButton?.classList?.remove('order-lines-active'); + } + setShowOrderLines(newShowOrderLinesState); + }; + } + }, [isChartReady, showOrderLines]); + + useEffect(() => { + if (tvWidget && isChartReady) { + tvWidget.onChartReady(() => { + tvWidget.chart().dataReady(() => { + if (showOrderLines) { + drawOrderLines(); + drawPositionLine(); + } else { + deleteChartLines(); + } + }); + }); + } + }, [isChartReady, showOrderLines, currentMarketPositionData, currentMarketOrders]); + + const drawPositionLine = () => { + if (!currentMarketPositionData) return; + + const entryPrice = currentMarketPositionData.entryPrice?.current; + const size = currentMarketPositionData.size?.current; + + const key = currentMarketPositionData.id; + const price = MustBigNumber(entryPrice).toNumber(); + + const maybePositionLine = chartLines[key]?.line; + const shouldShow = size && size !== 0; + + if (!shouldShow) { + if (maybePositionLine) { + maybePositionLine.remove(); + delete chartLines[key]; + return; + } + } else { + const quantity = size.toString(); + + if (maybePositionLine) { + if (maybePositionLine.getQuantity() !== quantity) { + maybePositionLine.setQuantity(quantity); + } + if (maybePositionLine.getPrice() !== price) { + maybePositionLine.setPrice(price); + } + } else { + const positionLine = tvWidget + ?.chart() + .createPositionLine({ disableUndo: false }) + .setText(stringGetter({ key: STRING_KEYS.ENTRY_PRICE_SHORT })) + .setPrice(price) + .setQuantity(quantity); + + if (positionLine) { + const chartLine = { line: positionLine, chartLineType: 'position' }; + setLineColors({ chartLine: chartLine }); + chartLines[key] = chartLine; + } + } + } + }; + + const drawOrderLines = () => { + if (!currentMarketOrders) return; + + currentMarketOrders.forEach( + ({ + id, + type, + status, + side, + cancelReason, + remainingSize, + size, + triggerPrice, + price, + trailingPercent, + }) => { + const key = id; + const quantity = (remainingSize ?? size).toString(); + + const orderType = type.rawValue as OrderType; + const orderLabel = stringGetter({ + key: ORDER_TYPE_STRINGS[orderType].orderTypeKey, + }); + const orderString = trailingPercent ? `${orderLabel} ${trailingPercent}%` : orderLabel; + + const shouldShow = + !cancelReason && + (status === AbacusOrderStatus.open || status === AbacusOrderStatus.untriggered); + + const maybeOrderLine = chartLines[key]?.line; + + if (!shouldShow) { + if (maybeOrderLine) { + maybeOrderLine.remove(); + delete chartLines[key]; + return; + } + } else { + if (maybeOrderLine) { + if (maybeOrderLine.getQuantity() !== quantity) { + maybeOrderLine.setQuantity(quantity); + } + } else { + const orderLine = tvWidget + ?.chart() + .createOrderLine({ disableUndo: false }) + .setPrice(MustBigNumber(triggerPrice ?? price).toNumber()) + .setQuantity(quantity) + .setText(orderString); + + if (orderLine) { + const chartLine: ChartLine = { + line: orderLine, + chartLineType: ORDER_SIDES[side.name], + }; + setLineColors({ chartLine: chartLine }); + chartLines[key] = chartLine; + } + } + } + } + ); + }; + + const deleteChartLines = () => { + Object.values(chartLines).forEach(({ line }) => { + line.remove(); + }); + chartLines = {}; + }; + + const setLineColors = ({ chartLine }: { chartLine: ChartLine }) => { + const { line, chartLineType } = chartLine; + const { maybeQuantityColor, borderColor, backgroundColor, textColor, textButtonColor } = + getChartLineColors({ + appTheme, + appColorMode, + chartLineType, + }); + + line + .setQuantityBorderColor(borderColor) + .setBodyBackgroundColor(backgroundColor) + .setBodyBorderColor(borderColor) + .setBodyTextColor(textColor) + .setQuantityTextColor(textButtonColor); + + maybeQuantityColor && + line.setLineColor(maybeQuantityColor).setQuantityBackgroundColor(maybeQuantityColor); + }; + + return { chartLines }; +}; diff --git a/src/hooks/tradingView/useChartMarketAndResolution.ts b/src/hooks/tradingView/useChartMarketAndResolution.ts new file mode 100644 index 0000000..9343b2d --- /dev/null +++ b/src/hooks/tradingView/useChartMarketAndResolution.ts @@ -0,0 +1,65 @@ +import { useEffect } from 'react'; + +import { useDispatch, useSelector } from 'react-redux'; + +import type { ResolutionString } from 'public/tradingview/charting_library'; + +import { DEFAULT_RESOLUTION, RESOLUTION_CHART_CONFIGS } from '@/constants/candles'; +import { DEFAULT_MARKETID } from '@/constants/markets'; +import type { TvWidget } from '@/constants/tvchart'; + +import { setTvChartResolution } from '@/state/perpetuals'; +import { getCurrentMarketId, getSelectedResolutionForMarket } from '@/state/perpetualsSelectors'; + +/** + * @description Hook to handle changing markets and setting chart resolution + */ + +export const useChartMarketAndResolution = ({ + tvWidget, + isWidgetReady, + savedResolution, +}: { + tvWidget: TvWidget | null; + isWidgetReady?: boolean; + savedResolution?: ResolutionString; +}) => { + const dispatch = useDispatch(); + + const currentMarketId: string = useSelector(getCurrentMarketId) || DEFAULT_MARKETID; + const selectedResolution: string = + useSelector(getSelectedResolutionForMarket(currentMarketId)) || DEFAULT_RESOLUTION; + + const chart = isWidgetReady ? tvWidget?.chart() : undefined; + const chartResolution = chart?.resolution?.(); + + useEffect(() => { + if (currentMarketId && isWidgetReady) { + const resolution = savedResolution || selectedResolution; + tvWidget?.setSymbol(currentMarketId, resolution as ResolutionString, () => {}); + } + }, [currentMarketId, isWidgetReady, savedResolution, selectedResolution]); + + useEffect(() => { + if (chartResolution) { + if (chartResolution !== selectedResolution) { + dispatch(setTvChartResolution({ marketId: currentMarketId, resolution: chartResolution })); + } + + setVisibleRangeForResolution({ resolution: chartResolution }); + } + }, [currentMarketId, chartResolution, selectedResolution]); + + const setVisibleRangeForResolution = ({ resolution }: { resolution: ResolutionString }) => { + // Different resolutions have different timeframes to display data efficiently. + const { defaultRange } = RESOLUTION_CHART_CONFIGS[resolution]; + + // from/to values converted to epoch seconds + const newRange = { + from: (Date.now() - defaultRange) / 1000, + to: Date.now() / 1000, + }; + + tvWidget?.activeChart().setVisibleRange(newRange, { percentRightMargin: 10 }); + }; +}; diff --git a/src/hooks/tradingView/useTradingView.ts b/src/hooks/tradingView/useTradingView.ts index dc8a641..0de4d18 100644 --- a/src/hooks/tradingView/useTradingView.ts +++ b/src/hooks/tradingView/useTradingView.ts @@ -1,17 +1,19 @@ -import { useEffect } from 'react'; +import React, { useEffect } from 'react'; + import { shallowEqual, useSelector } from 'react-redux'; + import isEmpty from 'lodash/isEmpty'; import { LanguageCode, ResolutionString, widget } from 'public/tradingview/charting_library'; import { DEFAULT_RESOLUTION } from '@/constants/candles'; import { SUPPORTED_LOCALE_BASE_TAGS, STRING_KEYS } from '@/constants/localization'; - import { LocalStorageKey } from '@/constants/localStorage'; +import type { TvWidget } from '@/constants/tvchart'; import { useDydxClient, useLocalStorage, useStringGetter } from '@/hooks'; -import { store } from '@/state/_store'; +import { store } from '@/state/_store'; import { getSelectedNetwork } from '@/state/appSelectors'; import { getAppTheme, getAppColorMode } from '@/state/configsSelectors'; import { getSelectedLocale } from '@/state/localizationSelectors'; @@ -28,8 +30,8 @@ export const useTradingView = ({ displayButtonRef, setIsChartReady, }: { - tvWidgetRef: React.MutableRefObject; - displayButtonRef: React.MutableRefObject; + tvWidgetRef: React.MutableRefObject; + displayButtonRef: React.MutableRefObject; setIsChartReady: React.Dispatch>; }) => { const stringGetter = useStringGetter(); @@ -69,15 +71,17 @@ export const useTradingView = ({ tvWidgetRef.current = tvChartWidget; tvWidgetRef.current.onChartReady(() => { - tvWidgetRef?.current?.headerReady().then(() => { - displayButtonRef.current = tvWidgetRef?.current?.createButton(); - displayButtonRef.current.innerHTML = `${stringGetter({ - key: STRING_KEYS.ORDER_LINES, - })}
`; - displayButtonRef.current.setAttribute( - 'title', - stringGetter({ key: STRING_KEYS.ORDER_LINES_TOOLTIP }) - ); + tvWidgetRef.current?.headerReady().then(() => { + if (displayButtonRef && tvWidgetRef.current) { + displayButtonRef.current = tvWidgetRef.current.createButton(); + displayButtonRef.current.innerHTML = `${stringGetter({ + key: STRING_KEYS.ORDER_LINES, + })}
`; + displayButtonRef.current.setAttribute( + 'title', + stringGetter({ key: STRING_KEYS.ORDER_LINES_TOOLTIP }) + ); + } }); tvWidgetRef?.current?.subscribe('onAutoSaveNeeded', () => diff --git a/src/hooks/tradingView/useTradingViewTheme.ts b/src/hooks/tradingView/useTradingViewTheme.ts index d9b4467..8930cbf 100644 --- a/src/hooks/tradingView/useTradingViewTheme.ts +++ b/src/hooks/tradingView/useTradingViewTheme.ts @@ -1,16 +1,14 @@ import { useEffect } from 'react'; + import { useSelector } from 'react-redux'; -import type { - IChartingLibraryWidget, - IOrderLineAdapter, - ThemeName, -} from 'public/tradingview/charting_library'; +import { THEME_NAMES } from '@/constants/styles/colors'; +import type { ChartLine, TvWidget } from '@/constants/tvchart'; import { AppColorMode, AppTheme } from '@/state/configs'; import { getAppTheme, getAppColorMode } from '@/state/configsSelectors'; -import { getWidgetOverrides, getOrderLineColors } from '@/lib/tradingView/utils'; +import { getWidgetOverrides, getChartLineColors } from '@/lib/tradingView/utils'; /** * @description Method to define a type guard and check that an element is an IFRAME @@ -26,12 +24,12 @@ const isIFrame = (element: HTMLElement | null): element is HTMLIFrameElement => * In order to support our Classic along with Dark/Light, we are directly accessing the within the iFrame. */ export const useTradingViewTheme = ({ - orderLines, + chartLines, tvWidget, isWidgetReady, }: { - orderLines: Record; - tvWidget: (IChartingLibraryWidget & { _id?: string; _ready?: boolean }) | null; + chartLines: Record; + tvWidget: TvWidget | null; isWidgetReady?: boolean; }) => { const appTheme: AppTheme = useSelector(getAppTheme); @@ -39,70 +37,64 @@ export const useTradingViewTheme = ({ useEffect(() => { if (tvWidget && isWidgetReady) { - tvWidget - .changeTheme?.( - { - [AppTheme.Classic]: '', - [AppTheme.Dark]: 'dark', - [AppTheme.Light]: 'light', - }[appTheme] as ThemeName - ) - .then(() => { - const tvChartId = tvWidget?._id; + tvWidget.changeTheme?.(THEME_NAMES[appTheme]).then(() => { + const tvChartId = tvWidget?._id; - if (tvChartId) { - const frame = document?.getElementById(tvChartId); + if (tvChartId) { + const frame = document?.getElementById(tvChartId); - if (isIFrame(frame) && frame.contentWindow) { - const innerHtml = frame.contentWindow.document.documentElement; - switch (appTheme) { - case AppTheme.Classic: - innerHtml?.classList.remove('theme-dark', 'theme-light'); - break; - case AppTheme.Dark: - innerHtml?.classList.remove('theme-light'); - innerHtml?.classList.add('theme-dark'); - break; - case AppTheme.Light: - innerHtml?.classList.remove('theme-dark'); - innerHtml?.classList.add('theme-light'); - } + if (isIFrame(frame) && frame.contentWindow) { + const innerHtml = frame.contentWindow.document.documentElement; + switch (appTheme) { + case AppTheme.Classic: + innerHtml?.classList.remove('theme-dark', 'theme-light'); + break; + case AppTheme.Dark: + innerHtml?.classList.remove('theme-light'); + innerHtml?.classList.add('theme-dark'); + break; + case AppTheme.Light: + innerHtml?.classList.remove('theme-dark'); + innerHtml?.classList.add('theme-light'); } } + } - const { overrides, studies_overrides } = getWidgetOverrides({ appTheme, appColorMode }); - tvWidget?.applyOverrides(overrides); - tvWidget?.applyStudiesOverrides(studies_overrides); + const { overrides, studies_overrides } = getWidgetOverrides({ appTheme, appColorMode }); + tvWidget?.applyOverrides(overrides); + tvWidget?.applyStudiesOverrides(studies_overrides); - // Necessary to update existing indicators - const volumeStudyId = tvWidget - ?.activeChart() - ?.getAllStudies() - ?.find((x) => x.name === 'Volume')?.id; + // Necessary to update existing indicators + const volumeStudyId = tvWidget + ?.activeChart() + ?.getAllStudies() + ?.find((x) => x.name === 'Volume')?.id; - if (volumeStudyId) { - const volume = tvWidget?.activeChart()?.getStudyById(volumeStudyId); - volume.applyOverrides({ - 'volume.color.0': studies_overrides['volume.volume.color.0'], - 'volume.color.1': studies_overrides['volume.volume.color.1'], - }); + if (volumeStudyId) { + const volume = tvWidget?.activeChart()?.getStudyById(volumeStudyId); + volume.applyOverrides({ + 'volume.color.0': studies_overrides['volume.volume.color.0'], + 'volume.color.1': studies_overrides['volume.volume.color.1'], + }); + } + + // Necessary to update existing chart lines + Object.values(chartLines).forEach(({ chartLineType, line }) => { + const { maybeQuantityColor, borderColor, backgroundColor, textColor, textButtonColor } = + getChartLineColors({ chartLineType: chartLineType, appTheme, appColorMode }); + + if (maybeQuantityColor) { + line.setLineColor(maybeQuantityColor).setQuantityBackgroundColor(maybeQuantityColor); } - // Necessary to update existing chart lines - Object.entries(orderLines).forEach(([key, line]) => { - const { orderColor, borderColor, backgroundColor, textColor, textButtonColor } = - getOrderLineColors({ side: key.split('-')[0], appTheme, appColorMode }); - - line - .setLineColor(orderColor) - .setQuantityBackgroundColor(orderColor) - .setQuantityBorderColor(borderColor) - .setBodyBackgroundColor(backgroundColor) - .setBodyBorderColor(borderColor) - .setBodyTextColor(textColor) - .setQuantityTextColor(textButtonColor); - }); + line + .setQuantityBorderColor(borderColor) + .setBodyBackgroundColor(backgroundColor) + .setBodyBorderColor(borderColor) + .setBodyTextColor(textColor) + .setQuantityTextColor(textButtonColor); }); + }); } }, [appTheme, appColorMode, isWidgetReady]); }; diff --git a/src/lib/tradingView/utils.ts b/src/lib/tradingView/utils.ts index 3e55537..4359731 100644 --- a/src/lib/tradingView/utils.ts +++ b/src/lib/tradingView/utils.ts @@ -1,8 +1,10 @@ import { OrderSide } from '@dydxprotocol/v4-client-js'; import { Candle, TradingViewBar, TradingViewSymbol } from '@/constants/candles'; +import { THEME_NAMES } from '@/constants/styles/colors'; +import type { ChartLineType } from '@/constants/tvchart'; -import { AppTheme, type AppColorMode } from '@/state/configs'; +import { type AppColorMode, AppTheme } from '@/state/configs'; import { Themes } from '@/styles/themes'; @@ -49,23 +51,24 @@ export const getHistorySlice = ({ return bars.filter(({ time }) => time >= fromMs); }; -export const getOrderLineColors = ({ +export const getChartLineColors = ({ appTheme, appColorMode, - side, + chartLineType, }: { appTheme: AppTheme; appColorMode: AppColorMode; - side: OrderSide; + chartLineType: ChartLineType; }) => { const theme = Themes[appTheme][appColorMode]; - const orderColor = { + const orderColors = { [OrderSide.BUY]: theme.positive, [OrderSide.SELL]: theme.negative, - }[side]; + ['position']: null, + }; return { - orderColor, + maybeQuantityColor: orderColors[chartLineType], borderColor: theme.borderDefault, backgroundColor: theme.layer1, textColor: theme.textTertiary, @@ -83,7 +86,7 @@ export const getWidgetOverrides = ({ const theme = Themes[appTheme][appColorMode]; return { - theme: appTheme === AppTheme.Dark ? 'dark' : AppTheme.Light ? 'light' : '', + theme: THEME_NAMES[appTheme], overrides: { 'paneProperties.background': theme.layer2, 'paneProperties.horzGridProperties.color': theme.layer3, diff --git a/src/views/charts/TvChart.tsx b/src/views/charts/TvChart.tsx index 783549c..45b4455 100644 --- a/src/views/charts/TvChart.tsx +++ b/src/views/charts/TvChart.tsx @@ -1,205 +1,40 @@ -import { useEffect, useRef, useState } from 'react'; +import { useRef, useState } from 'react'; -import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import styled, { type AnyStyledComponent, css } from 'styled-components'; -import type { - IChartingLibraryWidget, - IOrderLineAdapter, - ResolutionString, -} from 'public/tradingview/charting_library'; +import type { ResolutionString } from 'public/tradingview/charting_library'; -import { AbacusOrderStatus } from '@/constants/abacus'; -import { DEFAULT_RESOLUTION, RESOLUTION_CHART_CONFIGS } from '@/constants/candles'; -import { DEFAULT_MARKETID } from '@/constants/markets'; -import { type OrderType, ORDER_TYPE_STRINGS } from '@/constants/trade'; +import type { TvWidget } from '@/constants/tvchart'; -import { useStringGetter } from '@/hooks'; -import { useTradingView, useTradingViewTheme } from '@/hooks/tradingView'; +import { + useChartLines, + useChartMarketAndResolution, + useTradingView, + useTradingViewTheme, +} from '@/hooks/tradingView'; import { LoadingSpace } from '@/components/Loading/LoadingSpinner'; -import { getCurrentMarketOrders } from '@/state/accountSelectors'; -import { getAppTheme, getAppColorMode } from '@/state/configsSelectors'; -import { setTvChartResolution } from '@/state/perpetuals'; -import { getCurrentMarketId, getSelectedResolutionForMarket } from '@/state/perpetualsSelectors'; - import { layoutMixins } from '@/styles/layoutMixins'; -import { MustBigNumber } from '@/lib/numbers'; -import { getOrderLineColors } from '@/lib/tradingView/utils'; - -type TvWidget = IChartingLibraryWidget & { _id?: string; _ready?: boolean }; - -let orderLines: Record = {}; - export const TvChart = () => { - const dispatch = useDispatch(); - const stringGetter = useStringGetter(); - const [isChartReady, setIsChartReady] = useState(false); - const [showOrderLines, setShowOrderLines] = useState(false); - - const displayButtonRef = useRef(null); - - const appTheme = useSelector(getAppTheme); - const appColorMode = useSelector(getAppColorMode); - - const currentMarketId: string = useSelector(getCurrentMarketId) || DEFAULT_MARKETID; - const currentMarketOrders = useSelector(getCurrentMarketOrders, shallowEqual); - - const selectedResolution: string = - useSelector(getSelectedResolutionForMarket(currentMarketId)) || DEFAULT_RESOLUTION; const tvWidgetRef = useRef(null); const tvWidget = tvWidgetRef.current; const isWidgetReady = tvWidget?._ready; - const chart = isWidgetReady ? tvWidget?.chart() : undefined; - const chartResolution = chart?.resolution?.(); + + const displayButtonRef = useRef(null); + const displayButton = displayButtonRef.current; const { savedResolution } = useTradingView({ tvWidgetRef, displayButtonRef, setIsChartReady }); - useTradingViewTheme({ tvWidget, isWidgetReady, orderLines }); - - const setVisibleRangeForResolution = ({ resolution }: { resolution: ResolutionString }) => { - // Different resolutions have different timeframes to display data efficiently. - const { defaultRange } = RESOLUTION_CHART_CONFIGS[resolution]; - - // from/to values converted to epoch seconds - const newRange = { - from: (Date.now() - defaultRange) / 1000, - to: Date.now() / 1000, - }; - - tvWidget?.activeChart().setVisibleRange(newRange, { percentRightMargin: 10 }); - }; - - /** - * @description Hook to handle changing chart resolution - */ - useEffect(() => { - if (chartResolution) { - if (chartResolution !== selectedResolution) { - dispatch(setTvChartResolution({ marketId: currentMarketId, resolution: chartResolution })); - } - - setVisibleRangeForResolution({ resolution: chartResolution }); - } - }, [chartResolution]); - - /** - * @description Hook to handle changing markets - */ - useEffect(() => { - if (currentMarketId && isWidgetReady) { - const resolution = savedResolution || selectedResolution; - tvWidget?.setSymbol(currentMarketId, resolution as ResolutionString, () => {}); - } - }, [currentMarketId, isWidgetReady]); - - /** - * @description Hook to handle order line toggle state - */ - useEffect(() => { - if (isChartReady && displayButtonRef && displayButtonRef.current) { - displayButtonRef.current.onclick = () => { - const newShowOrderLinesState = !showOrderLines; - if (newShowOrderLinesState) { - displayButtonRef.current?.classList?.add('order-lines-active'); - } else { - displayButtonRef.current?.classList?.remove('order-lines-active'); - } - setShowOrderLines(newShowOrderLinesState); - }; - } - }, [isChartReady, showOrderLines]); - - /** - * @description Hook to handle drawing order lines - */ - useEffect(() => { - if (tvWidget && isChartReady) { - tvWidget.onChartReady(() => { - tvWidget.chart().dataReady(() => { - if (showOrderLines) { - drawOrderLines(); - } else { - deleteOrderLines(); - } - }); - }); - } - }, [isChartReady, showOrderLines, currentMarketOrders, currentMarketId]); - - const drawOrderLines = () => { - if (!currentMarketOrders) return; - currentMarketOrders.forEach( - ({ - id, - type, - status, - side, - cancelReason, - remainingSize, - size, - triggerPrice, - price, - trailingPercent, - }) => { - const key = `${side.rawValue}-${id}`; - const quantity = (remainingSize ?? size).toString(); - - const orderType = type.rawValue as OrderType; - const orderLabel = stringGetter({ - key: ORDER_TYPE_STRINGS[orderType].orderTypeKey, - }); - const orderString = trailingPercent ? `${orderLabel} ${trailingPercent}%` : orderLabel; - - const shouldShow = - !cancelReason && - (status === AbacusOrderStatus.open || status === AbacusOrderStatus.untriggered); - - const maybeOrderLine = key in orderLines ? orderLines[key] : null; - if (maybeOrderLine) { - if (!shouldShow) { - maybeOrderLine.remove(); - delete orderLines[key]; - return; - } else if (maybeOrderLine.getQuantity() !== quantity) { - maybeOrderLine.setQuantity(quantity); - return; - } - } else if (shouldShow) { - const { orderColor, borderColor, backgroundColor, textColor, textButtonColor } = - getOrderLineColors({ side: side.rawValue, appTheme, appColorMode }); - - const orderLine = tvWidget - ?.chart() - .createOrderLine({ disableUndo: false }) - .setPrice(MustBigNumber(triggerPrice ?? price).toNumber()) - .setQuantity(quantity) - .setText(orderString) - .setLineColor(orderColor) - .setQuantityBackgroundColor(orderColor) - .setQuantityBorderColor(borderColor) - .setBodyBackgroundColor(backgroundColor) - .setBodyBorderColor(borderColor) - .setBodyTextColor(textColor) - .setQuantityTextColor(textButtonColor); - - if (orderLine) { - orderLines[key] = orderLine; - } - } - } - ); - }; - - const deleteOrderLines = () => { - Object.values(orderLines).forEach((line) => { - line.remove(); - }); - orderLines = {}; - }; + useChartMarketAndResolution({ + tvWidget, + isWidgetReady, + savedResolution: savedResolution as ResolutionString | undefined, + }); + const { chartLines } = useChartLines({ tvWidget, displayButton, isChartReady }); + useTradingViewTheme({ tvWidget, isWidgetReady, chartLines }); return (