mirror of
https://github.com/oceanprotocol/market.git
synced 2024-12-02 05:57:29 +01:00
Liquidity & price history graph (#248)
* graph prototype * switch items * liquidity history graph prototype * more graph styling * epoch times conversion * get data in root component * redraw fix * more graph styling * loading fix * re-render fixes * re-render fixes * new Aquarius responses * price graph and switch buttons * spacing tweaks
This commit is contained in:
parent
f8a0ff41c3
commit
461fcaf8ae
44
package-lock.json
generated
44
package-lock.json
generated
@ -5333,6 +5333,15 @@
|
||||
"@types/responselike": "*"
|
||||
}
|
||||
},
|
||||
"@types/chart.js": {
|
||||
"version": "2.9.27",
|
||||
"resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.27.tgz",
|
||||
"integrity": "sha512-b3ho2RpPLWzLzOXKkFwpvlRDEVWQrCknu2/p90mLY5v2DO8owk0OwWkv4MqAC91kJL52bQGXkVw/De+N/0/1+A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"moment": "^2.10.2"
|
||||
}
|
||||
},
|
||||
"@types/classnames": {
|
||||
"version": "2.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.2.11.tgz",
|
||||
@ -10398,6 +10407,32 @@
|
||||
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
|
||||
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="
|
||||
},
|
||||
"chart.js": {
|
||||
"version": "2.9.4",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.9.4.tgz",
|
||||
"integrity": "sha512-B07aAzxcrikjAPyV+01j7BmOpxtQETxTSlQ26BEYJ+3iUkbNKaOJ/nDbT6JjyqYxseM0ON12COHYdU2cTIjC7A==",
|
||||
"requires": {
|
||||
"chartjs-color": "^2.1.0",
|
||||
"moment": "^2.10.2"
|
||||
}
|
||||
},
|
||||
"chartjs-color": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.4.1.tgz",
|
||||
"integrity": "sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==",
|
||||
"requires": {
|
||||
"chartjs-color-string": "^0.6.0",
|
||||
"color-convert": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"chartjs-color-string": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz",
|
||||
"integrity": "sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==",
|
||||
"requires": {
|
||||
"color-name": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"checkpoint-store": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/checkpoint-store/-/checkpoint-store-1.1.0.tgz",
|
||||
@ -27540,6 +27575,15 @@
|
||||
"vanilla-swipe": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"react-chartjs-2": {
|
||||
"version": "2.11.1",
|
||||
"resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-2.11.1.tgz",
|
||||
"integrity": "sha512-G7cNq/n2Bkh/v4vcI+GKx7Q1xwZexKYhOSj2HmrFXlvNeaURWXun6KlOUpEQwi1cv9Tgs4H3kGywDWMrX2kxfA==",
|
||||
"requires": {
|
||||
"lodash": "^4.17.19",
|
||||
"prop-types": "^15.7.2"
|
||||
}
|
||||
},
|
||||
"react-color": {
|
||||
"version": "2.19.3",
|
||||
"resolved": "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz",
|
||||
|
@ -35,6 +35,7 @@
|
||||
"@vercel/node": "^1.8.4",
|
||||
"@walletconnect/web3-provider": "^1.3.1",
|
||||
"axios": "^0.21.0",
|
||||
"chart.js": "^2.9.4",
|
||||
"classnames": "^2.2.6",
|
||||
"date-fns": "^2.16.1",
|
||||
"decimal.js": "^10.2.1",
|
||||
@ -66,6 +67,7 @@
|
||||
"query-string": "^6.13.7",
|
||||
"react": "^17.0.1",
|
||||
"react-alice-carousel": "^2.0.2",
|
||||
"react-chartjs-2": "^2.11.1",
|
||||
"react-data-table-component": "^6.11.5",
|
||||
"react-datepicker": "^3.3.0",
|
||||
"react-dom": "^17.0.1",
|
||||
@ -95,6 +97,7 @@
|
||||
"@svgr/webpack": "^5.4.0",
|
||||
"@testing-library/jest-dom": "^5.11.5",
|
||||
"@testing-library/react": "^11.1.1",
|
||||
"@types/chart.js": "^2.9.27",
|
||||
"@types/jest": "^26.0.15",
|
||||
"@types/loadable__component": "^5.13.1",
|
||||
"@types/lodash.debounce": "^4.0.3",
|
||||
|
@ -8,6 +8,15 @@ import Badge from '../Badge'
|
||||
|
||||
const cx = classNames.bind(styles)
|
||||
|
||||
export function formatPrice(price: string, locale: string): string {
|
||||
return formatCurrency(Number(price), '', locale, false, {
|
||||
// Not exactly clear what `significant figures` are for this library,
|
||||
// but setting this seems to give us the formatting we want.
|
||||
// See https://github.com/oceanprotocol/market/issues/70
|
||||
significantFigures: 4
|
||||
})
|
||||
}
|
||||
|
||||
export default function PriceUnit({
|
||||
price,
|
||||
className,
|
||||
@ -34,14 +43,7 @@ export default function PriceUnit({
|
||||
return (
|
||||
<div className={styleClasses}>
|
||||
<div>
|
||||
{Number.isNaN(Number(price))
|
||||
? '-'
|
||||
: formatCurrency(Number(price), '', locale, false, {
|
||||
// Not exactly clear what `significant figures` are for this library,
|
||||
// but setting this seems to give us the formatting we want.
|
||||
// See https://github.com/oceanprotocol/market/issues/70
|
||||
significantFigures: 4
|
||||
})}{' '}
|
||||
{Number.isNaN(Number(price)) ? '-' : formatPrice(price, locale)}{' '}
|
||||
<span className={styles.symbol}>{symbol || 'OCEAN'}</span>
|
||||
{type && type === 'pool' && (
|
||||
<Badge label="pool" className={styles.badge} />
|
||||
|
37
src/components/organisms/AssetActions/Pool/Graph.module.css
Normal file
37
src/components/organisms/AssetActions/Pool/Graph.module.css
Normal file
@ -0,0 +1,37 @@
|
||||
.graphWrap {
|
||||
min-height: 97px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: calc(var(--spacer) / 6) -2rem calc(var(--spacer) / 1.5) -2rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.graphWrap canvas {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.type {
|
||||
position: absolute;
|
||||
bottom: -10px;
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
padding: 5px var(--spacer);
|
||||
}
|
||||
|
||||
.button,
|
||||
.button:hover {
|
||||
display: inline-block;
|
||||
color: var(--color-secondary);
|
||||
font-size: var(--font-size-mini);
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--border-radius);
|
||||
padding: calc(var(--spacer) / 16) calc(var(--spacer) / 4) !important;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.button.active {
|
||||
color: var(--font-color-text);
|
||||
border-color: var(--border-color);
|
||||
}
|
184
src/components/organisms/AssetActions/Pool/Graph.tsx
Normal file
184
src/components/organisms/AssetActions/Pool/Graph.tsx
Normal file
@ -0,0 +1,184 @@
|
||||
import React, { ChangeEvent, ReactElement, useEffect, useState } from 'react'
|
||||
import { Line, defaults } from 'react-chartjs-2'
|
||||
import {
|
||||
ChartData,
|
||||
ChartDataSets,
|
||||
ChartOptions,
|
||||
ChartTooltipItem,
|
||||
ChartTooltipOptions
|
||||
} from 'chart.js'
|
||||
import styles from './Graph.module.css'
|
||||
import Loader from '../../../atoms/Loader'
|
||||
import { formatPrice } from '../../../atoms/Price/PriceUnit'
|
||||
import { useUserPreferences } from '../../../../providers/UserPreferences'
|
||||
import useDarkMode from 'use-dark-mode'
|
||||
import { darkModeConfig } from '../../../../../app.config'
|
||||
import Button from '../../../atoms/Button'
|
||||
import { Logger } from '@oceanprotocol/lib'
|
||||
|
||||
export interface ChartDataLiqudity {
|
||||
oceanAddRemove: ChartData[]
|
||||
datatokenAddRemove: ChartData[]
|
||||
oceanReserveHistory: ChartData[]
|
||||
datatokenReserveHistory: ChartData[]
|
||||
datatokenPriceHistory: ChartData[]
|
||||
}
|
||||
|
||||
declare type GraphType = 'liquidity' | 'price'
|
||||
|
||||
// Chart.js global defaults
|
||||
defaults.global.defaultFontFamily = `'Sharp Sans', -apple-system, BlinkMacSystemFont,
|
||||
'Segoe UI', Helvetica, Arial, sans-serif`
|
||||
defaults.global.animation = { easing: 'easeInOutQuart', duration: 800 }
|
||||
|
||||
const lineStyle: Partial<ChartDataSets> = {
|
||||
fill: false,
|
||||
lineTension: 0.1,
|
||||
borderWidth: 2,
|
||||
pointBorderWidth: 0,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 4,
|
||||
pointHoverBorderWidth: 0,
|
||||
pointHitRadius: 2,
|
||||
pointHoverBackgroundColor: '#ff4092'
|
||||
}
|
||||
|
||||
const tooltipOptions: Partial<ChartTooltipOptions> = {
|
||||
intersect: false,
|
||||
titleFontStyle: 'normal',
|
||||
titleFontSize: 10,
|
||||
bodyFontSize: 12,
|
||||
bodyFontStyle: 'bold',
|
||||
displayColors: false,
|
||||
xPadding: 10,
|
||||
yPadding: 10,
|
||||
cornerRadius: 3,
|
||||
borderWidth: 1,
|
||||
caretSize: 7
|
||||
}
|
||||
|
||||
function constructGraphData(data: ChartData[]): ChartData {
|
||||
const timestamps = data.map((item: any) => {
|
||||
// convert timestamps from epoch to locale date & time string
|
||||
const date = new Date(item[1] * 1000)
|
||||
return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`
|
||||
})
|
||||
const values = data.map((item: any) => item[0])
|
||||
|
||||
return {
|
||||
labels: timestamps,
|
||||
datasets: [
|
||||
{
|
||||
...lineStyle,
|
||||
label: 'Liquidity (OCEAN)',
|
||||
data: values,
|
||||
borderColor: `#8b98a9`,
|
||||
pointBackgroundColor: `#8b98a9`
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
function getOptions(locale: string, isDarkMode: boolean): ChartOptions {
|
||||
return {
|
||||
layout: {
|
||||
padding: {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 10
|
||||
}
|
||||
},
|
||||
tooltips: {
|
||||
...tooltipOptions,
|
||||
backgroundColor: isDarkMode ? `#141414` : `#fff`,
|
||||
titleFontColor: isDarkMode ? `#e2e2e2` : `#303030`,
|
||||
bodyFontColor: isDarkMode ? `#fff` : `#141414`,
|
||||
borderColor: isDarkMode ? `#41474e` : `#e2e2e2`,
|
||||
callbacks: {
|
||||
label: (tooltipItem: ChartTooltipItem) =>
|
||||
`${formatPrice(`${tooltipItem.yLabel}`, locale)} OCEAN`
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
scales: {
|
||||
yAxes: [
|
||||
{
|
||||
display: false
|
||||
// gridLines: {
|
||||
// drawBorder: false,
|
||||
// color: isDarkMode ? '#303030' : '#e2e2e2',
|
||||
// zeroLineColor: isDarkMode ? '#303030' : '#e2e2e2'
|
||||
// },
|
||||
// ticks: { display: false }
|
||||
}
|
||||
],
|
||||
xAxes: [{ display: false, gridLines: { display: true } }]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const graphTypes = ['Liquidity', 'Price']
|
||||
|
||||
export default function Graph({
|
||||
data
|
||||
}: {
|
||||
data: ChartDataLiqudity
|
||||
}): ReactElement {
|
||||
const { locale } = useUserPreferences()
|
||||
const darkMode = useDarkMode(false, darkModeConfig)
|
||||
|
||||
const [graphData, setGraphData] = useState<ChartData>()
|
||||
const [options, setOptions] = useState<ChartOptions>()
|
||||
const [graphType, setGraphType] = useState<GraphType>('liquidity')
|
||||
|
||||
useEffect(() => {
|
||||
Logger.log('Fired GraphOptions!')
|
||||
const options = getOptions(locale, darkMode.value)
|
||||
setOptions(options)
|
||||
}, [locale, darkMode.value])
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return
|
||||
Logger.log('Fired GraphData!')
|
||||
const graphData =
|
||||
graphType === 'liquidity'
|
||||
? constructGraphData(data.oceanReserveHistory)
|
||||
: constructGraphData(data.datatokenPriceHistory)
|
||||
setGraphData(graphData)
|
||||
}, [data, graphType])
|
||||
|
||||
function handleGraphTypeSwitch(e: ChangeEvent<HTMLButtonElement>) {
|
||||
e.preventDefault()
|
||||
setGraphType(e.currentTarget.textContent.toLowerCase() as GraphType)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.graphWrap}>
|
||||
{graphData ? (
|
||||
<>
|
||||
<nav className={styles.type}>
|
||||
{graphTypes.map((type: GraphType) => (
|
||||
<Button
|
||||
key={type}
|
||||
style="text"
|
||||
size="small"
|
||||
onClick={handleGraphTypeSwitch}
|
||||
className={`${styles.button} ${
|
||||
graphType === type.toLowerCase() ? styles.active : null
|
||||
}`}
|
||||
>
|
||||
{type}
|
||||
</Button>
|
||||
))}
|
||||
</nav>
|
||||
<Line height={70} data={graphData} options={options} />
|
||||
</>
|
||||
) : (
|
||||
<Loader />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -18,6 +18,8 @@ import Token from './Token'
|
||||
import TokenList from './TokenList'
|
||||
import { graphql, useStaticQuery } from 'gatsby'
|
||||
import Transactions from './Transactions'
|
||||
import Graph, { ChartDataLiqudity } from './Graph'
|
||||
import axios from 'axios'
|
||||
|
||||
export interface Balance {
|
||||
ocean: number
|
||||
@ -49,7 +51,7 @@ export default function Pool({ ddo }: { ddo: DDO }): ReactElement {
|
||||
const data = useStaticQuery(contentQuery)
|
||||
const content = data.content.edges[0].node.childContentJson.pool
|
||||
|
||||
const { ocean, accountId, networkId } = useOcean()
|
||||
const { ocean, accountId, networkId, config } = useOcean()
|
||||
const { price, refreshPrice, owner } = useMetadata(ddo)
|
||||
const { dtSymbol } = usePricing(ddo)
|
||||
const { isInPurgatory } = useAsset()
|
||||
@ -77,6 +79,8 @@ export default function Pool({ ddo }: { ddo: DDO }): ReactElement {
|
||||
const [creatorLiquidity, setCreatorLiquidity] = useState<Balance>()
|
||||
const [creatorPoolTokens, setCreatorPoolTokens] = useState<string>()
|
||||
const [creatorPoolShare, setCreatorPoolShare] = useState<string>()
|
||||
const [graphData, setGraphData] = useState<ChartDataLiqudity>()
|
||||
|
||||
// the purpose of the value is just to trigger the effect
|
||||
const [refreshPool, setRefreshPool] = useState(false)
|
||||
|
||||
@ -195,6 +199,34 @@ export default function Pool({ ddo }: { ddo: DDO }): ReactElement {
|
||||
return () => clearInterval(interval)
|
||||
}, [ocean, accountId, price, ddo, refreshPool, owner])
|
||||
|
||||
// Get graph history data
|
||||
useEffect(() => {
|
||||
if (!price?.address || !price?.ocean || !price?.value) return
|
||||
|
||||
const source = axios.CancelToken.source()
|
||||
const url = `${config.metadataCacheUri}/api/v1/aquarius/pools/history/${price.address}`
|
||||
|
||||
async function getData() {
|
||||
Logger.log('Fired GetGraphData!')
|
||||
try {
|
||||
const response = await axios(url, { cancelToken: source.token })
|
||||
if (!response || response.status !== 200) return
|
||||
setGraphData(response.data)
|
||||
} catch (error) {
|
||||
if (axios.isCancel(error)) {
|
||||
Logger.log(error.message)
|
||||
} else {
|
||||
Logger.error(error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
getData()
|
||||
|
||||
return () => {
|
||||
source.cancel()
|
||||
}
|
||||
}, [config.metadataCacheUri, price?.address, price?.ocean, price?.value])
|
||||
|
||||
const refreshInfo = async () => {
|
||||
setRefreshPool(!refreshPool)
|
||||
await refreshPrice()
|
||||
@ -284,6 +316,7 @@ export default function Pool({ ddo }: { ddo: DDO }): ReactElement {
|
||||
{weightOcean}/{weightDt}
|
||||
</span>
|
||||
)}
|
||||
<Graph data={graphData} />
|
||||
</>
|
||||
}
|
||||
ocean={`${price?.ocean}`}
|
||||
|
Loading…
Reference in New Issue
Block a user