Skip to content

Commit

Permalink
Merge pull request #64 from GenerationSoftware/stats-page
Browse files Browse the repository at this point in the history
Cabanalytics Stats Page
  • Loading branch information
Ncookiez authored Jul 10, 2024
2 parents a28c270 + 6b34b83 commit 4c6c26b
Show file tree
Hide file tree
Showing 58 changed files with 1,758 additions and 159 deletions.
1 change: 0 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"tailwindCSS.experimental.configFile": "./shared/config/tailwind.config.js",
"files.associations": {
"*.css": "tailwindcss"
}
Expand Down
30 changes: 25 additions & 5 deletions apps/analytics/src/components/Charts/AreaChart.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useScreenSize } from '@shared/generic-react-hooks'
import { ReactNode } from 'react'
import { CSSProperties, ReactNode } from 'react'
import {
Area,
Legend,
Expand Down Expand Up @@ -52,6 +52,7 @@ export interface AreaChartProps {
dot?: AreaDot
animate?: boolean
stackId?: string | number
opacity?: number
}[]
aspect?: number
xAxis?: {
Expand All @@ -73,6 +74,9 @@ export interface AreaChartProps {
content?: ContentType<number, string | number>
formatter?: (value: number, name: string | number) => ReactNode | ReactNode[]
labelFormatter?: (label: string) => ReactNode
sort?: 'asc' | 'desc'
labelClassName?: string
itemStyle?: CSSProperties
}
margin?: { left: number; mobileLeft: number }
legend?: { show?: boolean; formatter: (value: string | number) => ReactNode }
Expand Down Expand Up @@ -114,23 +118,31 @@ export const AreaChart = (props: AreaChartProps) => {
key={`area-${area.id}`}
type={area.type ?? DEFAULT.area.type}
dataKey={area.id}
stroke={area.stroke ?? DEFAULT.area.strokes[i]}
stroke={area.stroke ?? DEFAULT.area.strokes[i % DEFAULT.area.strokes.length]}
fill={
gradients ?? DEFAULT.gradients
? `url(#${getGradientFillId(area.id)})`
: area.stroke ?? DEFAULT.area.strokes[i]
: area.stroke ?? DEFAULT.area.strokes[i % DEFAULT.area.strokes.length]
}
strokeWidth={area.strokeWidth ?? DEFAULT.area.strokeWidth}
dot={area.dot ?? DEFAULT.area.dot}
isAnimationActive={area.animate ?? DEFAULT.area.animate}
stackId={area.stackId}
opacity={area.opacity}
/>
))}
{tooltip?.show && (
<Tooltip
content={tooltip.content}
formatter={tooltip.formatter}
labelFormatter={tooltip.labelFormatter}
itemSorter={
!!tooltip.sort
? (item) => (item.value as number) * (tooltip.sort === 'desc' ? -1 : 1)
: undefined
}
labelClassName={tooltip.labelClassName}
itemStyle={tooltip.itemStyle}
/>
)}
<XAxis
Expand Down Expand Up @@ -162,12 +174,20 @@ export const AreaChart = (props: AreaChartProps) => {
>
<stop
offset='5%'
stopColor={area.stroke ?? area.stroke ?? DEFAULT.area.strokes[i]}
stopColor={
area.stroke ??
area.stroke ??
DEFAULT.area.strokes[i % DEFAULT.area.strokes.length]
}
stopOpacity={0.8}
/>
<stop
offset='95%'
stopColor={area.stroke ?? area.stroke ?? DEFAULT.area.strokes[i]}
stopColor={
area.stroke ??
area.stroke ??
DEFAULT.area.strokes[i % DEFAULT.area.strokes.length]
}
stopOpacity={0.2}
/>
</linearGradient>
Expand Down
112 changes: 112 additions & 0 deletions apps/analytics/src/components/Charts/BarChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { useScreenSize } from '@shared/generic-react-hooks'
import { ReactNode } from 'react'
import {
Bar,
BarChart as RechartsBarChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis
} from 'recharts'
import { ContentType } from 'recharts/types/component/Tooltip'
import { AxisDomain, AxisInterval } from 'recharts/types/util/types'

const DEFAULT: {
bar: { fill: string; animate: boolean }
aspect: number
xAxis: { stroke: string }
yAxis: { stroke: string }
margin: { left: number; mobileLeft: number }
} = {
bar: { fill: '#6538C1', animate: false },
aspect: 2.8,
xAxis: { stroke: '#8c63cf' },
yAxis: { stroke: '#8c63cf' },
margin: { left: 5, mobileLeft: -12 }
}

export interface BarChartProps {
data: any[]
bars: {
id: string | number
fill?: string
stackId?: string | number
animate?: boolean
}[]
aspect?: number
xAxis?: {
type?: 'number' | 'category'
ticks?: (string | number)[]
domain?: AxisDomain
stroke?: string
interval?: AxisInterval
minTickGap?: number
tickFormatter?: (tick: string | number) => string
}
yAxis?: {
type?: 'number' | 'category'
domain?: AxisDomain
stroke?: string
tickFormatter?: (tick: string | number) => string
}
tooltip?: {
show?: boolean
content?: ContentType<number, string | number>
formatter?: (value: number, name: string | number) => ReactNode | ReactNode[]
labelFormatter?: (label: string) => ReactNode
}
margin?: { left: number; mobileLeft: number }
className?: string
}

export const BarChart = (props: BarChartProps) => {
const { data, bars, aspect, xAxis, yAxis, tooltip, margin, className } = props

const { isMobile } = useScreenSize()

return (
<ResponsiveContainer width='100%' aspect={aspect ?? DEFAULT.aspect} className={className}>
<RechartsBarChart
data={data}
margin={{
left: isMobile
? margin?.mobileLeft ?? DEFAULT.margin.mobileLeft
: margin?.left ?? DEFAULT.margin.left
}}
>
{bars.map((bar) => (
<Bar
key={`bar-${bar.id}`}
dataKey={bar.id}
fill={bar.fill ?? DEFAULT.bar.fill}
isAnimationActive={bar.animate ?? DEFAULT.bar.animate}
/>
))}
{tooltip?.show && (
<Tooltip
content={tooltip.content}
formatter={tooltip.formatter}
labelFormatter={tooltip.labelFormatter}
cursor={{ fillOpacity: 0.1 }}
/>
)}
<XAxis
dataKey='name'
type={xAxis?.type}
domain={xAxis?.domain}
ticks={xAxis?.ticks}
stroke={xAxis?.stroke ?? DEFAULT.xAxis.stroke}
interval={xAxis?.interval}
minTickGap={xAxis?.minTickGap}
tickFormatter={xAxis?.tickFormatter}
/>
<YAxis
type={yAxis?.type}
domain={yAxis?.domain}
stroke={yAxis?.stroke ?? DEFAULT.yAxis.stroke}
tickFormatter={yAxis?.tickFormatter}
/>
</RechartsBarChart>
</ResponsiveContainer>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -150,11 +150,11 @@ export const DrawsAvgLiqEfficiencyChart = (props: DrawsAvgLiqEfficiencyChartProp
return (
<div
className={classNames(
'w-full flex flex-col gap-2 font-medium text-pt-purple-200',
'w-full flex flex-col gap-2 font-medium text-pt-purple-800',
className
)}
>
<span className='ml-2'>Average Liquidation Efficiency</span>
<span className='ml-2 text-pt-purple-200 md:ml-6'>Average Liquidation Efficiency</span>
<LineChart
data={chartData}
lines={[{ id: 'percentage' }, { id: 'cumAvg', strokeDashArray: 5 }]}
Expand Down
2 changes: 1 addition & 1 deletion apps/analytics/src/components/Charts/LineChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export const LineChart = (props: LineChartProps) => {
key={`line-${line.id}`}
type={line.type ?? DEFAULT.line.type}
dataKey={line.id}
stroke={line.stroke ?? DEFAULT.line.strokes[i]}
stroke={line.stroke ?? DEFAULT.line.strokes[i % DEFAULT.line.strokes.length]}
strokeWidth={line.strokeWidth ?? DEFAULT.line.strokeWidth}
strokeDasharray={line.strokeDashArray}
dot={line.dot ?? DEFAULT.line.dot}
Expand Down
82 changes: 82 additions & 0 deletions apps/analytics/src/components/Charts/PPCOverTimeChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { PrizePool } from '@generationsoftware/hyperstructure-client-js'
import { usePrizeTokenData } from '@generationsoftware/hyperstructure-react-hooks'
import { Spinner, Toggle } from '@shared/ui'
import { formatNumberForDisplay } from '@shared/utilities'
import classNames from 'classnames'
import { useMemo, useState } from 'react'
import { usePPCsOverTime } from '@hooks/usePPCsOverTime'
import { BarChart, BarChartProps } from './BarChart'

interface PPCOverTimeChartProps {
prizePool: PrizePool
hideFirstDraws?: number
className?: string
}

export const PPCOverTimeChart = (props: PPCOverTimeChartProps) => {
const { prizePool, hideFirstDraws, className } = props

const { data: ppcs } = usePPCsOverTime(prizePool)

const { data: prizeToken } = usePrizeTokenData(prizePool)

const [isHidingFirstDraws, setIsHidingFirstDraws] = useState<boolean>(true)

const chartData = useMemo(() => {
const data: BarChartProps['data'] = []

if (!!ppcs) {
Object.entries(ppcs).forEach((entry, i) => {
if (
!hideFirstDraws ||
!isHidingFirstDraws ||
Object.keys(ppcs).length <= hideFirstDraws ||
i >= hideFirstDraws
) {
data.push({ name: `#${parseInt(entry[0])}`, ppc: entry[1] })
}
})
}

return data
}, [ppcs, hideFirstDraws, isHidingFirstDraws])

const isReady = !!chartData?.length && !!prizeToken

return (
<div
className={classNames('w-full flex flex-col gap-2 font-medium text-pt-purple-800', className)}
>
<span className='ml-2 text-pt-purple-100 font-bold'>PPCs Over Time</span>
{isReady ? (
<BarChart
data={chartData}
bars={[{ id: 'ppc', animate: true }]}
tooltip={{
show: true,
formatter: (value) => [
`${formatNumberForDisplay(value, { maximumFractionDigits: 4 })} ${prizeToken.symbol}`,
'PPC'
],
labelFormatter: (label) => `Draw ${label}`
}}
xAxis={{ minTickGap: 50 }}
/>
) : (
<div className='w-full aspect-[2.8] flex items-center justify-center'>
<Spinner />
</div>
)}
{isReady && !!hideFirstDraws && (
<Toggle
checked={!isHidingFirstDraws}
onChange={() => setIsHidingFirstDraws(!isHidingFirstDraws)}
size='sm'
label={`${isHidingFirstDraws ? 'Show' : 'Hide'} first ${hideFirstDraws} draws`}
className='ml-8'
labelClassName='text-pt-purple-200/60'
/>
)}
</div>
)
}
84 changes: 84 additions & 0 deletions apps/analytics/src/components/Charts/PieChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { Cell, Pie, PieChart as RechartsPieChart, ResponsiveContainer } from 'recharts'

const DEFAULT: {
slice: { fills: string[] }
animate: boolean
label: { hideUnderPercent: number }
} = {
slice: { fills: ['#1C64F2', '#1cb5f2', '#dd1cf2', '#f21c3c', '#1cf283', '#f2ee1c', '#f2951c'] },
animate: false,
label: { hideUnderPercent: 2.5 }
}

export interface PieChartProps {
data: any[]
slices?: { id: string | number; fill?: string }[]
radius?: { inner?: string | number; outer?: string | number }
paddingAngle?: number
animate?: boolean
label?: {
show?: boolean
nameFormatter?: (name: string) => string
center?: boolean
hideUnderPercent?: number
}
className?: string
}

export const PieChart = (props: PieChartProps) => {
const { data, slices, radius, paddingAngle, animate, label, className } = props

const dataKey = Object.keys(data[0] ?? {})[1]

return (
<ResponsiveContainer width='100%' aspect={1} className={className}>
{!!dataKey ? (
<RechartsPieChart>
<Pie
data={data}
innerRadius={radius?.inner}
outerRadius={radius?.outer}
paddingAngle={paddingAngle}
dataKey={dataKey}
isAnimationActive={animate ?? DEFAULT.animate}
label={
label
? ({ cx, cy, midAngle, innerRadius, outerRadius, percent, index }) => {
if (percent * 100 < (label.hideUnderPercent ?? DEFAULT.label.hideUnderPercent))
return <></>

const radius = label.center
? innerRadius + (outerRadius - innerRadius) * 0.5
: 25 + innerRadius + (outerRadius - innerRadius)
const x = cx + radius * Math.cos((-midAngle * Math.PI) / 180)
const y = cy + radius * Math.sin((-midAngle * Math.PI) / 180)

const rawName = data[index].name
const name = !!label.nameFormatter ? label.nameFormatter(rawName) : rawName
const percentage = `${(percent * 100).toFixed(1)}%`

return (
<text x={x} y={y} fill='white' textAnchor='middle' dominantBaseline='middle'>
{name} ({percentage})
</text>
)
}
: undefined
}
labelLine={label?.center ? false : undefined}
>
{data.map((_, i) => (
<Cell
key={`cell-${i}`}
fill={slices?.[i]?.fill ?? DEFAULT.slice.fills[i % DEFAULT.slice.fills.length]}
style={{ outline: 'none' }}
/>
))}
</Pie>
</RechartsPieChart>
) : (
<></>
)}
</ResponsiveContainer>
)
}
Loading

0 comments on commit 4c6c26b

Please sign in to comment.