Merge pull request #34 from blockworks-foundation/feature/kline-charts-spot

Feature/kline chart
This commit is contained in:
tylersssss 2022-12-21 14:43:29 -05:00 committed by GitHub
commit d135f5e22c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 590 additions and 15 deletions

View File

@ -103,8 +103,10 @@ export default {
time_to: to,
}
const query = Object.keys(urlParameters)
//@ts-ignore
.map((name: any) => `${name}=${encodeURIComponent(urlParameters[name])}`)
.map(
(name: any) =>
`${name}=${encodeURIComponent((urlParameters as any)[name])}`
)
.join('&')
try {
const data = await makeApiRequest(`defi/ohlcv/pair?${query}`)

View File

@ -1,10 +1,12 @@
const BE_API_KEY =
export const BE_API_KEY =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2Njc1NTI4MzV9.FpbBT3M6GN_TKSJ8CarGeOMU5U7ZUvgZOIy8789m1bk'
export const API_URL = 'https://public-api.birdeye.so/'
// Make requests to CryptoCompare API
export async function makeApiRequest(path: string) {
try {
const response = await fetch(`https://public-api.birdeye.so/${path}`, {
const response = await fetch(`${API_URL}${path}`, {
headers: {
'X-API-KEY': BE_API_KEY,
},

View File

@ -6,7 +6,11 @@ import { useTranslation } from 'next-i18next'
import { useTheme } from 'next-themes'
// import { useRouter } from 'next/router'
// import { useCallback } from 'react'
import { NOTIFICATION_POSITION_KEY, SIZE_INPUT_UI_KEY } from 'utils/constants'
import {
NOTIFICATION_POSITION_KEY,
SIZE_INPUT_UI_KEY,
TRADE_CHART_UI_KEY,
} from 'utils/constants'
const NOTIFICATION_POSITIONS = [
'bottom-left',
@ -54,6 +58,10 @@ const DisplaySettings = () => {
SIZE_INPUT_UI_KEY,
'Slider'
)
const [tradeChartUi, setTradeChartUi] = useLocalStorageState(
TRADE_CHART_UI_KEY,
'Trading'
)
// const handleLangChange = useCallback(
// (l: string) => {
@ -117,6 +125,16 @@ const DisplaySettings = () => {
/>
</div>
</div>
<div className="flex flex-col border-t border-th-bkg-3 py-4 md:flex-row md:items-center md:justify-between md:px-4">
<p className="mb-2 lg:mb-0">{t('settings:trading-chart')}</p>
<div className="w-full min-w-[160px] md:w-auto">
<ButtonGroup
activeValue={tradeChartUi}
onChange={(v) => setTradeChartUi(v)}
values={[t('settings:original'), t('settings:trading')]}
/>
</div>
</div>
</>
)
}

View File

@ -1,7 +1,6 @@
import { useMemo } from 'react'
import dynamic from 'next/dynamic'
import ReactGridLayout, { Responsive, WidthProvider } from 'react-grid-layout'
import mangoStore from '@store/mangoStore'
// import { IS_ONBOARDED_KEY } from 'utils/constants'
// import useLocalStorageState from 'hooks/useLocalStorageState'
@ -16,7 +15,7 @@ import OrderbookAndTrades from './OrderbookAndTrades'
// import TradeOnboardingTour from '@components/tours/TradeOnboardingTour'
import FavoriteMarketsBar from './FavoriteMarketsBar'
const TradingViewChart = dynamic(() => import('./TradingViewChart'), {
const TradingChartContainer = dynamic(() => import('./TradingChartContainer'), {
ssr: false,
})
@ -178,7 +177,7 @@ const TradeAdvancedPage = () => {
className="h-full border border-x-0 border-th-bkg-3"
>
<div className={`relative h-full overflow-auto`}>
<TradingViewChart />
<TradingChartContainer />
</div>
</div>
<div key="balances">

View File

@ -0,0 +1,26 @@
import useLocalStorageState from 'hooks/useLocalStorageState'
import dynamic from 'next/dynamic'
import { TRADE_CHART_UI_KEY } from 'utils/constants'
const TradingViewChart = dynamic(() => import('./TradingViewChart'), {
ssr: false,
})
const TradingViewChartKlineContainer = dynamic(
() => import('./TradingViewChartKlineContainer'),
{
ssr: false,
}
)
const TradingChartContainer = () => {
const [tradingChart] = useLocalStorageState(TRADE_CHART_UI_KEY)
const isTradingChart = tradingChart === 'Original'
return !isTradingChart ? (
<TradingViewChart></TradingViewChart>
) : (
<TradingViewChartKlineContainer></TradingViewChartKlineContainer>
)
}
export default TradingChartContainer

View File

@ -0,0 +1,359 @@
import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react'
import mangoStore from '@store/mangoStore'
import klinecharts, { init, dispose } from 'klinecharts'
import axios from 'axios'
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,
HISTORY,
mainTechnicalIndicatorTypes,
MAIN_INDICATOR_CLASS,
ONE_DAY_SECONDS,
RES_NAME_TO_RES_VAL,
subTechnicalIndicatorTypes,
} from 'utils/kLineChart'
import { ArrowsPointingOutIcon } from '@heroicons/react/24/outline'
import Loading from '@components/shared/Loading'
import clsx from 'clsx'
import { API_URL, BE_API_KEY } from 'apis/birdeye/helpers'
const UPDATE_INTERVAL = 10000
type Props = {
setIsFullView?: Dispatch<SetStateAction<boolean>>
isFullView?: boolean
}
const TradingViewChartKline = ({ setIsFullView, isFullView }: Props) => {
const { width } = useViewport()
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<klinecharts.Chart | null>(null)
const previousChart = usePrevious(chart)
const [baseChartQuery, setQuery] = useState<BASE_CHART_QUERY | null>(null)
const clearTimerRef = useRef<NodeJS.Timeout | null>(null)
const fetchData = async (baseQuery: BASE_CHART_QUERY, from: number) => {
try {
setIsLoading(true)
const query: CHART_QUERY = {
...baseQuery,
time_from: from,
}
const response = await axios.get(`${API_URL}defi/ohlcv/pair`, {
params: query,
headers: {
'X-API-KEY': BE_API_KEY,
},
})
const newData = response.data.data.items as HISTORY[]
const dataSize = newData.length
const dataList = []
for (let i = 0; i < dataSize; i++) {
const row = newData[i]
const kLineModel = {
open: row.o,
low: row.l,
high: row.h,
close: row.c,
volume: row.v,
timestamp: row.unixTime * 1000,
}
dataList.push(kLineModel)
}
setIsLoading(false)
return dataList
} catch (e) {
setIsLoading(false)
console.log(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.log('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 style = getComputedStyle(document.body)
const gridColor = style.getPropertyValue('--bkg-3')
const kLineChart = init('update-k-line')
kLineChart.setStyleOptions({
grid: {
show: true,
horizontal: {
style: 'solid',
color: gridColor,
},
vertical: {
style: 'solid',
color: gridColor,
},
},
candle: {
tooltip: {
labels: ['T: ', 'O: ', 'C: ', 'H: ', 'L: ', 'V: '],
},
},
xAxis: {
axisLine: {
show: true,
color: gridColor,
size: 1,
},
},
yAxis: {
axisLine: {
show: true,
color: gridColor,
size: 1,
},
},
separator: {
size: 2,
color: gridColor,
},
})
setChart(kLineChart)
}
initKline()
return () => {
dispose('update-k-line')
}
}, [])
return (
<div
className={clsx(
'fixed h-full w-full',
isFullView &&
'left-[64px] top-0 right-0 bottom-0 bg-th-bkg-1 text-th-fgd-1'
)}
>
<div className="flex w-full">
{Object.keys(RES_NAME_TO_RES_VAL).map((key) => (
<div
className={clsx(
'cursor-pointer py-1 px-2',
resolution === RES_NAME_TO_RES_VAL[key] && 'text-th-active'
)}
key={key}
onClick={() => setResolution(RES_NAME_TO_RES_VAL[key])}
>
{key}
</div>
))}
<div
className="cursor-pointer py-1 px-2 "
onClick={() => setIsTechnicalModalOpen(true)}
>
Indicator
</div>
{setIsFullView && (
<div className="cursor-pointer py-1 px-2">
<ArrowsPointingOutIcon
onClick={() => setIsFullView(!isFullView)}
className="w-5"
></ArrowsPointingOutIcon>
</div>
)}
<div className="py-1 px-2">
{isLoading && <Loading className="w-4"></Loading>}
</div>
</div>
<div
style={{ height: 'calc(100% - 30px)', width: '100%' }}
id="update-k-line"
className="k-line-chart"
/>
<Modal
isOpen={isTechnicalModalOpen}
onClose={() => setIsTechnicalModalOpen(false)}
>
<div className="flex max-h-96 flex-col overflow-auto text-left">
<h2 className="py-4">Main Indicator</h2>
{mainTechnicalIndicatorTypes.map((type) => {
return (
<IndicatorSwitch
key={type}
type={type}
checked={!!mainTechnicalIndicators.find((x) => 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),
])
}
}}
></IndicatorSwitch>
)
})}
<h2 className="py-4">Sub Indicator</h2>
{subTechnicalIndicatorTypes.map((type) => {
return (
<IndicatorSwitch
key={type}
type={type}
checked={
!!Object.keys(subTechnicalIndicators).find((x) => 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)
}
}}
></IndicatorSwitch>
)
})}
</div>
</Modal>
</div>
)
}
const IndicatorSwitch = ({
type,
onChange,
checked,
}: {
type: string
onChange: (checked: boolean) => void
checked: boolean
}) => {
return (
<div
className="flex justify-between border-t border-th-bkg-3 p-4 text-th-fgd-4"
key={type}
>
{type}
<Switch checked={checked} onChange={onChange} />
</div>
)
}
export default TradingViewChartKline

View File

@ -0,0 +1,31 @@
import dynamic from 'next/dynamic'
import { useState } from 'react'
import { createPortal } from 'react-dom'
const TradingViewChartKline = dynamic(() => import('./TradingViewChartKline'), {
ssr: false,
})
const TradingViewChartKlineContainer = () => {
const [isFullView, setIsFullView] = useState<boolean>(false)
return (
<>
{isFullView ? (
createPortal(
<TradingViewChartKline
setIsFullView={setIsFullView}
isFullView={isFullView}
></TradingViewChartKline>,
document.body
)
) : (
<TradingViewChartKline
setIsFullView={setIsFullView}
isFullView={isFullView}
></TradingViewChartKline>
)}
</>
)
}
export default TradingViewChartKlineContainer

View File

@ -25,13 +25,16 @@
"@types/howler": "^2.2.7",
"@types/lodash": "^4.14.185",
"assert": "^2.0.0",
"axios": "^1.2.0",
"big.js": "^6.2.1",
"clsx": "^1.2.1",
"date-fns": "^2.29.3",
"dayjs": "^1.11.3",
"decimal.js": "^10.4.0",
"howler": "^2.2.3",
"html-react-parser": "^3.0.4",
"immer": "^9.0.12",
"klinecharts": "^8.6.3",
"lodash": "^4.17.21",
"next": "^13.0.0",
"next-i18next": "^11.1.1",

View File

@ -38,5 +38,8 @@
"top-left": "Top-Left",
"top-right": "Top-Right",
"transaction-fail": "Transaction Fail",
"transaction-success": "Transaction Success"
"transaction-success": "Transaction Success",
"original": "Original",
"trading": "Trading",
"trading-chart": "Trading Chart"
}

View File

@ -38,5 +38,8 @@
"top-left": "Top-Left",
"top-right": "Top-Right",
"transaction-fail": "Transaction Fail",
"transaction-success": "Transaction Success"
"transaction-success": "Transaction Success",
"original": "Original",
"trading": "Trading",
"trading-chart": "Trading Chart"
}

View File

@ -38,5 +38,8 @@
"top-left": "Top-Left",
"top-right": "Top-Right",
"transaction-fail": "Transaction Fail",
"transaction-success": "Transaction Success"
"transaction-success": "Transaction Success",
"original": "Original",
"trading": "Trading",
"trading-chart": "Trading Chart"
}

View File

@ -38,5 +38,8 @@
"top-left": "Top-Left",
"top-right": "Top-Right",
"transaction-fail": "Transaction Fail",
"transaction-success": "Transaction Success"
"transaction-success": "Transaction Success",
"original": "Original",
"trading": "Trading",
"trading-chart": "Trading Chart"
}

