Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show LTV histories on the Markets>Monitor page #795

Merged
merged 12 commits into from
Feb 7, 2024
Merged
25 changes: 1 addition & 24 deletions earn/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { Suspense, useEffect } from 'react';

import { ApolloClient, InMemoryCache, HttpLink, gql } from '@apollo/react-hooks';
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/react-hooks';
import { Route, Routes, Navigate, useNavigate } from 'react-router-dom';
import AccountBlockedModal from 'shared/lib/components/common/AccountBlockedModal';
import Footer from 'shared/lib/components/common/Footer';
Expand Down Expand Up @@ -167,7 +167,6 @@ function AppBodyWrapper() {

function App() {
const [activeChain, setActiveChain] = React.useState<Chain>(DEFAULT_CHAIN);
const [blockNumber, setBlockNumber] = useSafeState<string | null>(null);
const [accountRisk, setAccountRisk] = useSafeState<AccountRiskResult>({ isBlocked: false, isLoading: true });
const [geoFencingInfo, setGeoFencingInfo] = useSafeState<GeoFencingInfo>({
isAllowed: false,
Expand All @@ -179,18 +178,6 @@ function App() {
const provider = useProvider({ chainId: activeChain.id });

const value = { activeChain, setActiveChain };
const twentyFourHoursAgo = Date.now() / 1000 - 24 * 60 * 60;
const BLOCK_QUERY = gql`
{
blocks(first: 1, orderBy: timestamp, orderDirection: asc, where: {timestamp_gt: "${twentyFourHoursAgo.toFixed(
0
)}"}) {
id
number
timestamp
}
}
`;
haydenshively marked this conversation as resolved.
Show resolved Hide resolved

useEffectOnce(() => {
(async () => {
Expand All @@ -214,16 +201,6 @@ function App() {
})();
}, [userAddress, setAccountRisk]);

useEffect(() => {
const queryBlocks = async () => {
const response = await theGraphEthereumBlocksClient.query({ query: BLOCK_QUERY });
setBlockNumber(response.data.blocks[0].number);
};
if (blockNumber === null) {
queryBlocks();
}
});

useEffect(() => {
let mounted = true;

Expand Down
96 changes: 96 additions & 0 deletions earn/src/components/graph/LineGraph.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import React from 'react';

import { Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
import { CurveType } from 'recharts/types/shape/Curve';

export type GraphChart = {
uniqueId: string;
type: CurveType;
dataKey: string;
stroke: string;
strokeWidth: number;
strokeDasharray?: string;
activeDot?: JSX.Element;
};

export type GraphProps = {
data: any;
charts: GraphChart[];
linearGradients?: React.SVGProps<SVGLinearGradientElement>[];
CustomTooltip?: JSX.Element;
tooltipPosition?: { x: number | undefined; y: number | undefined };
tooltipOffset?: number;
tooltipCursor?: React.SVGProps<SVGElement>;
size?: { width: number | '100%'; height: number };
aspectRatio?: number;
};

export default function LineGraph(props: GraphProps) {
const { data, charts, linearGradients, CustomTooltip, tooltipPosition, tooltipOffset, tooltipCursor } = props;

const responsiveContainerProps = props.aspectRatio ? { aspect: props.aspectRatio } : props.size;

return (
<ResponsiveContainer {...responsiveContainerProps}>
<LineChart
data={data}
margin={{
top: 0,
left: -1,
bottom: -2,
right: -2,
}}
// @ts-ignore
baseValue={'dataMin'}
>
<defs>
{linearGradients &&
linearGradients.map((gradient, index) => <React.Fragment key={index}>{gradient}</React.Fragment>)}
</defs>
<XAxis
hide={true}
dataKey='x'
type='number'
domain={['dataMin', 'dataMax']}
axisLine={false}
tickLine={false}
/>
<YAxis
hide={true}
width={0}
height={0}
orientation='right'
type='number'
domain={['dataMin - 2', 'dataMax + 2']}
axisLine={false}
tickLine={false}
/>
<Tooltip
content={CustomTooltip}
allowEscapeViewBox={{ x: false, y: false }}
position={tooltipPosition}
offset={tooltipOffset}
cursor={tooltipCursor}
wrapperStyle={{ outline: 'none' }}
isAnimationActive={false}
/>
{charts.map((chart, index) => (
<Line
key={index}
id={chart.uniqueId}
type={chart.type}
dataKey={chart.dataKey}
legendType='none'
dot={false}
activeDot={chart.activeDot}
stroke={chart.stroke}
strokeWidth={chart.strokeWidth}
strokeDasharray={chart.strokeDasharray}
connectNulls={true}
isAnimationActive={false}
/>
))}
</LineChart>
</ResponsiveContainer>
);
}
177 changes: 177 additions & 0 deletions earn/src/components/info/InfoGraph.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { SVGProps, useEffect, useMemo, useState } from 'react';

import { Text } from 'shared/lib/components/common/Typography';
import { RESPONSIVE_BREAKPOINTS, RESPONSIVE_BREAKPOINT_TABLET } from 'shared/lib/data/constants/Breakpoints';
import { GREY_600 } from 'shared/lib/data/constants/Colors';
import styled from 'styled-components';

import { LendingPair } from '../../data/LendingPair';
import LineGraph, { GraphChart } from '../graph/LineGraph';
import InfoGraphTooltip from './InfoGraphTooltip';

const MOBILE_HEIGHT = '320';
const FULL_HEIGHT = '642';
const FULL_WIDTH = '260';
IanWoodard marked this conversation as resolved.
Show resolved Hide resolved

const TableContainer = styled.div`
overflow-x: auto;
border: 2px solid ${GREY_600};
border-radius: 6px;

height: 100%;
min-width: ${FULL_WIDTH}px;
max-width: ${FULL_WIDTH}px;
width: ${FULL_WIDTH}px;

@media (max-width: ${RESPONSIVE_BREAKPOINT_TABLET}) {
min-width: 100%;
max-width: 100%;
width: 100%;
}
`;

const Table = styled.table`
border-spacing: 0;
border-collapse: separate;
`;

const TableHeaderElement = styled.th`
border-bottom: 2px solid ${GREY_600};
`;

export type InfoGraphLabel = `${string}/${string}`;
export type InfoGraphData = Map<InfoGraphLabel, { x: Date; ltv: number }[]>;
export type InfoGraphColors = Map<InfoGraphLabel, { color0: string; color1: string }>;

function getWindowDimensions() {
const { innerWidth: width, innerHeight: height } = window;
return { width, height };
}

export default function InfoGraph(props: {
graphData: InfoGraphData | undefined;
graphColors: InfoGraphColors;
hoveredPair: LendingPair | undefined;
}) {
const { graphData, graphColors, hoveredPair } = props;
const labels = Array.from(graphData?.keys() ?? []);

const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions());
const isTabletOrBigger = windowDimensions.width > RESPONSIVE_BREAKPOINTS['TABLET'];

useEffect(() => {
const handleResize = () => setWindowDimensions(getWindowDimensions());
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);

const flattenedGraphData: { [k: InfoGraphLabel]: number; x: Date }[] = [];
graphData?.forEach((arr, label) =>
arr.forEach((point) => {
const entry = {
x: point.x,
[label]: point.ltv * 100,
} as { [k: InfoGraphLabel]: number; x: Date };

flattenedGraphData.push(entry);
})
);

flattenedGraphData.sort((a, b) => a.x.getTime() - b.x.getTime());
IanWoodard marked this conversation as resolved.
Show resolved Hide resolved

const displayedGraphData: { [k: InfoGraphLabel]: number; x: number }[] = [];
for (let i = 0; i < flattenedGraphData.length; i++) {
const entry = { ...flattenedGraphData[i], x: flattenedGraphData[i].x.getTime() };

if (entry.x !== displayedGraphData.at(-1)?.x) {
displayedGraphData.push({
...(displayedGraphData.at(-1) ?? {}),
...entry,
});
continue;
}

const previousEntry = displayedGraphData.at(-1)!;
Object.assign(previousEntry, entry);
}
IanWoodard marked this conversation as resolved.
Show resolved Hide resolved

const charts: GraphChart[] = useMemo(
() =>
labels.map((label) => {
const shouldBeColored =
hoveredPair === undefined || label === `${hoveredPair!.token0.symbol}/${hoveredPair!.token1.symbol}`;
let stroke = 'rgba(43, 64, 80, 0.5)';
if (shouldBeColored) {
stroke = graphColors.has(label) ? `url(#${label.replace('/', '-')})` : 'white';
}

const shouldBeThick = shouldBeColored && hoveredPair !== undefined;

return {
uniqueId: label,
dataKey: label,
stroke,
strokeWidth: shouldBeThick ? 4 : 2,
type: 'monotone',
};
}),
[labels, graphColors, hoveredPair]
);

const linearGradients = useMemo(() => {
const arr: SVGProps<SVGLinearGradientElement>[] = [];

graphColors.forEach((v, k) => {
arr.push(
<linearGradient id={k.replace('/', '-')} x1='0' y1='0' x2='0' y2='1'>
<stop offset='-29%' stopColor={v.color0} stopOpacity={1} />
<stop offset='75%' stopColor={v.color1} stopOpacity={1} />
</linearGradient>
);
});

return arr;
}, [graphColors]);

return (
<TableContainer>
<Table>
<thead className='text-start'>
<tr>
<TableHeaderElement className='px-4 py-2 text-center whitespace-nowrap'>
<Text size='M' weight='bold'>
LTV History
</Text>
</TableHeaderElement>
</tr>
</thead>
<tbody>
<tr>
<td>
{graphData && (
<LineGraph
linearGradients={linearGradients}
CustomTooltip={<InfoGraphTooltip />}
tooltipPosition={{ x: 0, y: 0 }}
charts={charts}
data={displayedGraphData}
size={
isTabletOrBigger
? {
width: Number(FULL_WIDTH),
height: Number(FULL_HEIGHT) - 48,
}
: {
width: windowDimensions.width - 32,
height: Number(MOBILE_HEIGHT) - 48,
}
}
/>
)}
</td>
</tr>
</tbody>
</Table>
</TableContainer>
);
}
55 changes: 55 additions & 0 deletions earn/src/components/info/InfoGraphTooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { format } from 'date-fns';
import { Text } from 'shared/lib/components/common/Typography';
import styled from 'styled-components';
import tw from 'twin.macro';

const TOOLTIP_BG_COLOR = 'rgba(0, 0, 0, 0.4)';
const TOOLTIP_BORDER_COLOR = 'rgba(255, 255, 255, 0.1)';
const TOOLTIP_TEXT_COLOR = 'rgba(130, 160, 182, 1)';

const TooltipContainer = styled.div`
${tw`rounded-md shadow-md`}
background: ${TOOLTIP_BG_COLOR};
border: 1px solid ${TOOLTIP_BORDER_COLOR};
`;

const TooltipTitleContainer = styled.div`
${tw`flex flex-col justify-center align-middle pt-3 px-3 pb-1`}
border-bottom: 1px solid ${TOOLTIP_BORDER_COLOR};
`;

export default function InfoGraphTooltip(data: any, active = false) {
if (!active || data.label === undefined) return null;

const datetime = new Date(data.label);
const formattedDate = datetime ? format(datetime, 'MMM dd, yyyy') : '';
const formattedTime = datetime ? format(datetime, 'hh:mm a') : '';

const payload = data.payload.concat().sort((a: any, b: any) => b.value - a.value);
const tooltipValues = payload.map((item: any, index: number) => {
return (
<div className='flex justify-between gap-2' key={index}>
<Text size='S' weight='medium' color={item.color}>
{item.name}
</Text>
<Text size='S' weight='medium' color={item.color}>
{item.value.toFixed(0)}%
</Text>
</div>
);
});

return (
<TooltipContainer>
<TooltipTitleContainer>
<Text size='XS' weight='medium' color={TOOLTIP_TEXT_COLOR}>
{formattedDate}
</Text>
<Text size='XS' weight='medium' color={TOOLTIP_TEXT_COLOR}>
{formattedTime}
</Text>
</TooltipTitleContainer>
<div className='flex flex-col justify-between gap-2 mt-1 px-3 pb-3'>{tooltipValues}</div>
</TooltipContainer>
);
}
Loading
Loading