Skip to content

Commit

Permalink
Show LTV histories on the Markets>Monitor page
Browse files Browse the repository at this point in the history
  • Loading branch information
haydenshively committed Feb 5, 2024
1 parent 51f6a4f commit b8d78b5
Show file tree
Hide file tree
Showing 11 changed files with 606 additions and 221 deletions.
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 [geoFencingResponse, setGeoFencingResponse] = React.useState<GeoFencingResponse | null>(null);
const [lendingPairs, setLendingPairs] = useChainDependentState<LendingPair[] | null>(null, activeChain.id);
Expand All @@ -176,18 +175,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
}
}
`;

useEffectOnce(() => {
let mounted = true;
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';

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());

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

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

0 comments on commit b8d78b5

Please sign in to comment.