View File

@ -38,5 +38,8 @@
"top-left": "Top-Left",
"top-right": "Top-Right",
"transaction-fail": "Transaction Fail",
"transaction-success": "Transaction Success"
"transaction-success": "Transaction Success",
"original": "Original",
"trading": "Trading",
"trading-chart": "Trading Chart"
}

View File

@ -33,6 +33,7 @@ export const GRID_LAYOUT_KEY = 'savedLayouts-0.2'
export const NOTIFICATION_POSITION_KEY = 'notificationPosition-0.1'
export const TRADE_CHART_UI_KEY = 'tradeChart'
export const FAVORITE_MARKETS_KEY = 'favoriteMarkets-0.2'
export const THEME_KEY = 'theme-0.1'

62
utils/kLineChart.ts Normal file
View File

@ -0,0 +1,62 @@
export const ONE_HOUR_MINS = 60
export const ONE_MINUTE_SECONDS = 60
export const ONE_HOUR_SECONDS = ONE_HOUR_MINS * ONE_MINUTE_SECONDS
export const ONE_DAY_SECONDS = ONE_HOUR_SECONDS * 24
export type BASE_CHART_QUERY = {
address: string
type: string
time_to: number
}
export type CHART_QUERY = BASE_CHART_QUERY & {
time_from: number
}
export type HISTORY = {
c: number
h: number
l: number
o: number
unixTime: number
v: number
}
//Translate values that api accepts to chart seconds
export const RES_NAME_TO_RES_VAL: {
[key: string]: {
val: string
seconds: number
}
} = {
'1m': { val: '1M', seconds: ONE_MINUTE_SECONDS },
'5m': { val: '5M', seconds: 5 * ONE_MINUTE_SECONDS },
'30m': {
val: `30M`,
seconds: (ONE_HOUR_MINS / 2) * ONE_MINUTE_SECONDS,
},
'1H': { val: `1H`, seconds: ONE_HOUR_SECONDS },
'2H': { val: `2H`, seconds: ONE_HOUR_SECONDS * 2 },
'4H': { val: `4H`, seconds: ONE_HOUR_SECONDS * 4 },
'1D': { val: '1D', seconds: 24 * ONE_HOUR_SECONDS },
}
export const mainTechnicalIndicatorTypes = [
'MA',
'EMA',
'SAR',
'BOLL',
'SMA',
'BBI',
'TRIX',
]
export const subTechnicalIndicatorTypes = [
'VOL',
'MACD',
'RSI',
'KDJ',
'OBV',
'CCI',
'WR',
'DMI',
'MTM',
'EMV',
]
export const DEFAULT_SUB_INDICATOR = 'VOL'
export const DEFAULT_MAIN_INDICATORS = ['EMA']
export const MAIN_INDICATOR_CLASS = 'candle_pane'

