push progress

This commit is contained in:
Ian-Bright
2023-06-05 12:23:50 -06:00
parent d261fef888
commit 6d603e5861
33 changed files with 40974 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

29430
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

46
package.json Normal file
View File

@@ -0,0 +1,46 @@
{
"name": "bls-frontend",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.34",
"@types/react": "^18.2.8",
"@types/react-dom": "^18.2.4",
"moment": "^2.29.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-jss": "^10.10.0",
"react-scripts": "5.0.1",
"recharts": "^2.6.2",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

43
public/index.html Normal file
View File

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

BIN
public/logo192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

25
public/manifest.json Normal file
View File

@@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

3
public/robots.txt Normal file
View File

@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

100
src/App.tsx Normal file
View File

@@ -0,0 +1,100 @@
import Counter from 'components/Counter';
import { CHARTS, COUNTERS } from 'data';
import { useEffect, useState } from 'react';
import { createUseStyles } from 'react-jss';
import Flex from 'components/Flex';
import ChartWrapper from 'components/Charts/ChartWrapper';
import Typography from 'components/Typography';
const { REACT_APP_API_URL } = process.env;
const useStyles = createUseStyles({
card: {
backgroundColor: '#34383D',
borderRadius: '4px',
padding: '20px',
},
container: {
padding: '16px',
},
});
type Layout = {
charts: any[];
counters: any[];
};
function App() {
const styles = useStyles();
const [layout, setLayout] = useState<Layout>({
charts: [],
counters: [],
});
useEffect(() => {
(async () => {
const res = await fetch(`${REACT_APP_API_URL}/queries`);
const data = await res.json();
const charts = CHARTS.map((chart) => ({
...chart,
data: data[chart.queryId],
}));
const counters = COUNTERS.map((counter) => ({
...counter,
data: data[counter.queryId],
}));
setLayout({ counters, charts });
})();
}, []);
return (
<div className={styles.container}>
<Flex
justifyContent='center'
mb='32px'
mt='32px'
style={{ color: '#FCFCFC' }}
>
<Typography variant='h4'>BLS Wallet Analytics</Typography>
</Flex>
<Flex childFlex='1 0 calc(25% - 60px)' gap='6px' wrap='wrap'>
{layout.counters.map((counter) => (
<div className={styles.card} style={{ height: '200px' }}>
<Typography style={{ color: '#FCFCFC' }} variant='h6'>
{counter.title}
</Typography>
<Counter
counterKey={counter.counterKey}
data={counter.data}
label={counter.label}
/>
</div>
))}
</Flex>
<Flex childFlex='1 0 calc(50% - 60px)' gap='6px' mt='16px' wrap='wrap'>
{layout.charts.map((chart) => (
<div className={styles.card} style={{ height: '350px' }}>
<Typography
style={{ color: '#FCFCFC', marginBottom: '8px' }}
variant='h6'
>
{chart.title}
</Typography>
<ChartWrapper
color={chart.color}
chartType={chart.chartType}
data={chart.data}
stackBy={chart.stackBy}
xAxisTitle={chart.xAxisTitle}
xKey={chart.xKey}
yAxisTitle={chart.yAxisTitle}
yKey={chart.yKey}
/>
</div>
))}
</Flex>
</div>
);
}
export default App;

View File

@@ -0,0 +1,98 @@
import moment from 'moment';
import { useMemo } from 'react';
import {
Area,
AreaChart as RechartsAreaChart,
CartesianGrid,
ResponsiveContainer,
Tooltip as RechartsTooltip,
TooltipProps,
XAxis,
YAxis,
} from 'recharts';
import { determineTimeframe, formatLargeNumber, handleXKey } from 'utils';
import Tooltip from '../components/Tooltip';
import {
AxisChartProps,
PALETTE,
TICKMARK_FONT_SIZE,
X_AXIS_LABEL_INSTRUCTIONS,
Y_AXIS_LABEL_INSTRUCTIONS,
} from 'utils/constants';
type AreaChartProps = Omit<AxisChartProps, 'curveType'>;
export default function AreaChart({
color,
data,
grid,
scale,
stackBy,
xAxisTitle,
xKey,
yAxisTitle,
yKey,
}: AreaChartProps): JSX.Element {
const timeframe = useMemo(() => {
return determineTimeframe(stackBy ? data.formattedData : data, xKey);
}, [data, stackBy, xKey]);
return (
<ResponsiveContainer>
<RechartsAreaChart
data={stackBy ? data.formattedData : data}
margin={{ bottom: xAxisTitle ? 15 : 0, left: 5 }}
>
{grid && <CartesianGrid strokeDasharray='3 3' />}
<RechartsTooltip
content={(props: TooltipProps<number, string>) => (
<Tooltip {...props} xKey={xKey} />
)}
wrapperStyle={{ outline: 'none', zIndex: 100 }}
/>
{stackBy ? (
data.keys?.map((key: string, index: number) => (
<Area
activeDot={{ r: 2, strokeWidth: 1 }}
dataKey={key}
fill={PALETTE[index % PALETTE.length]}
fillOpacity={0.75}
key={key}
stackId={'1'}
stroke={PALETTE[index % PALETTE.length]}
type='linear'
/>
))
) : (
<Area
activeDot={{ r: 5, strokeWidth: 0.5 }}
dataKey={yKey}
dot={false}
fill={color}
fillOpacity={0.6}
isAnimationActive={false}
stroke={color}
type='monotone'
/>
)}
<XAxis
dataKey={(x) => handleXKey(x, xKey)}
label={X_AXIS_LABEL_INSTRUCTIONS(xAxisTitle ?? '')}
fontSize={TICKMARK_FONT_SIZE}
tickFormatter={(tick) =>
timeframe ? moment(tick).format(timeframe) : tick
}
/>
<YAxis
dataKey={stackBy ? '' : yKey}
domain={[0, 'auto']}
fontSize={TICKMARK_FONT_SIZE}
label={Y_AXIS_LABEL_INSTRUCTIONS(yAxisTitle ?? '')}
tickFormatter={(tick) => formatLargeNumber(tick)}
scale={scale}
/>
</RechartsAreaChart>
</ResponsiveContainer>
);
}

View File

@@ -0,0 +1,90 @@
import moment from 'moment';
import { useMemo } from 'react';
import {
Bar,
BarChart as RechartsBarChart,
CartesianGrid,
ResponsiveContainer,
Tooltip as RechartsTooltip,
TooltipProps,
XAxis,
YAxis,
} from 'recharts';
import { determineTimeframe, formatLargeNumber, handleXKey } from 'utils';
import Tooltip from '../components/Tooltip';
import {
AxisChartProps,
PALETTE,
TICKMARK_FONT_SIZE,
X_AXIS_LABEL_INSTRUCTIONS,
Y_AXIS_LABEL_INSTRUCTIONS,
} from 'utils/constants';
type BarChartProps = Omit<AxisChartProps, 'curveType'>;
export default function BarChart({
color,
data,
grid,
scale,
stackBy,
xAxisTitle,
xKey,
yAxisTitle,
yKey,
}: BarChartProps): JSX.Element {
const timeframe = useMemo(() => {
return determineTimeframe(stackBy ? data.formattedData : data, xKey);
}, [data, stackBy, xKey]);
return (
<ResponsiveContainer>
<RechartsBarChart
data={stackBy ? data.formattedData : data}
margin={{ bottom: xAxisTitle ? 15 : 0, left: 5 }}
>
{grid && <CartesianGrid strokeDasharray='3 3' />}
<RechartsTooltip
content={(props: TooltipProps<number, string>) => (
<Tooltip {...props} xKey={xKey} />
)}
wrapperStyle={{ outline: 'none', zIndex: 100 }}
/>
{stackBy ? (
data.keys?.map((key: string, index: number) => (
<Bar
dataKey={key}
fill={PALETTE[index % PALETTE.length]}
fillOpacity={1}
isAnimationActive={false}
key={key}
stackId={'1'}
type='monotone'
/>
))
) : (
<Bar dataKey={yKey} fill={color} isAnimationActive={false} />
)}
<XAxis
dataKey={(x) => handleXKey(x, xKey)}
fontSize={TICKMARK_FONT_SIZE}
label={X_AXIS_LABEL_INSTRUCTIONS(xAxisTitle)}
name={xAxisTitle}
tickFormatter={(tick) =>
timeframe ? moment(tick).format(timeframe) : tick
}
/>
<YAxis
dataKey={stackBy ? '' : yKey}
domain={[0, 'auto']}
fontSize={TICKMARK_FONT_SIZE}
label={Y_AXIS_LABEL_INSTRUCTIONS(yAxisTitle ?? '')}
name={yAxisTitle}
scale={scale}
tickFormatter={(tick) => formatLargeNumber(tick)}
/>
</RechartsBarChart>
</ResponsiveContainer>
);
}

View File

@@ -0,0 +1,114 @@
import AreaChart from 'components/Charts/AreaChart';
import BarChart from 'components/Charts/BarChart';
import LineChart from 'components/Charts/LineChart';
import { useMemo } from 'react';
import { ChartScale, ChartType, StackBy } from 'utils/constants';
import PieChart from '../PieChart';
import { generateStackData } from 'utils';
type ChartWrapperProps = {
chartType: string;
color: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: any[];
grid?: boolean;
stackBy?: StackBy;
scale?: ChartScale;
xAxisTitle: string;
xKey: string;
yAxisTitle: string;
yKey: string;
};
export default function ChartWrapper({
chartType,
color,
data,
grid,
scale = ChartScale.Linear,
stackBy,
xAxisTitle,
xKey,
yAxisTitle,
yKey,
}: ChartWrapperProps): JSX.Element {
const stackedData = useMemo(() => {
if (!stackBy) return [];
return generateStackData(data, stackBy, xKey);
}, [data, stackBy, xKey]);
// TODO: Remove and place sort on DB side
const sortedData = useMemo(() => {
if (stackBy && !stackedData.formattedData.length) return [];
const sorted = (stackBy ? stackedData.formattedData : data).sort(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(a: any, b: any) => {
if (typeof a[xKey] === 'object') {
return (
new Date(a[xKey].value).getTime() -
new Date(b[xKey].value).getTime()
);
} else if (!isNaN(a[xKey])) {
return a - b;
} else {
return new Date(a[xKey]).getTime() - new Date(b[xKey]).getTime();
}
}
);
if (stackBy) {
return {
formattedData: sorted,
keys: stackedData.keys,
};
}
return sorted;
}, [data, stackBy, stackedData, xKey]);
if (chartType === ChartType.Area) {
return (
<AreaChart
color={color}
data={sortedData}
grid={!!grid}
scale={scale}
stackBy={stackBy}
xAxisTitle={xAxisTitle}
xKey={xKey || 'x'}
yAxisTitle={yAxisTitle}
yKey={yKey || 'y'}
/>
);
} else if (chartType === ChartType.Bar) {
return (
<BarChart
color={color}
data={sortedData}
grid={!!grid}
scale={scale}
stackBy={stackBy}
xAxisTitle={xAxisTitle}
xKey={xKey || 'x'}
yAxisTitle={yAxisTitle}
yKey={yKey || 'y'}
/>
);
} else if (chartType === ChartType.Line) {
return (
<LineChart
color={color}
curveType='basis'
data={sortedData}
grid={!!grid}
scale={scale}
stackBy={stackBy}
xAxisTitle={xAxisTitle}
xKey={xKey || 'x'}
yAxisTitle={yAxisTitle}
yKey={yKey || 'y'}
/>
);
} else {
return <PieChart data={data} dataKey={yKey || 'y'} nameKey={xKey || 'x'} />;
}
}

View File

@@ -0,0 +1,97 @@
import moment from 'moment';
import { useMemo } from 'react';
import {
CartesianGrid,
Line,
LineChart as RechartsLineChart,
ResponsiveContainer,
Tooltip as RechartsTooltip,
TooltipProps,
XAxis,
YAxis,
} from 'recharts';
import { determineTimeframe, formatLargeNumber, handleXKey } from 'utils';
import Tooltip from '../components/Tooltip';
import {
AxisChartProps,
PALETTE,
TICKMARK_FONT_SIZE,
X_AXIS_LABEL_INSTRUCTIONS,
Y_AXIS_LABEL_INSTRUCTIONS,
} from 'utils/constants';
type LineChartProps = AxisChartProps;
export default function LineChart({
color,
curveType,
data,
grid,
scale,
stackBy,
xAxisTitle,
xKey,
yAxisTitle,
yKey,
}: LineChartProps): JSX.Element {
const timeframe = useMemo(() => {
return determineTimeframe(stackBy ? data.formattedData : data, xKey);
}, [data, stackBy, xKey]);
return (
<ResponsiveContainer>
<RechartsLineChart
data={stackBy ? data.formattedData : data}
margin={{ bottom: xAxisTitle ? 15 : 0, left: 5 }}
>
{grid && <CartesianGrid strokeDasharray='3 3' />}
<RechartsTooltip
content={(props: TooltipProps<number, string>) => (
<Tooltip {...props} xKey={xKey} />
)}
wrapperStyle={{ outline: 'none', zIndex: 100 }}
/>
{stackBy ? (
data.keys?.map((key: string, index: number) => (
<Line
activeDot={{ strokeWidth: 1 }}
dataKey={key}
dot={false}
fill={PALETTE[index % PALETTE.length]}
key={key}
stroke={PALETTE[index % PALETTE.length]}
strokeWidth={2}
/>
))
) : (
<Line
activeDot={{ r: 5, strokeWidth: 0.5 }}
isAnimationActive={false}
dataKey={yKey}
dot={false}
stroke={color}
type='monotone'
/>
)}
<XAxis
dataKey={(x) => handleXKey(x, xKey)}
fontSize={TICKMARK_FONT_SIZE}
label={X_AXIS_LABEL_INSTRUCTIONS(xAxisTitle)}
name={xAxisTitle}
tickFormatter={(tick) =>
timeframe ? moment(tick).format(timeframe) : tick
}
/>
<YAxis
dataKey={stackBy ? '' : yKey}
domain={[0, 'auto']}
fontSize={TICKMARK_FONT_SIZE}
label={Y_AXIS_LABEL_INSTRUCTIONS(yAxisTitle ?? '')}
scale={scale}
name={yAxisTitle}
tickFormatter={(tick) => formatLargeNumber(tick)}
/>
</RechartsLineChart>
</ResponsiveContainer>
);
}

View File

@@ -0,0 +1,82 @@
import { useState } from 'react';
import {
Cell,
Pie,
PieChart as RechartsPieChart,
ResponsiveContainer,
Sector,
Tooltip as RechartsTooltip,
TooltipProps,
} from 'recharts';
import Tooltip from '../components/Tooltip';
import { PALETTE } from 'utils/constants';
type PieChartProps = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: any[];
dataKey: string;
nameKey: string;
};
export default function PieChart({
data,
dataKey,
nameKey,
}: PieChartProps): JSX.Element {
const [activeIndex, setActiveIndex] = useState(-1);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const activeShape = (props: any) => {
const { cx, cy, endAngle, fill, innerRadius, outerRadius, startAngle } =
props;
return (
<Sector
cx={cx}
cy={cy}
innerRadius={innerRadius * 0.95}
outerRadius={outerRadius * 1.05}
startAngle={startAngle}
endAngle={endAngle}
fill={fill}
/>
);
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const onPieEnter = (_: any, index: number) => {
setActiveIndex(index);
};
return (
<ResponsiveContainer>
<RechartsPieChart>
<RechartsTooltip
content={(props: TooltipProps<number, string>) => (
<Tooltip {...props} xKey={nameKey} />
)}
wrapperStyle={{ outline: 'none', zIndex: 100 }}
/>
<Pie
activeIndex={activeIndex}
activeShape={activeShape}
cx='50%'
cy='50%'
data={data}
dataKey={dataKey}
labelLine={false}
nameKey={nameKey}
onMouseEnter={onPieEnter}
onMouseLeave={() => setActiveIndex(-1)}
>
{data.map((_, index) => (
<Cell
key={`cell-${index}`}
fill={PALETTE[index % PALETTE.length]}
/>
))}
</Pie>
</RechartsPieChart>
</ResponsiveContainer>
);
}

View File

@@ -0,0 +1,41 @@
import Flex from 'components/Flex';
import moment from 'moment';
import { createUseStyles } from 'react-jss';
import { TooltipProps as RechartsTooltipProps } from 'recharts';
import { formatNumber, isDateOrTimestamp } from 'utils';
const useStyles = createUseStyles({
container: {
backgroundColor: '#34383D',
border: '1px solid #717371',
borderRadius: '4px',
fontSize: '16px',
padding: '12px',
},
});
type TooltipProps = { xKey: string } & RechartsTooltipProps<number, string>;
export default function Tooltip(props: TooltipProps): JSX.Element {
const { label, payload, xKey } = props;
const styles = useStyles();
if (!payload) return <></>;
return (
<div className={styles.container}>
<Flex key={xKey} gap='12px' style={{ color: '#FCFCFC' }}>
<div>{xKey}:</div>
<div>
{isDateOrTimestamp(label)
? moment(label).format('MMM Do, YYYY')
: label}
</div>
</Flex>
{payload.map(({ color, name, payload: { fill }, value }) => (
<Flex key={name} gap='12px' style={{ color: color ?? fill }}>
<div>{name}:</div>
<div>{formatNumber(value ?? 0, 0)}</div>
</Flex>
))}
</div>
);
}

View File

@@ -0,0 +1,34 @@
import Flex from 'components/Flex';
import Typography from 'components/Typography';
import { formatNumber } from 'utils';
type CounterProps = {
counterKey: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: any;
label: string;
};
export default function Counter({
counterKey,
data,
label,
}: CounterProps): JSX.Element {
return (
<>
<Flex
alignItems='center'
justifyContent='center'
h='100%'
style={{ color: '#FCFCFC' }}
>
<Flex direction='column' alignItems='center'>
<Typography variant='subtitle1'>{label}</Typography>
<Typography variant='h4'>
{formatNumber(data[0][counterKey], 0)}
</Typography>
</Flex>
</Flex>
</>
);
}

View File

@@ -0,0 +1,138 @@
import { CSSProperties, forwardRef, ReactNode } from 'react';
import { createUseStyles } from 'react-jss';
const useStyles = createUseStyles({
wrapper: (props: FlexStyleProps) => ({
'& > *': {
flex: props.childFlex ? props.childFlex : 'initial',
},
}),
});
type FlexProps = {
alignItems?:
| 'flex-start'
| 'flex-end'
| 'center'
| 'space-between'
| 'space-around'
| 'stretch';
children: ReactNode;
childFlex?: string;
cursor?: 'pointer';
direction?: 'row' | 'column' | 'column-reverse' | 'row-reverse';
justifyContent?:
| 'flex-start'
| 'flex-end'
| 'center'
| 'space-between'
| 'space-around';
gap?: string;
h?: string;
m?: string;
mb?: string;
ml?: string;
mr?: string;
mt?: string;
mx?: string;
my?: string;
onClick?: () => void;
p?: string;
paddingItemsCount?: number;
paddingItemsStyle?: CSSProperties;
pb?: string;
pl?: string;
pr?: string;
pt?: string;
px?: string;
py?: string;
style?: CSSProperties;
w?: string;
wrap?: 'wrap' | 'nowrap';
};
type FlexStyleProps = Pick<FlexProps, 'childFlex'>;
// eslint-disable-next-line react/display-name
const Flex = forwardRef(
(
{
alignItems,
children,
childFlex,
cursor,
direction,
justifyContent,
gap,
h,
m,
mb,
ml,
mr,
mt,
mx,
my,
onClick,
p,
paddingItemsCount,
paddingItemsStyle,
pb,
pl,
pr,
pt,
px,
py,
w,
wrap,
style,
}: FlexProps,
ref
): JSX.Element => {
const styles = useStyles({ childFlex });
return (
<div
className={styles.wrapper}
onClick={() => onClick && onClick()}
// eslint-disable-next-line
// @ts-ignore
ref={ref}
style={{
alignItems: alignItems ? alignItems : 'initial',
cursor: onClick || cursor ? 'pointer' : 'inherit',
display: 'flex',
flexDirection: direction ? direction : 'initial',
justifyContent: justifyContent ? justifyContent : 'initial',
gap: gap ? gap : 'initial',
height: h ? h : 'initial',
margin: m ? m : 'initial',
marginBlock: my ? my : 'inital',
marginBottom: mb ? mb : 'initial',
marginInline: mx ? mx : 'initial',
marginLeft: ml ? ml : 'initial',
marginRight: mr ? mr : 'initial',
marginTop: mt ? mt : 'initial',
paddingBlock: py ? py : 'initial',
paddingBottom: pb ? pb : 'initial',
paddingInline: px ? px : 'initial',
paddingLeft: pl ? pl : 'initial',
paddingRight: pr ? pr : 'initial',
paddingTop: pt ? pt : 'initial',
padding: p ? p : 'initial',
flexWrap: wrap ? wrap : 'initial',
width: w ? w : 'initial',
...style,
}}
>
{children}
{!!paddingItemsCount &&
new Array(paddingItemsCount)
.fill(0)
.map((_, index) => (
<div key={index} style={{ ...paddingItemsStyle }} />
))}
</div>
);
}
);
export default Flex;

View File

@@ -0,0 +1,63 @@
import { CSSProperties, ReactNode } from 'react';
import { createUseStyles } from 'react-jss';
import { BLSTheme, TypographyVariant } from 'theme';
const useStyles = createUseStyles((theme: BLSTheme) => ({
text: (props: TypographyProps) => ({
...theme.typography[props.variant],
cursor: props.onClick ? 'pointer' : 'inherit',
}),
}));
type TypographyProps = {
center?: boolean;
children: ReactNode;
className?: string;
onClick?: () => void;
style?: CSSProperties;
variant: TypographyVariant;
};
export default function Typography({
center,
children,
onClick,
style,
variant,
className,
}: TypographyProps): JSX.Element {
const styles = useStyles({ children, onClick, variant });
return (
<div
className={className ? `${styles.text} ${className}` : styles.text}
onClick={onClick}
style={{ textAlign: center ? 'center' : 'initial', ...style }}
>
{children}
</div>
);
}
export function Span({
children,
onClick,
style,
variant,
className,
}: TypographyProps): JSX.Element {
const styles = useStyles({ children, onClick, variant });
return (
<span
className={className ? `${styles.text} ${className}` : styles.text}
onClick={(e) => {
if (onClick) {
e.stopPropagation();
onClick();
}
}}
style={{ ...style }}
>
{children}
</span>
);
}

251
src/data.ts Normal file
View File

@@ -0,0 +1,251 @@
import { ChartType } from "utils/constants";
export const COUNTERS = [
{
counterKey: 'Num_Wallets_Created',
label: 'Wallets Created',
queryId: 'aa55c0623a28438eac9aecfb7b767726',
title: 'Wallets'
},
{
counterKey: 'Num_Wallets_Recovered',
label: 'Wallets Recovered',
queryId: 'aa55c0623a28438eac9aecfb7b767726',
title: 'Wallets'
},
{
counterKey: 'Num_Bundles_Submitted',
label: 'Bundles Submitted',
queryId: 'aa55c0623a28438eac9aecfb7b767726',
title: 'Tx Groups'
},
{
counterKey: 'Num_Operations_Failed',
label: 'Operations Failed',
queryId: 'aa55c0623a28438eac9aecfb7b767726',
title: 'Tx Groups'
},
{
counterKey: 'Num_Actions_Submitted',
label: 'Actions Submitted',
queryId: 'aa55c0623a28438eac9aecfb7b767726',
title: 'Tx Groups'
},
{
counterKey: 'Num_Operations_Submitted',
label: 'Operations Submitted',
queryId: 'aa55c0623a28438eac9aecfb7b767726',
title: 'Tx Groups'
},
{
counterKey: 'Avg_Operations_Per_Bundle',
label: 'Operations/Bundle',
queryId: 'aa55c0623a28438eac9aecfb7b767726',
title: 'Averages'
},
{
counterKey: 'Avg_Actions_Per_Bundle',
label: 'Actions/Bundle',
queryId: 'aa55c0623a28438eac9aecfb7b767726',
title: 'Averages'
},
{
counterKey: 'Avg_Actions_Per_Operation',
label: 'Actions/Operations',
queryId: 'aa55c0623a28438eac9aecfb7b767726',
title: 'Averages'
},
{
counterKey: 'minGas',
label: 'Min Gas per Tx',
queryId: 'aa55c0623a28438eac9aecfb7b767726',
title: 'Gas'
},
{
counterKey: 'maxGas',
label: 'Max Gas per Tx',
queryId: 'aa55c0623a28438eac9aecfb7b767726',
title: 'Gas'
},
{
counterKey: 'Avg_Gas',
label: 'Ave Gas per Tx',
queryId: 'aa55c0623a28438eac9aecfb7b767726',
title: 'Gas'
},
];
export const CHARTS = [
{
chartType: ChartType.Pie,
queryId: '7ad7fb1828094272880202e903af4239',
title: 'Action Method Ids Called',
xKey: 'actionMethodId',
yKey: 'action_count'
},
{
chartType: ChartType.Pie,
queryId: '2578dd88f123458183e0fe077d285243',
title: 'Action Recipients',
xKey: 'actionsRecipient',
yKey: 'action_count'
},
{
chartType: ChartType.Bar,
color: '#B8B7D0',
queryId: 'ab1a30af5991425b8b4abd7c37f859fa',
title: 'Number of Wallets Created By Day',
xAxisTitle: 'Day',
xKey: 'day',
yAxisTitle: 'Number of Wallets Created',
yKey: 'Num_Wallets_Created'
},
{
chartType: ChartType.Bar,
color: '#84abd9',
queryId: 'ab1a30af5991425b8b4abd7c37f859fa',
title: 'Number of Wallets Recovered By Day',
xAxisTitle: 'Day',
xKey: 'day',
yAxisTitle: 'Number of Wallets Recovered',
yKey: 'Num_Wallets_Recovered'
},
{
chartType: ChartType.Bar,
color: '#CE83D9',
queryId: 'ab1a30af5991425b8b4abd7c37f859fa',
title: 'Number of Bundles Submitted per Day',
xAxisTitle: 'Day',
xKey: 'day',
yAxisTitle: 'Bundles Submitted',
yKey: 'Num_Bundles_Submitted'
},
{
chartType: ChartType.Bar,
color: '#8784D8',
queryId: 'ab1a30af5991425b8b4abd7c37f859fa',
title: 'Number of Actions Submitted per Day',
xAxisTitle: 'Day',
xKey: 'day',
yAxisTitle: 'Actions Submitted',
yKey: 'Num_Actions_Submitted'
},
{
chartType: ChartType.Bar,
color: '#C6C5CF',
queryId: 'ab1a30af5991425b8b4abd7c37f859fa',
title: 'Number of Operations Submitted per Day',
xAxisTitle: 'Day',
xKey: 'day',
yAxisTitle: 'Number of Operations',
yKey: 'Num_Operations_Submitted'
},
{
chartType: ChartType.Bar,
color: '#E4214F',
queryId: 'ab1a30af5991425b8b4abd7c37f859fa',
title: 'Number of Operations Failed per Day',
xAxisTitle: 'Day',
xKey: 'day',
yAxisTitle: 'Failed Operations',
yKey: 'Num_Operations_Failed'
},
{
chartType: ChartType.Bar,
color: '#84D9CC',
queryId: 'ab1a30af5991425b8b4abd7c37f859fa',
title: 'Average Operations per Bundle per day',
xAxisTitle: 'Day',
xKey: 'day',
yAxisTitle: 'Average Operations per Bundle',
yKey: 'Avg_Operations_Per_Bundle'
},
{
chartType: ChartType.Bar,
color: '#8784D8',
queryId: 'ab1a30af5991425b8b4abd7c37f859fa',
title: 'Average Actions per Bundle Per Dayy',
xAxisTitle: 'Day',
xKey: 'day',
yAxisTitle: 'Actions',
yKey: 'Avg_Actions_Per_Bundle'
},
{
chartType: ChartType.Bar,
color: '#B6B6BF',
queryId: 'ab1a30af5991425b8b4abd7c37f859fa',
title: 'Average Actions per Operation per Day',
xAxisTitle: 'Day',
xKey: 'day',
yAxisTitle: 'Actions per Operation',
yKey: 'Avg_Actions_Per_Operation'
},
{
chartType: ChartType.Bar,
color: '#1B4FC2',
queryId: 'ab1a30af5991425b8b4abd7c37f859fa',
title: 'Min Gas Per Day',
xAxisTitle: 'Day',
xKey: 'day',
yAxisTitle: 'Min Gas Per Transaction',
yKey: 'minGas'
},
{
chartType: ChartType.Bar,
color: '#C2431B',
queryId: 'ab1a30af5991425b8b4abd7c37f859fa',
title: 'Max Gas Per Day',
xAxisTitle: 'Day',
xKey: 'day',
yAxisTitle: 'Maximum Gas Per Transaction',
yKey: 'maxGas'
},
{
chartType: ChartType.Bar,
color: '#A19FCD',
queryId: 'ab1a30af5991425b8b4abd7c37f859fa',
title: 'Average Gas per Day',
xAxisTitle: 'Day',
xKey: 'day',
yAxisTitle: 'Average Gas per Transaction',
yKey: 'avgGas'
},
{
chartType: ChartType.Bar,
queryId: '74a8783fd397441ca0dd9cbc96423e81',
stackBy: {
stackColumn: 'actionMethodId',
valueColumn: 'action_count'
},
title: 'Actions Grouped By Method Id',
xAxisTitle: 'Day',
xKey: 'day',
yAxisTitle: 'Actions Grouped by Method Id',
},
{
chartType: ChartType.Bar,
queryId: '8616b0bfad394b3c8314c6573497b472',
stackBy: {
stackColumn: 'actionsRecipient',
valueColumn: 'action_count'
},
title: 'Actions Grouped By Recipient',
xAxisTitle: 'Day',
xKey: 'day',
yAxisTitle: 'Actions Grouped By Recipient',
},
];
// type ChartWrapperProps = {
// chartType: string;
// color: string;
// // eslint-disable-next-line @typescript-eslint/no-explicit-any
// data: any[];
// grid?: boolean;
// stackBy?: StackBy;
// scale?: ChartScale;
// xAxisTitle: string;
// xKey: string;
// yAxisTitle: string;
// yKey: string;
// };

BIN
src/fonts/Heebo-Regular.ttf Normal file

Binary file not shown.

Binary file not shown.

33
src/index.css Normal file
View File

@@ -0,0 +1,33 @@
/* Fonts */
@font-face {
font-family: 'Montserrat';
src: url(./fonts/Montserrat-Regular.ttf) format('truetype');
font-weight: 400;
}
@font-face {
font-family: 'Heebo';
src: url(./fonts/Heebo-Regular.ttf) format('truetype');
font-weight: 400;
}
@font-face {
font-family: 'Heebo';
src: url(./fonts/Heebo-Regular.ttf) format('truetype');
font-weight: 500;
}
body {
background-color: #262726;
margin: 0;
font-family: 'Heebo', 'Montserrat', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

23
src/index.tsx Normal file
View File

@@ -0,0 +1,23 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { ThemeProvider } from 'react-jss';
import { theme } from 'theme';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<ThemeProvider theme={theme}>
<App />
</ThemeProvider>
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

1
src/logo.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

1
src/react-app-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="react-scripts" />

15
src/reportWebVitals.ts Normal file
View File

@@ -0,0 +1,15 @@
import { ReportHandler } from 'web-vitals';
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

5
src/setupTests.ts Normal file
View File

@@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

60
src/theme/index.ts Normal file
View File

@@ -0,0 +1,60 @@
import { CSSProperties } from 'react';
export type TypographyVariant =
| 'caption'
| 'h4'
| 'h5'
| 'h6'
| 'subtitle1'
| 'subtitle2';
type TypographyVariantMap = { [key in TypographyVariant]: CSSProperties };
export type BLSTheme = {
typography: TypographyVariantMap;
};
export const typography: TypographyVariantMap = {
caption: {
fontFamily: 'montserrat',
fontSize: '12px',
fontWeight: 400,
lineHeight: '14px',
},
h4: {
fontFamily: 'Heebo',
fontSize: '34px',
fontWeight: 500,
letterSpacing: '0.5px',
lineHeight: '40px',
},
h5: {
fontFamily: 'Heebo',
fontSize: '24px',
fontWeight: 500,
lineHeight: '28px',
},
h6: {
fontFamily: 'Heebo',
fontSize: '20px',
fontWeight: 500,
letterSpacing: '0.5px',
lineHeight: '24px',
},
subtitle1: {
fontFamily: 'Heebo',
fontSize: '16px',
fontWeight: 400,
lineHeight: '18px',
},
subtitle2: {
fontFamily: 'Heebo',
fontSize: '14px',
fontWeight: 500,
lineHeight: '16px',
},
};
export const theme: BLSTheme = {
typography,
};

70
src/utils/constants.ts Normal file
View File

@@ -0,0 +1,70 @@
import { CurveType } from 'recharts/types/shape/Curve';
export type AxisChartProps = {
color: string;
curveType: CurveType;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: any;
grid: boolean;
height?: number;
scale: ChartScale;
stackBy?: StackBy;
width?: number;
xAxisTitle: string;
xKey: string;
yAxisTitle?: string;
yKey: string;
};
export enum ChartScale {
Linear = 'linear',
Log = 'log',
}
export enum ChartType {
Area = 'area',
Bar = 'bar',
Line = 'line',
Pie = 'pie',
Counter = 'counter',
}
export const MS_PER_DAY = 24 * 60 * 60 * 1000; // Milleseconds per day
export const MS_PER_WEEK = 7 * MS_PER_DAY; // Milleseconds per week
export const MS_PER_MONTH = 30.44 * MS_PER_DAY; // Milleseconds per month
export const MS_PER_YEAR = 365.25 * MS_PER_DAY; // Millesconds per year
export const PALETTE = [
'#a6cee3',
'#1f78b4',
'#b2df8a',
'#33a02c',
'#fb9a99',
'#e31a1c',
'#fdbf6f',
'#ff7f00',
'#cab2d6',
'#6a3d9a',
'#ffff99',
'#b15928',
];
export type StackBy = {
stackColumn: string;
valueColumn: string;
};
export const AXIS_LABEL_FONT_SIZE = '16px';
export const TICKMARK_FONT_SIZE = 14;
export const X_AXIS_LABEL_INSTRUCTIONS = (title: string): object => ({
dy: 15,
style: { fontSize: '14px' },
value: title,
});
export const Y_AXIS_LABEL_INSTRUCTIONS = (title: string): object => ({
angle: -90,
dx: 0,
position: 'insideLeft',
style: { fontSize: '16px', textAnchor: 'middle' },
value: title,
});

135
src/utils/index.ts Normal file
View File

@@ -0,0 +1,135 @@
import { MS_PER_MONTH, MS_PER_WEEK, MS_PER_YEAR } from "./constants";
import { StackBy } from "./constants";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const determineTimeframe = (data: any[], xKey: string): string => {
if (!data) return '';
let startTime = data[0][xKey];
let endTime = data[data.length - 1][xKey];
// Check for bigquery date format
if (typeof startTime === 'object' || typeof endTime === 'object') {
startTime = startTime.value;
endTime = endTime.value;
}
if (isDateOrTimestamp(startTime) && isDateOrTimestamp(endTime)) {
const elapsed = new Date(endTime).getTime() - new Date(startTime).getTime();
if (elapsed >= MS_PER_YEAR) {
return `MMM Do, YYYY`;
} else if (elapsed >= MS_PER_MONTH) {
return `MMM Do`;
} else if (elapsed >= MS_PER_WEEK) {
return `Do`;
} else {
return `HH:mm`;
}
} else {
return '';
}
};
/**
* Add commas to large numbers and limit to three decimal places if nonzero
* @param {number|string} num - The number to format
* @param {number} decimals - The number of decimals to show
* @return {string} - The formatted number
*/
export const formatNumber = (num: number | string, decimals = 2): string => {
if (!num) return '0'; // Don't append decimals to 0
// If desired decimals exceed max significant decimal places then do not convert to float
// and directly commify instead (here for number inputs)
try {
if (typeof num === 'string') {
num = parseFloat(num);
}
return num.toLocaleString(undefined, {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
});
} catch (e) {
return '0';
}
};
export const formatLargeNumber = (num: number | string): string => {
num = Number(num);
if (num >= 10 ** 12) {
return `${formatNumber(num / 10 ** 12, 1)}t`;
}
if (num >= 10 ** 9) {
return `${formatNumber(num / 10 ** 9, 1)}b`;
}
if (num >= 10 ** 6) {
return `${formatNumber(num / 10 ** 6, 1)}m`;
}
if (num >= 10 ** 5) {
return `${formatNumber(num / 10 ** 3, 1)}k`;
}
return formatNumber(num, 1);
};
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
export const isDateOrTimestamp = (value: any): boolean => {
return isNaN(value) && !isNaN(Date.parse(value));
};
/**
*
* @param data {any} - data at specific index in data array
* @param key {string} - Key to parse from data
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
export const handleXKey = (data: any, key: string) => {
const val = data[key];
// If val is object we can assume it is a BigQuery date. Return value inside
if (val instanceof Object) {
return val.value;
}
return val;
};
export const generateStackData = (
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
rows: any[],
stackBy: StackBy,
xKey: string
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
): any => {
const { stackColumn, valueColumn } = stackBy;
const unique = uniqueValues(stackColumn, rows);
const keyObj = setToObject(unique);
const reduceByXKey = rows.reduce((obj, entry) => {
const parsedXKey = handleXKey(entry, xKey);
// If date doesn't exist then intialize with all keys
if (!obj[parsedXKey]) {
obj[parsedXKey] = { ...keyObj };
}
obj[parsedXKey][entry[stackColumn]] = entry[valueColumn];
return obj;
}, {});
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
const formattedData = Object.entries(reduceByXKey).map((entry: any) => {
return {
[xKey]: entry[0],
...entry[1],
};
});
return { formattedData, keys: Array.from(unique) };
};
export const setToObject = (set: Set<string>): { [key: string]: number } => {
const obj: { [key: string]: number } = {};
set.forEach((value) => {
obj[value] = 0;
});
return obj;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const uniqueValues = (key: string, rows: any[]): Set<string> => {
const set: Set<string> = new Set();
rows.forEach((row) => {
if (row[key] !== null) set.add(row[key]);
});
return set;
};

27
tsconfig.json Normal file
View File

@@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"baseUrl": "src"
},
"include": [
"src"
]
}

9925
yarn.lock Normal file

File diff suppressed because it is too large Load Diff