Skip to content

Commit

Permalink
Charts + analytics (#1161)
Browse files Browse the repository at this point in the history
  • Loading branch information
Space-Bean authored Oct 21, 2024
2 parents c8a8931 + 418b69d commit c7b3b88
Show file tree
Hide file tree
Showing 73 changed files with 2,847 additions and 2,666 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@
"ex": "yarn workspace @beanstalk/examples x",
"anvil-arbitrum": "yarn cli:anvil-arbitrum",
"anvil-eth-mainnet": "yarn cli:anvil-eth-mainnet",
"anvil": "anvil --fork-url https://eth-mainnet.g.alchemy.com/v2/5ubn94zT7v7DnB5bNW1VOnoIbX5-AG2N --chain-id 1337",
"anvil4tests": "anvil --fork-url https://eth-mainnet.g.alchemy.com/v2/Kk7ktCQL5wz4v4AG8bR2Gun8TAASQ-qi --chain-id 1337 --fork-block-number 18629000"
"anvil": "anvil --fork-url https://arb-mainnet.g.alchemy.com/v2/5ubn94zT7v7DnB5bNW1VOnoIbX5-AG2N --chain-id 1337",
"anvil4tests": "anvil --fork-url https://arb-mainnet.g.alchemy.com/v2/Kk7ktCQL5wz4v4AG8bR2Gun8TAASQ-qi --chain-id 1337 --fork-block-number 18629000"
},
"dependencies": {
"prettier-plugin-solidity": "1.4.1"
Expand Down
14 changes: 9 additions & 5 deletions projects/sdk/src/lib/BeanstalkSDK.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,14 +135,18 @@ export class BeanstalkSDK {
}

this.signer = config.signer;
if (!config.provider && !config.signer) {

if (config.signer?.provider){
this.provider = config.signer.provider as Provider;
} else if (config.provider) {
this.provider = config.provider;
} else {
console.log("WARNING: No provider or signer specified, using DefaultProvider.");
this.provider = ethers.getDefaultProvider() as Provider;
} else {
this.provider = (config.signer?.provider as Provider) ?? config.provider!;
}
this.readProvider = config.readProvider;
this.providerOrSigner = config.signer ?? config.provider!;

this.readProvider = config.readProvider ?? this.provider;
this.providerOrSigner = this.signer ?? this.provider;

this.DEBUG = config.DEBUG ?? false;

Expand Down
2 changes: 1 addition & 1 deletion projects/sdk/src/utils/TestUtils/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const ACCOUNTS = [
export const getProvider = () =>
new ethers.providers.StaticJsonRpcProvider(`http://127.0.0.1:8545`, {
name: "foundry",
chainId: 41337 // default to arbitrum-local
chainId: 1337 // default to arbitrum-local
});

export const setupConnection = (provider: ethers.providers.JsonRpcProvider = getProvider()) => {
Expand Down
228 changes: 110 additions & 118 deletions projects/ui/src/components/Analytics/AdvancedChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ import { Box, Button, Card, CircularProgress, Drawer } from '@mui/material';
import AddRoundedIcon from '@mui/icons-material/AddRounded';
import CloseIcon from '@mui/icons-material/Close';
import useToggle from '~/hooks/display/useToggle';
import { apolloClient } from '~/graph/client';
import useSeason from '~/hooks/beanstalk/useSeason';
import { Range, Time } from 'lightweight-charts';
import { useQueries } from '@tanstack/react-query';
import { fetchAllSeasonData } from '~/util/Graph';
import { exists, mayFunctionToValue } from '~/util';
import { RESEED_SEASON } from '~/constants';
import useOnAnimationFrame from '~/hooks/display/useOnAnimationFrame';
import ChartV2 from './ChartV2';
import DropdownIcon from '../Common/DropdownIcon';
import SelectDialog from './SelectDialog';
Expand All @@ -27,130 +31,100 @@ const AdvancedChart: FC<{ isMobile?: boolean }> = ({ isMobile = false }) => {
const season = useSeason();
const chartSetupData = useChartSetupData();

// wait to mount before fetching data
const ready = useOnAnimationFrame();

const storedSetting1 = localStorage.getItem('advancedChartTimePeriod');
const storedTimePeriod = storedSetting1 ? JSON.parse(storedSetting1) : undefined;

const storedSetting2 = localStorage.getItem('advancedChartSelectedCharts');
const storedSelectedCharts = storedSetting2 ? JSON.parse(storedSetting2) : undefined;

const [timePeriod, setTimePeriod] = useState<Range<Time> | undefined>(storedTimePeriod);
const [selectedCharts, setSelectedCharts] = useState<number[]>(storedSelectedCharts || [0]);

const [dialogOpen, showDialog, hideDialog] = useToggle();
const [queryData, setQueryData] = useState<QueryData[][]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<boolean>(false);

useMemo(() => {
async function getSeasonData(getAllData?: boolean) {
const promises: any[] = [];
const output: any[] = [];
const timestamps = new Map();
const queries = useQueries({
queries: selectedCharts.map((chartId) => {
const params = chartSetupData[chartId];
const queryKey = ['analytics', params.id, season.toNumber()];
return {
queryKey,
queryFn: async () => {
const dataFormatter = params.dataFormatter;
const valueFormatter = params.valueFormatter;
const priceKey = params.priceScaleKey;
const timestamps = new Set<number>();

const maxRetries = 8
for (let retries = 0; retries < maxRetries; retries += 1) {
console.debug('[AdvancedChart] Fetching data...');
try {
for (let i = 0; i < selectedCharts.length; i += 1) {
const chartId = selectedCharts[i];
const queryConfig = chartSetupData[chartId].queryConfig;
const document = chartSetupData[chartId].document;
const entity = chartSetupData[chartId].documentEntity;
const allSeasonData = await fetchAllSeasonData(params, season.toNumber());
const output = allSeasonData.map((seasonData) => {
try {
const time = Number(seasonData[params.timeScaleKey]);
const data = dataFormatter ? dataFormatter?.(seasonData): seasonData;
const value = mayFunctionToValue<number>(
valueFormatter(data[priceKey]),
seasonData.season <= RESEED_SEASON - 1 ? 'l1' : 'l2'
);

const currentSeason = season.toNumber();
const invalidTime = !exists(time) || timestamps.has(time) || time <= 0;
if (invalidTime || !exists(value)) return undefined;

const iterations = getAllData ? Math.ceil(currentSeason / 1000) + 1 : 1;
for (let j = 0; j < iterations; j += 1) {
const startSeason = getAllData ? currentSeason - j * 1000 : 999999999;
if (startSeason <= 0) continue;
promises.push(
apolloClient
.query({
...queryConfig,
query: document,
variables: {
...queryConfig?.variables,
first: 1000,
season_lte: startSeason,
},
notifyOnNetworkStatusChange: true,
fetchPolicy: 'no-cache', // Hitting the network every time is MUCH faster than the cache
})
.then((r) => {
r.data[entity].forEach((seasonData: any) => {
if (seasonData?.season && seasonData.season) {
if (!output[chartId]?.length) {
output[chartId] = [];
}
if (!timestamps.has(seasonData.season)) {
timestamps.set(
seasonData.season,
Number(seasonData[chartSetupData[chartId].timeScaleKey])
);
};
// Some charts will occasionally return two seasons as having the
// same timestamp, here we ensure we only have one datapoint per timestamp
if (timestamps.get(seasonData.season + 1) !== timestamps.get(seasonData.season)
&& timestamps.get(seasonData.season - 1) !== timestamps.get(seasonData.season)
) {
const formattedTime = timestamps.get(seasonData.season);
const dataFormatter = chartSetupData[chartId].dataFormatter;
const _seasonData = dataFormatter ? dataFormatter(seasonData) : seasonData;
timestamps.add(time);

const formattedValue = chartSetupData[
chartId
].valueFormatter(
_seasonData[chartSetupData[chartId].priceScaleKey]
);
if (formattedTime > 0) {
output[chartId][_seasonData.season] = {
time: formattedTime,
value: formattedValue,
customValues: {
season: _seasonData.season
}
};
};
};
};
});
})
);
return {
time: time as Time,
value,
customValues: {
season: data.season,
},
} as QueryData;
} catch (e) {
console.debug(`[advancedChart] failed to process some data for ${queryKey}`, e);
return undefined;
}
};
await Promise.all(promises);
output.forEach((dataSet, index) => {
output[index] = dataSet.filter(Boolean);
});
setQueryData(output);
console.debug('[AdvancedChart] Fetched data successfully!');
break;
} catch (e) {
console.debug('[AdvancedChart] Failed to fetch data.');
console.error(e);
if (retries === maxRetries - 1) {
setError(true);
};
};
}).filter(Boolean) as QueryData[];

// Sort by time
const data = output.sort((a, b) => Number(a.time) - Number(b.time));
console.debug(`[advancedChart] ${queryKey}`, data);
return data as QueryData[];
},
retry: false,
enabled: ready && season.gt(0),
staleTime: Infinity,
};
};
}),
});

const error = useMemo(() => queries.find((a) => !!a.error)?.error, [queries]);

setLoading(true);
getSeasonData(true);
setLoading(false);
}, [chartSetupData, selectedCharts, season]);
const loading = queries.every((q) => q.isLoading) && queries.length > 0;

const queryData = useMemo(
() => queries.map((q) => q.data).filter(Boolean) as QueryData[][],
[queries]
);

function handleDeselectChart(selectionIndex: number) {
const newSelection = [...selectedCharts];
newSelection.splice(selectionIndex, 1);
setSelectedCharts(newSelection);
localStorage.setItem('advancedChartSelectedCharts', JSON.stringify(newSelection));
};
localStorage.setItem(
'advancedChartSelectedCharts',
JSON.stringify(newSelection)
);
}

return (
<>
<Box display="flex" flexDirection="row" gap={2}>
<Card sx={{ position: 'relative', width: '100%', height: '70vh', overflow: 'clip' }}>
<Card
sx={{
position: 'relative',
width: '100%',
height: '70vh',
overflow: 'clip',
}}
>
{!isMobile ? (
<Card
sx={{
Expand All @@ -162,7 +136,7 @@ const AdvancedChart: FC<{ isMobile?: boolean }> = ({ isMobile = false }) => {
marginTop: '-1px',
transition: 'left 0.3s',
borderRadius: 0,
borderLeftColor: 'transparent'
borderLeftColor: 'transparent',
}}
>
<SelectDialog
Expand Down Expand Up @@ -217,7 +191,9 @@ const AdvancedChart: FC<{ isMobile?: boolean }> = ({ isMobile = false }) => {
paddingX: 0.75,
}}
endIcon={
<CloseIcon sx={{ color: 'inherit', marginTop: '0.5px' }} />
<CloseIcon
sx={{ color: 'inherit', marginTop: '0.5px' }}
/>
}
onClick={() => handleDeselectChart(index)}
>
Expand Down Expand Up @@ -245,7 +221,12 @@ const AdvancedChart: FC<{ isMobile?: boolean }> = ({ isMobile = false }) => {
paddingY: 0.25,
paddingX: 0.75,
}}
endIcon={<DropdownIcon open={false} sx={{ fontSize: 20, marginTop: '0.5px' }} />}
endIcon={
<DropdownIcon
open={false}
sx={{ fontSize: 20, marginTop: '0.5px' }}
/>
}
onClick={() => showDialog()}
>
{selectedCharts.length === 1
Expand Down Expand Up @@ -278,7 +259,13 @@ const AdvancedChart: FC<{ isMobile?: boolean }> = ({ isMobile = false }) => {
color: 'primary.contrastText',
},
}}
endIcon={<AddRoundedIcon fontSize="small" color="inherit" sx={{ marginTop: '0.5px' }} />}
endIcon={
<AddRoundedIcon
fontSize="small"
color="inherit"
sx={{ marginTop: '0.5px' }}
/>
}
onClick={() => showDialog()}
>
Add Data
Expand All @@ -300,8 +287,7 @@ const AdvancedChart: FC<{ isMobile?: boolean }> = ({ isMobile = false }) => {
>
<CircularProgress variant="indeterminate" />
</Box>
) :
error ? (
) : error ? (
<Box
sx={{
display: 'flex',
Expand All @@ -312,18 +298,24 @@ const AdvancedChart: FC<{ isMobile?: boolean }> = ({ isMobile = false }) => {
>
Error fetching data
</Box>
) :
(
selectedCharts.length > 0 ? (
<ChartV2
formattedData={queryData}
selected={selectedCharts}
drawPegLine
timePeriod={timePeriod}
/>
) : (
<Box sx={{display: 'flex', height: '90%', justifyContent: 'center', alignItems: 'center'}}>Click the Add Data button to start charting</Box>
)
) : selectedCharts.length > 0 ? (
<ChartV2
formattedData={queryData}
selected={selectedCharts}
drawPegLine
timePeriod={timePeriod}
/>
) : (
<Box
sx={{
display: 'flex',
height: '90%',
justifyContent: 'center',
alignItems: 'center',
}}
>
Click the Add Data button to start charting
</Box>
)}
</Card>
</Box>
Expand Down
Loading

0 comments on commit c7b3b88

Please sign in to comment.