diff --git a/src/components/Asset/AssetActions/Pool/Graph.tsx b/src/components/Asset/AssetActions/Pool/Graph.tsx deleted file mode 100644 index 682defd04..000000000 --- a/src/components/Asset/AssetActions/Pool/Graph.tsx +++ /dev/null @@ -1,271 +0,0 @@ -import React, { - ChangeEvent, - ReactElement, - useCallback, - useEffect, - useState -} from 'react' -import { - Chart as ChartJS, - LinearScale, - CategoryScale, - PointElement, - Tooltip, - ChartData, - ChartOptions, - defaults, - ChartDataset, - TooltipOptions, - TooltipItem, - BarElement, - LineElement, - LineController, - BarController -} from 'chart.js' -import { Chart } from 'react-chartjs-2' -import Loader from '@shared/atoms/Loader' -import { formatPrice } from '@shared/Price/PriceUnit' -import { useUserPreferences } from '@context/UserPreferences' -import useDarkMode from 'use-dark-mode' -import { darkModeConfig } from '../../../../../app.config' -import Button from '@shared/atoms/Button' -import { LoggerInstance } from '@oceanprotocol/lib' -import { useAsset } from '@context/Asset' -import { gql, OperationResult } from 'urql' -import { PoolHistory } from '../../../../@types/subgraph/PoolHistory' -import { fetchData, getQueryContext } from '@utils/subgraph' -import styles from './Graph.module.css' -import Decimal from 'decimal.js' - -ChartJS.register( - LineElement, - BarElement, - PointElement, - LinearScale, - CategoryScale, - // Title, - Tooltip, - LineController, - BarController -) - -declare type GraphType = 'liquidity' | 'price' | 'volume' - -// Chart.js global defaults -defaults.font.family = `'Sharp Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif` -defaults.animation = { easing: 'easeInOutQuart', duration: 1000 } - -const REFETCH_INTERVAL = 10000 - -const lineStyle: Partial = { - fill: false, - borderWidth: 2, - pointBorderWidth: 0, - pointRadius: 0, - pointHoverRadius: 4, - pointHoverBorderWidth: 0, - pointHitRadius: 2, - pointHoverBackgroundColor: '#ff4092' -} - -const tooltipOptions: Partial = { - intersect: false, - displayColors: false, - padding: 10, - cornerRadius: 3, - borderWidth: 1, - caretSize: 7 -} - -function getOptions(locale: string, isDarkMode: boolean): ChartOptions { - return { - layout: { - padding: { - left: 0, - right: 0, - top: 0, - bottom: 10 - } - }, - plugins: { - tooltip: { - ...tooltipOptions, - backgroundColor: isDarkMode ? `#141414` : `#fff`, - titleColor: isDarkMode ? `#e2e2e2` : `#303030`, - bodyColor: isDarkMode ? `#fff` : `#141414`, - borderColor: isDarkMode ? `#41474e` : `#e2e2e2`, - callbacks: { - label: (tooltipItem: TooltipItem) => - `${formatPrice(`${tooltipItem.formattedValue}`, locale)} OCEAN` - } - } - }, - hover: { intersect: false }, - scales: { - y: { display: false }, - x: { display: false } - } - } -} - -const graphTypes = ['Liquidity', 'Price', 'Volume'] - -const poolHistoryQuery = gql` - query PoolHistory($id: String!) { - poolSnapshots(first: 1000, where: { pool: $id }, orderBy: date) { - date - spotPrice - baseTokenLiquidity - datatokenLiquidity - swapVolume - } - } -` - -export default function Graph(): ReactElement { - const { locale } = useUserPreferences() - const { price, ddo } = useAsset() - const darkMode = useDarkMode(false, darkModeConfig) - - const [options, setOptions] = useState() - const [graphType, setGraphType] = useState('liquidity') - const [error, setError] = useState() - const [isLoading, setIsLoading] = useState(true) - const [dataHistory, setDataHistory] = useState() - const [graphData, setGraphData] = useState() - const [graphFetchInterval, setGraphFetchInterval] = useState() - - const getPoolHistory = useCallback(async () => { - try { - const queryResult: OperationResult = await fetchData( - poolHistoryQuery, - { id: price.address.toLowerCase() }, - getQueryContext(ddo.chainId) - ) - setDataHistory(queryResult?.data) - } catch (error) { - console.error('Error fetchData: ', error.message) - setError(error) - } - }, [ddo?.chainId, price?.address]) - - const refetchGraph = useCallback(async () => { - if (graphFetchInterval) return - - const newInterval = setInterval(() => getPoolHistory(), REFETCH_INTERVAL) - setGraphFetchInterval(newInterval) - }, [getPoolHistory, graphFetchInterval]) - - useEffect(() => { - LoggerInstance.log('Fired GraphOptions!') - const options = getOptions(locale, darkMode.value) - setOptions(options) - }, [locale, darkMode.value]) - - useEffect(() => { - async function init() { - if (!dataHistory) { - await getPoolHistory() - return - } - LoggerInstance.log('Fired GraphData!') - - const timestamps = dataHistory.poolSnapshots.map((item) => { - const date = new Date(item.date * 1000) - return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}` - }) - - let baseTokenLiquidityCumulative = '0' - const liquidityHistory = dataHistory.poolSnapshots.map((item) => { - baseTokenLiquidityCumulative = new Decimal(baseTokenLiquidityCumulative) - .add(item.baseTokenLiquidity) - .toString() - return baseTokenLiquidityCumulative - }) - - const priceHistory = dataHistory.poolSnapshots.map( - (item) => item.spotPrice - ) - - let volumeCumulative = '0' - const volumeHistory = dataHistory.poolSnapshots.map((item) => { - volumeCumulative = new Decimal(volumeCumulative) - .add(item.swapVolume) - .toString() - return baseTokenLiquidityCumulative - }) - - let data - switch (graphType) { - case 'price': - data = priceHistory.slice(0) - break - case 'volume': - data = volumeHistory.slice(0) - break - default: - data = liquidityHistory.slice(0) - break - } - - setGraphData({ - labels: timestamps.slice(0), - datasets: [ - { - ...lineStyle, - label: 'Liquidity (OCEAN)', - data, - borderColor: `#8b98a9` - } - ] - }) - setIsLoading(false) - refetchGraph() - } - init() - - return () => clearInterval(graphFetchInterval) - }, [dataHistory, graphType, graphFetchInterval, getPoolHistory, refetchGraph]) - - function handleGraphTypeSwitch(e: ChangeEvent) { - e.preventDefault() - setGraphType(e.currentTarget.textContent.toLowerCase() as GraphType) - } - - return ( -
- {isLoading ? ( - - ) : error ? ( - {error.message} - ) : ( - <> - - - - - )} -
- ) -} diff --git a/src/components/Asset/AssetActions/Pool/Graph.module.css b/src/components/Asset/AssetActions/Pool/Graph/Nav.module.css similarity index 59% rename from src/components/Asset/AssetActions/Pool/Graph.module.css rename to src/components/Asset/AssetActions/Pool/Graph/Nav.module.css index 1aa5c5a32..27741c4be 100644 --- a/src/components/Asset/AssetActions/Pool/Graph.module.css +++ b/src/components/Asset/AssetActions/Pool/Graph/Nav.module.css @@ -1,24 +1,3 @@ -.graphWrap { - min-height: 97px; - display: flex; - align-items: center; - justify-content: center; - margin: calc(var(--spacer) / 6) -1.35rem calc(var(--spacer) / 1.5) -1.35rem; - position: relative; -} - -@media (min-width: 40rem) { - .graphWrap { - margin-left: -2rem; - margin-right: -2rem; - } -} - -.graphWrap canvas { - position: relative; - z-index: 0; -} - .type { position: absolute; bottom: -10px; diff --git a/src/components/Asset/AssetActions/Pool/Graph/Nav.tsx b/src/components/Asset/AssetActions/Pool/Graph/Nav.tsx new file mode 100644 index 000000000..4fef95a0c --- /dev/null +++ b/src/components/Asset/AssetActions/Pool/Graph/Nav.tsx @@ -0,0 +1,40 @@ +import Button from '@shared/atoms/Button' +import React, { + ChangeEvent, + Dispatch, + ReactElement, + SetStateAction +} from 'react' +import styles from './Nav.module.css' +import { graphTypes, GraphType } from './_constants' + +export default function Nav({ + graphType, + setGraphType +}: { + graphType: GraphType + setGraphType: Dispatch> +}): ReactElement { + function handleGraphTypeSwitch(e: ChangeEvent) { + e.preventDefault() + setGraphType(e.currentTarget.textContent.toLowerCase() as GraphType) + } + + return ( + + ) +} diff --git a/src/components/Asset/AssetActions/Pool/Graph/_constants.ts b/src/components/Asset/AssetActions/Pool/Graph/_constants.ts new file mode 100644 index 000000000..4716c1796 --- /dev/null +++ b/src/components/Asset/AssetActions/Pool/Graph/_constants.ts @@ -0,0 +1,75 @@ +import { formatPrice } from '@shared/Price/PriceUnit' +import { + ChartDataset, + TooltipOptions, + ChartOptions, + TooltipItem +} from 'chart.js' +import { gql } from 'urql' + +export declare type GraphType = 'liquidity' | 'price' | 'volume' + +export const poolHistoryQuery = gql` + query PoolHistory($id: String!) { + poolSnapshots(first: 1000, where: { pool: $id }, orderBy: date) { + date + spotPrice + baseTokenLiquidity + datatokenLiquidity + swapVolume + } + } +` + +export const lineStyle: Partial = { + fill: false, + borderWidth: 2, + pointBorderWidth: 0, + pointRadius: 0, + pointHoverRadius: 4, + pointHoverBorderWidth: 0, + pointHitRadius: 2, + pointHoverBackgroundColor: '#ff4092' +} + +export const tooltipOptions: Partial = { + intersect: false, + displayColors: false, + padding: 10, + cornerRadius: 3, + borderWidth: 1, + caretSize: 7 +} + +export function getOptions(locale: string, isDarkMode: boolean): ChartOptions { + return { + layout: { + padding: { + left: 0, + right: 0, + top: 0, + bottom: 10 + } + }, + plugins: { + tooltip: { + ...tooltipOptions, + backgroundColor: isDarkMode ? `#141414` : `#fff`, + titleColor: isDarkMode ? `#e2e2e2` : `#303030`, + bodyColor: isDarkMode ? `#fff` : `#141414`, + borderColor: isDarkMode ? `#41474e` : `#e2e2e2`, + callbacks: { + label: (tooltipItem: TooltipItem) => + `${formatPrice(`${tooltipItem.formattedValue}`, locale)} OCEAN` + } + } + }, + hover: { intersect: false }, + scales: { + y: { display: false }, + x: { display: false } + } + } +} + +export const graphTypes = ['Liquidity', 'Price', 'Volume'] diff --git a/src/components/Asset/AssetActions/Pool/Graph/index.module.css b/src/components/Asset/AssetActions/Pool/Graph/index.module.css new file mode 100644 index 000000000..12a4254b2 --- /dev/null +++ b/src/components/Asset/AssetActions/Pool/Graph/index.module.css @@ -0,0 +1,20 @@ +.graphWrap { + min-height: 97px; + display: flex; + align-items: center; + justify-content: center; + margin: calc(var(--spacer) / 6) -1.35rem calc(var(--spacer) / 1.5) -1.35rem; + position: relative; +} + +@media (min-width: 40rem) { + .graphWrap { + margin-left: -2rem; + margin-right: -2rem; + } +} + +.graphWrap canvas { + position: relative; + z-index: 0; +} diff --git a/src/components/Asset/AssetActions/Pool/Graph/index.tsx b/src/components/Asset/AssetActions/Pool/Graph/index.tsx new file mode 100644 index 000000000..556b03c22 --- /dev/null +++ b/src/components/Asset/AssetActions/Pool/Graph/index.tsx @@ -0,0 +1,202 @@ +import React, { + ChangeEvent, + ReactElement, + useCallback, + useEffect, + useState +} from 'react' +import { + Chart as ChartJS, + LinearScale, + CategoryScale, + PointElement, + Tooltip, + ChartData, + ChartOptions, + defaults, + ChartDataset, + TooltipOptions, + TooltipItem, + BarElement, + LineElement, + LineController, + BarController +} from 'chart.js' +import { Chart } from 'react-chartjs-2' +import Loader from '@shared/atoms/Loader' +import { useUserPreferences } from '@context/UserPreferences' +import useDarkMode from 'use-dark-mode' +import { darkModeConfig } from '../../../../../../app.config' +import { LoggerInstance } from '@oceanprotocol/lib' +import { useAsset } from '@context/Asset' +import { OperationResult } from 'urql' +import { PoolHistory } from '../../../../../@types/subgraph/PoolHistory' +import { fetchData, getQueryContext } from '@utils/subgraph' +import styles from './index.module.css' +import Decimal from 'decimal.js' +import { + poolHistoryQuery, + getOptions, + lineStyle, + GraphType +} from './_constants' +import Nav from './Nav' + +ChartJS.register( + LineElement, + BarElement, + PointElement, + LinearScale, + CategoryScale, + Tooltip, + LineController, + BarController +) + +// Chart.js global defaults +defaults.font.family = `'Sharp Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif` +defaults.animation = { easing: 'easeInOutQuart', duration: 1000 } + +export default function Graph(): ReactElement { + const { locale } = useUserPreferences() + const { price, ddo, refreshInterval } = useAsset() + const darkMode = useDarkMode(false, darkModeConfig) + + const [options, setOptions] = useState() + const [graphType, setGraphType] = useState('liquidity') + const [error, setError] = useState() + const [isLoading, setIsLoading] = useState(true) + const [dataHistory, setDataHistory] = useState() + const [graphData, setGraphData] = useState() + const [graphFetchInterval, setGraphFetchInterval] = useState() + + // Helper: fetch pool snapshots data + const fetchPoolHistory = useCallback(async () => { + try { + const queryResult: OperationResult = await fetchData( + poolHistoryQuery, + { id: price.address.toLowerCase() }, + getQueryContext(ddo.chainId) + ) + setDataHistory(queryResult?.data) + setIsLoading(false) + + LoggerInstance.log( + `[pool graph] Fetched pool snapshots:`, + queryResult?.data + ) + } catch (error) { + LoggerInstance.error('[pool graph] Error fetchData: ', error.message) + setError(error) + } + }, [ddo?.chainId, price?.address]) + + // Helper: start interval fetching + const initFetchInterval = useCallback(() => { + if (graphFetchInterval) return + + const newInterval = setInterval(() => { + fetchPoolHistory() + LoggerInstance.log( + `[pool graph] Refetch interval fired after ${refreshInterval / 1000}s` + ) + }, refreshInterval) + setGraphFetchInterval(newInterval) + }, [fetchPoolHistory, graphFetchInterval, refreshInterval]) + + useEffect(() => { + return () => clearInterval(graphFetchInterval) + }, [graphFetchInterval]) + + // + // 0 Get Graph options + // + useEffect(() => { + LoggerInstance.log('[pool graph] Fired getOptions() for color scheme.') + const options = getOptions(locale, darkMode.value) + setOptions(options) + }, [locale, darkMode.value]) + + // + // 1 Fetch all the data on mount + // All further effects depend on the fetched data + // and only do further data checking and manipulation. + // + useEffect(() => { + fetchPoolHistory() + initFetchInterval() + }, [fetchPoolHistory, initFetchInterval]) + + // + // 2 Data manipulation + // + useEffect(() => { + if (!dataHistory?.poolSnapshots) return + + const timestamps = dataHistory.poolSnapshots.map((item) => { + const date = new Date(item.date * 1000) + return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}` + }) + + let baseTokenLiquidityCumulative = '0' + const liquidityHistory = dataHistory.poolSnapshots.map((item) => { + baseTokenLiquidityCumulative = new Decimal(baseTokenLiquidityCumulative) + .add(item.baseTokenLiquidity) + .toString() + return baseTokenLiquidityCumulative + }) + + const priceHistory = dataHistory.poolSnapshots.map((item) => item.spotPrice) + + let volumeCumulative = '0' + const volumeHistory = dataHistory.poolSnapshots.map((item) => { + volumeCumulative = new Decimal(volumeCumulative) + .add(item.swapVolume) + .toString() + return baseTokenLiquidityCumulative + }) + + let data + switch (graphType) { + case 'price': + data = priceHistory.slice(0) + break + case 'volume': + data = volumeHistory.slice(0) + break + default: + data = liquidityHistory.slice(0) + break + } + + const newGraphData = { + labels: timestamps.slice(0), + datasets: [{ ...lineStyle, data, borderColor: `#8b98a9` }] + } + setGraphData(newGraphData) + LoggerInstance.log('[pool graph] New graph data:', newGraphData) + }, [dataHistory?.poolSnapshots, graphType]) + + return ( +
+ {isLoading ? ( + + ) : error ? ( + {error.message} + ) : ( + <> +
+ ) +}