import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react' import mangoStore from '@store/mangoStore' import klinecharts, { init, dispose } from 'klinecharts' import { useViewport } from 'hooks/useViewport' import usePrevious from '@components/shared/usePrevious' import Modal from '@components/shared/Modal' import Switch from '@components/forms/Switch' import { BASE_CHART_QUERY, CHART_QUERY, DEFAULT_MAIN_INDICATORS, DEFAULT_SUB_INDICATOR, mainTechnicalIndicatorTypes, MAIN_INDICATOR_CLASS, ONE_DAY_SECONDS, RES_NAME_TO_RES_VAL, subTechnicalIndicatorTypes, } from 'utils/kLineChart' import Loading from '@components/shared/Loading' import clsx from 'clsx' import { useTheme } from 'next-themes' import { COLORS } from 'styles/colors' import { IconButton } from '@components/shared/Button' import { ArrowsPointingOutIcon, XMarkIcon } from '@heroicons/react/20/solid' import { queryBars } from 'apis/birdeye/datafeed' const UPDATE_INTERVAL = 10000 type Props = { setIsFullView?: Dispatch> isFullView?: boolean } const TradingViewChartKline = ({ setIsFullView, isFullView }: Props) => { const { width } = useViewport() const { theme } = useTheme() const prevWidth = usePrevious(width) const selectedMarket = mangoStore((s) => s.selectedMarket.current) const selectedMarketName = selectedMarket?.name const [isTechnicalModalOpen, setIsTechnicalModalOpen] = useState(false) const [mainTechnicalIndicators, setMainTechnicalIndicators] = useState< string[] >([]) const [subTechnicalIndicators, setSubTechnicalIndicators] = useState<{ //indicatorName: class [indicatorName: string]: string }>({}) const [isLoading, setIsLoading] = useState(false) const [resolution, setResolution] = useState(RES_NAME_TO_RES_VAL['1H']) const [chart, setChart] = useState(null) const previousChart = usePrevious(chart) const [baseChartQuery, setQuery] = useState(null) const clearTimerRef = useRef(null) const fetchData = async (baseQuery: BASE_CHART_QUERY, from: number) => { try { setIsLoading(true) const query: CHART_QUERY = { ...baseQuery, time_from: from, } const response = await queryBars(query.address, query.type, { firstDataRequest: false, from: query.time_from, to: query.time_to, }) const dataSize = response.length const dataList = [] for (let i = 0; i < dataSize; i++) { const row = response[i] const kLineModel = { ...row, } dataList.push(kLineModel) } setIsLoading(false) return dataList } catch (e) { setIsLoading(false) console.error(e) return [] } } //update data every 10 secs function updateData( kLineChart: klinecharts.Chart, baseQuery: BASE_CHART_QUERY ) { if (clearTimerRef.current) { clearInterval(clearTimerRef.current) } clearTimerRef.current = setTimeout(async () => { if (kLineChart) { const from = baseQuery.time_to - resolution.seconds const newData = (await fetchData(baseQuery!, from))[0] if (newData) { newData.timestamp += UPDATE_INTERVAL kLineChart.updateData(newData) updateData(kLineChart, baseQuery) } } }, UPDATE_INTERVAL) } const fetchFreshData = async (daysToSubtractFromToday: number) => { const from = Math.floor(Date.now() / 1000) - ONE_DAY_SECONDS * daysToSubtractFromToday const data = await fetchData(baseChartQuery!, from) if (chart) { chart.applyNewData(data) //after we fetch fresh data start to update data every x seconds updateData(chart, baseChartQuery!) } } //size change useEffect(() => { if (width !== prevWidth && chart) { //wait for event que to be empty //to have current width setTimeout(() => { chart?.resize() }, 0) } }, [width]) //when base query change we refetch with fresh data useEffect(() => { if (chart && baseChartQuery) { fetchFreshData(14) //add callback to fetch more data when zoom out chart.loadMore(() => { try { fetchFreshData(365) } catch (e) { console.error('Error fetching new data') } chart.loadMore(() => null) }) } }, [baseChartQuery]) //change query based on market and resolution useEffect(() => { if (selectedMarketName && resolution) { setQuery({ type: resolution.val, address: '8BnEgHoWFysVcuFFX7QztDmzuH8r5ZFvyP3sYwn1XTh6', time_to: Math.floor(Date.now() / 1000), }) } }, [selectedMarketName, resolution]) // init default technical indicators after init of chart useEffect(() => { if (chart !== null && previousChart === null) { if (DEFAULT_SUB_INDICATOR) { const subId = chart.createTechnicalIndicator( DEFAULT_SUB_INDICATOR, true ) setSubTechnicalIndicators({ [DEFAULT_SUB_INDICATOR]: subId }) } if (DEFAULT_MAIN_INDICATORS?.length) { for (const type of DEFAULT_MAIN_INDICATORS) { chart?.createTechnicalIndicator(type, true, { id: MAIN_INDICATOR_CLASS, }) } setMainTechnicalIndicators(DEFAULT_MAIN_INDICATORS) } } }, [chart !== null]) //init chart without data useEffect(() => { const initKline = async () => { const kLineChart = init('update-k-line') kLineChart.setStyleOptions({ grid: { show: false, }, candle: { bar: { upColor: COLORS.UP[theme], downColor: COLORS.DOWN[theme], }, tooltip: { labels: ['', 'O:', 'C:', 'H:', 'L:', 'V:'], text: { size: 12, family: 'TT Mono', weight: 'normal', color: COLORS.FGD4[theme], marginLeft: 8, marginTop: 6, marginRight: 8, marginBottom: 0, }, }, priceMark: { show: true, high: { show: true, color: COLORS.FGD4[theme], textMargin: 5, textSize: 10, textFamily: 'TT Mono', textWeight: 'normal', }, low: { show: true, color: COLORS.FGD4[theme], textMargin: 5, textSize: 10, textFamily: 'TT Mono', textWeight: 'normal', }, last: { show: true, upColor: COLORS.BKG4[theme], downColor: COLORS.BKG4[theme], noChangeColor: COLORS.BKG4[theme], line: { show: true, // 'solid'|'dash' style: 'dash', dashValue: [4, 4], size: 1, }, text: { show: true, size: 10, paddingLeft: 2, paddingTop: 2, paddingRight: 2, paddingBottom: 2, color: '#FFFFFF', family: 'TT Mono', weight: 'normal', borderRadius: 2, }, }, }, }, xAxis: { axisLine: { show: true, color: COLORS.BKG4[theme], size: 1, }, tickLine: { show: true, size: 1, length: 3, color: COLORS.BKG4[theme], }, tickText: { show: true, color: COLORS.FGD4[theme], family: 'TT Mono', weight: 'normal', size: 10, }, }, yAxis: { axisLine: { show: true, color: COLORS.BKG4[theme], size: 1, }, tickLine: { show: true, size: 1, length: 3, color: COLORS.BKG4[theme], }, tickText: { show: true, color: COLORS.FGD4[theme], family: 'TT Mono', weight: 'normal', size: 10, }, }, crosshair: { show: true, horizontal: { show: true, line: { show: true, style: 'dash', dashValue: [4, 2], size: 1, color: COLORS.FGD4[theme], }, text: { show: true, color: '#FFFFFF', size: 10, family: 'TT Mono', weight: 'normal', paddingLeft: 2, paddingRight: 2, paddingTop: 2, paddingBottom: 2, borderSize: 1, borderColor: COLORS.FGD4[theme], borderRadius: 2, backgroundColor: COLORS.FGD4[theme], }, }, vertical: { show: true, line: { show: true, style: 'dash', dashValue: [4, 2], size: 1, color: COLORS.FGD4[theme], }, text: { show: true, color: '#FFFFFF', size: 10, family: 'TT Mono', weight: 'normal', paddingLeft: 2, paddingRight: 2, paddingTop: 2, paddingBottom: 2, borderSize: 1, borderColor: COLORS.FGD4[theme], borderRadius: 2, backgroundColor: COLORS.FGD4[theme], }, }, }, technicalIndicator: { margin: { top: 0.2, bottom: 0.1, }, bar: { upColor: COLORS.UP[theme], downColor: COLORS.DOWN[theme], noChangeColor: '#888888', }, line: { size: 1, colors: ['#FF9600', '#9D65C9', '#2196F3', '#E11D74', '#01C5C4'], }, circle: { upColor: '#26A69A', downColor: '#EF5350', noChangeColor: '#888888', }, lastValueMark: { show: false, text: { show: false, color: '#ffffff', size: 12, family: 'Helvetica Neue', weight: 'normal', paddingLeft: 3, paddingTop: 2, paddingRight: 3, paddingBottom: 2, borderRadius: 2, }, }, tooltip: { // 'always' | 'follow_cross' | 'none' showRule: 'always', // 'standard' | 'rect' showType: 'standard', showName: true, showParams: true, defaultValue: 'n/a', text: { size: 12, family: 'TT Mono', weight: 'normal', color: COLORS.FGD4[theme], marginTop: 6, marginRight: 8, marginBottom: 0, marginLeft: 8, }, }, }, separator: { size: 2, color: COLORS.BKG4[theme], }, }) setChart(kLineChart) } initKline() return () => { dispose('update-k-line') } }, []) return (
{Object.keys(RES_NAME_TO_RES_VAL).map((key) => ( ))}
{isLoading && }
{setIsFullView ? ( setIsFullView(!isFullView)} > {isFullView ? ( ) : ( )} ) : null}
setIsTechnicalModalOpen(false)} >

Main Indicator

{mainTechnicalIndicatorTypes.map((type) => { return ( x === type)} onChange={(check) => { if (check) { chart?.createTechnicalIndicator(type, true, { id: MAIN_INDICATOR_CLASS, }) setMainTechnicalIndicators([ ...mainTechnicalIndicators, type, ]) } else { chart?.removeTechnicalIndicator(MAIN_INDICATOR_CLASS, type) setMainTechnicalIndicators([ ...mainTechnicalIndicators.filter((x) => x !== type), ]) } }} > ) })}

Bottom Indicator

{subTechnicalIndicatorTypes.map((type) => { return ( x === type) } onChange={(check) => { if (check) { const subId = chart?.createTechnicalIndicator(type, true) setSubTechnicalIndicators({ ...subTechnicalIndicators, [type]: subId!, }) } else { chart?.removeTechnicalIndicator( subTechnicalIndicators[type], type ) const newItems = { ...subTechnicalIndicators } delete newItems[type] // or whichever key you want setSubTechnicalIndicators(newItems) } }} > ) })}
) } const IndicatorSwitch = ({ type, onChange, checked, }: { type: string onChange: (checked: boolean) => void checked: boolean }) => { return (
{type}
) } export default TradingViewChartKline