1
0
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:
Matthias Kretschmann 2020-11-16 15:10:33 +01:00 committed by GitHub
parent f8a0ff41c3
commit 461fcaf8ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 312 additions and 9 deletions

44
package-lock.json generated
View File

@ -5333,6 +5333,15 @@
"@types/responselike": "*" "@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": { "@types/classnames": {
"version": "2.2.11", "version": "2.2.11",
"resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.2.11.tgz", "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", "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" "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": { "checkpoint-store": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/checkpoint-store/-/checkpoint-store-1.1.0.tgz", "resolved": "https://registry.npmjs.org/checkpoint-store/-/checkpoint-store-1.1.0.tgz",
@ -27540,6 +27575,15 @@
"vanilla-swipe": "^2.2.0" "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": { "react-color": {
"version": "2.19.3", "version": "2.19.3",
"resolved": "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz", "resolved": "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz",

View File

@ -35,6 +35,7 @@
"@vercel/node": "^1.8.4", "@vercel/node": "^1.8.4",
"@walletconnect/web3-provider": "^1.3.1", "@walletconnect/web3-provider": "^1.3.1",
"axios": "^0.21.0", "axios": "^0.21.0",
"chart.js": "^2.9.4",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"date-fns": "^2.16.1", "date-fns": "^2.16.1",
"decimal.js": "^10.2.1", "decimal.js": "^10.2.1",
@ -66,6 +67,7 @@
"query-string": "^6.13.7", "query-string": "^6.13.7",
"react": "^17.0.1", "react": "^17.0.1",
"react-alice-carousel": "^2.0.2", "react-alice-carousel": "^2.0.2",
"react-chartjs-2": "^2.11.1",
"react-data-table-component": "^6.11.5", "react-data-table-component": "^6.11.5",
"react-datepicker": "^3.3.0", "react-datepicker": "^3.3.0",
"react-dom": "^17.0.1", "react-dom": "^17.0.1",
@ -95,6 +97,7 @@
"@svgr/webpack": "^5.4.0", "@svgr/webpack": "^5.4.0",
"@testing-library/jest-dom": "^5.11.5", "@testing-library/jest-dom": "^5.11.5",
"@testing-library/react": "^11.1.1", "@testing-library/react": "^11.1.1",
"@types/chart.js": "^2.9.27",
"@types/jest": "^26.0.15", "@types/jest": "^26.0.15",
"@types/loadable__component": "^5.13.1", "@types/loadable__component": "^5.13.1",
"@types/lodash.debounce": "^4.0.3", "@types/lodash.debounce": "^4.0.3",

View File

@ -8,6 +8,15 @@ import Badge from '../Badge'
const cx = classNames.bind(styles) 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({ export default function PriceUnit({
price, price,
className, className,
@ -34,14 +43,7 @@ export default function PriceUnit({
return ( return (
<div className={styleClasses}> <div className={styleClasses}>
<div> <div>
{Number.isNaN(Number(price)) {Number.isNaN(Number(price)) ? '-' : formatPrice(price, locale)}{' '}
? '-'
: 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
})}{' '}
<span className={styles.symbol}>{symbol || 'OCEAN'}</span> <span className={styles.symbol}>{symbol || 'OCEAN'}</span>
{type && type === 'pool' && ( {type && type === 'pool' && (
<Badge label="pool" className={styles.badge} /> <Badge label="pool" className={styles.badge} />

View 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);
}

View 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>
)
}

View File

@ -18,6 +18,8 @@ import Token from './Token'
import TokenList from './TokenList' import TokenList from './TokenList'
import { graphql, useStaticQuery } from 'gatsby' import { graphql, useStaticQuery } from 'gatsby'
import Transactions from './Transactions' import Transactions from './Transactions'
import Graph, { ChartDataLiqudity } from './Graph'
import axios from 'axios'
export interface Balance { export interface Balance {
ocean: number ocean: number
@ -49,7 +51,7 @@ export default function Pool({ ddo }: { ddo: DDO }): ReactElement {
const data = useStaticQuery(contentQuery) const data = useStaticQuery(contentQuery)
const content = data.content.edges[0].node.childContentJson.pool 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 { price, refreshPrice, owner } = useMetadata(ddo)
const { dtSymbol } = usePricing(ddo) const { dtSymbol } = usePricing(ddo)
const { isInPurgatory } = useAsset() const { isInPurgatory } = useAsset()
@ -77,6 +79,8 @@ export default function Pool({ ddo }: { ddo: DDO }): ReactElement {
const [creatorLiquidity, setCreatorLiquidity] = useState<Balance>() const [creatorLiquidity, setCreatorLiquidity] = useState<Balance>()
const [creatorPoolTokens, setCreatorPoolTokens] = useState<string>() const [creatorPoolTokens, setCreatorPoolTokens] = useState<string>()
const [creatorPoolShare, setCreatorPoolShare] = useState<string>() const [creatorPoolShare, setCreatorPoolShare] = useState<string>()
const [graphData, setGraphData] = useState<ChartDataLiqudity>()
// the purpose of the value is just to trigger the effect // the purpose of the value is just to trigger the effect
const [refreshPool, setRefreshPool] = useState(false) const [refreshPool, setRefreshPool] = useState(false)
@ -195,6 +199,34 @@ export default function Pool({ ddo }: { ddo: DDO }): ReactElement {
return () => clearInterval(interval) return () => clearInterval(interval)
}, [ocean, accountId, price, ddo, refreshPool, owner]) }, [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 () => { const refreshInfo = async () => {
setRefreshPool(!refreshPool) setRefreshPool(!refreshPool)
await refreshPrice() await refreshPrice()
@ -284,6 +316,7 @@ export default function Pool({ ddo }: { ddo: DDO }): ReactElement {
{weightOcean}/{weightDt} {weightOcean}/{weightDt}
</span> </span>
)} )}
<Graph data={graphData} />
</> </>
} }
ocean={`${price?.ocean}`} ocean={`${price?.ocean}`}