View File

@ -2177,6 +2177,11 @@ async-mutex@^0.3.2:
dependencies:
tslib "^2.3.1"
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
atomic-sleep@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b"
@ -2218,6 +2223,15 @@ axios@^0.25.0:
dependencies:
follow-redirects "^1.14.7"
axios@^1.2.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.2.1.tgz#44cf04a3c9f0c2252ebd85975361c026cb9f864a"
integrity sha512-I88cFiGu9ryt/tfVEi4kX2SITsvDddTajXTOFmt2uK1ZVA8LytjtdeyefdQWEf5PU8w+4SSJDoYnggflB5tW4A==
dependencies:
follow-redirects "^1.15.0"
form-data "^4.0.0"
proxy-from-env "^1.1.0"
axobject-query@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be"
@ -2635,7 +2649,7 @@ cliui@^5.0.0:
strip-ansi "^5.2.0"
wrap-ansi "^5.1.0"
clsx@^1.1.1:
clsx@^1.1.1, clsx@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
@ -2664,6 +2678,13 @@ color-name@^1.1.4, color-name@~1.1.4:
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
combined-stream@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
dependencies:
delayed-stream "~1.0.0"
commander@^2.20.3:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
@ -2932,6 +2953,11 @@ delay@^5.0.0:
resolved "https://registry.yarnpkg.com/delay/-/delay-5.0.0.tgz#137045ef1b96e5071060dd5be60bf9334436bd1d"
integrity sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
des.js@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843"
@ -3723,7 +3749,7 @@ flatted@^3.1.0:
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3"
integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==
follow-redirects@^1.14.0, follow-redirects@^1.14.7:
follow-redirects@^1.14.0, follow-redirects@^1.14.7, follow-redirects@^1.15.0:
version "1.15.2"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
@ -3733,6 +3759,15 @@ foreach@^2.0.5:
resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99"
integrity sha1-C+4AUBiusmDQo6865ljdATbsG5k=
form-data@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
mime-types "^2.1.12"
fraction.js@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950"
@ -4400,6 +4435,11 @@ keyvaluestorage-interface@^1.0.0:
resolved "https://registry.yarnpkg.com/keyvaluestorage-interface/-/keyvaluestorage-interface-1.0.0.tgz#13ebdf71f5284ad54be94bd1ad9ed79adad515ff"
integrity sha512-8t6Q3TclQ4uZynJY9IGr2+SsIGwK9JHcO6ootkHCGA0CrQCRy+VkouYNO2xicET6b9al7QKzpebNow+gkpCL8g==
klinecharts@^8.6.3:
version "8.6.3"
resolved "https://registry.yarnpkg.com/klinecharts/-/klinecharts-8.6.3.tgz#9ff2c40e31d86ca0600abc5fb8bf546c61daf130"
integrity sha512-hGDtWiMNywEDneZFmt+vZ6tOYutCDWV5FPBcXcn7L8kGwe73Q5yJayk8UzP9pIQSBWyxswWIySKh/BVFA6GhuQ==
language-subtag-registry@~0.3.2:
version "0.3.21"
resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz#04ac218bea46f04cb039084602c6da9e788dd45a"
@ -4559,6 +4599,18 @@ miller-rabin@^4.0.0:
bn.js "^4.0.0"
brorand "^1.0.1"
mime-db@1.52.0:
version "1.52.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
mime-types@^2.1.12:
version "2.1.35"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
dependencies:
mime-db "1.52.0"
mimic-fn@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
@ -5135,6 +5187,11 @@ prop-types@15.x, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2, pro
object-assign "^4.1.1"
react-is "^16.13.1"
proxy-from-env@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
ps-tree@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/ps-tree/-/ps-tree-1.2.0.tgz#5e7425b89508736cdd4f2224d028f7bb3f722ebd"