Skip to content

Commit

Permalink
Merge pull request #1115 from oasisprotocol/mz/pieChart
Browse files Browse the repository at this point in the history
Pie chart
  • Loading branch information
buberdds authored Jan 12, 2024
2 parents 04fa019 + 08b06fc commit af263ee
Show file tree
Hide file tree
Showing 4 changed files with 198 additions and 4 deletions.
1 change: 1 addition & 0 deletions .changelog/1115.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Create pie chart component
142 changes: 142 additions & 0 deletions src/app/components/charts/PieChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { memo, useState } from 'react'
import { Legend, ResponsiveContainer, Tooltip, PieChart as RechartsPieChart, Pie, Cell } from 'recharts'
import { useTranslation } from 'react-i18next'
import Box from '@mui/material/Box'
import CircleIcon from '@mui/icons-material/Circle'
import List from '@mui/material/List'
import ListItem from '@mui/material/ListItem'
import Typography from '@mui/material/Typography'
import { TooltipContent, type Formatters } from './Tooltip'
import { COLORS } from '../../../styles/theme/colors'
import { COLORS as TESTNET_COLORS } from '../../../styles/theme/testnet/colors'
import { Props } from 'recharts/types/component/DefaultLegendContent'
import { PieSectorDataItem } from 'recharts/types/polar/Pie'

interface PieChartProps<T extends object> extends Formatters {
compact: boolean
data: T[]
dataKey: Extract<keyof T, string>
}

const colorPalette = [COLORS.brandDark, COLORS.brandMedium, TESTNET_COLORS.testnet, COLORS.grayMedium2]

type CustomLegendProps = Props & {
activeLabel?: string
compact: boolean
}

const CustomLegend = (props: CustomLegendProps) => {
const { activeLabel, compact, payload } = props
const { t } = useTranslation()

return (
<List sx={{ listStyleType: 'none' }}>
{payload?.map(item => {
if (!item.payload) {
return null
}

const payload = item.payload as PieSectorDataItem
const label = payload.name
const isActive = activeLabel === label
return (
<ListItem key={label} sx={{ padding: 0 }}>
<Box
sx={{
width: compact ? 32 : 48,
height: compact ? 32 : 48,
display: 'flex',
position: 'relative',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '50%',
backgroundColor: isActive ? `${item.color}40` : 'transparent',
}}
>
<CircleIcon sx={{ color: item.color, fontSize: compact ? 12 : 18 }} />
</Box>
<Typography
component="span"
sx={{
color: isActive ? item.color : COLORS.grayMedium,
fontSize: compact ? 12 : 18,
ml: 2,
fontWeight: isActive ? 700 : 400,
}}
>
{label} (
{t('common.valuePair', {
value: payload.percent,
formatParams: {
value: {
style: 'percent',
maximumFractionDigits: 2,
} satisfies Intl.NumberFormatOptions,
},
})}
)
</Typography>
</ListItem>
)
})}
</List>
)
}

const PieChartCmp = <T extends object>({ compact, data, dataKey, formatters }: PieChartProps<T>) => {
const [activeLabel, setActiveLabel] = useState<string>()
if (!data.length) {
return null
}
const labelKey = Object.keys(data[0]).find(key => key !== dataKey)
if (!labelKey) {
throw new Error('Not able to determine label key')
}
return (
<ResponsiveContainer width="100%">
<RechartsPieChart width={100} height={100}>
<Tooltip
cursor={false}
wrapperStyle={{ outline: 'none' }}
content={<TooltipContent formatters={formatters} labelKey={labelKey} />}
offset={15}
/>
<Legend
width={compact ? 150 : 250}
layout="vertical"
align="left"
verticalAlign="middle"
content={props => (
<CustomLegend activeLabel={activeLabel} compact={compact} payload={props.payload} />
)}
/>
<Pie
onMouseEnter={(item, index) => setActiveLabel(item[labelKey])}
onMouseLeave={() => setActiveLabel(undefined)}
stroke="none"
data={data}
innerRadius={compact ? 25 : 40}
outerRadius={compact ? 50 : 90}
paddingAngle={0}
dataKey={dataKey}
>
{data.map((item, index) => {
const label = item[labelKey as keyof T] as string
return (
<Cell
fill={colorPalette[index % colorPalette.length]}
key={`${label}-${index}`}
name={label}
style={{
filter: activeLabel === label ? `drop-shadow(0px 0px 5px ${COLORS.grayMedium}` : 'none',
}}
/>
)
})}
</Pie>
</RechartsPieChart>
</ResponsiveContainer>
)
}

export const PieChart = memo(PieChartCmp) as typeof PieChartCmp
17 changes: 13 additions & 4 deletions src/app/components/charts/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,28 @@ export type Formatters = {
}
}

type TooltipContentProps = TooltipProps<number, string> & Formatters
type TooltipContentProps = TooltipProps<number, string> &
Formatters & {
labelKey?: string
}

export const TooltipContent = ({ active, payload, formatters }: TooltipContentProps) => {
export const TooltipContent = ({
active,
payload,
formatters,
labelKey: dataLabelKey,
}: TooltipContentProps) => {
if (!active || !payload || !payload.length) {
return null
}

const { [payload[0].dataKey!]: value, ...rest } = payload[0].payload
const labelKey = Object.keys(rest)[0]
const labelKey = dataLabelKey || Object.keys(rest)[0]

return (
<StyledPaper>
<Typography paragraph={false} sx={{ fontSize: 12 }}>
{formatters?.label ? formatters.label(payload[0]?.payload[labelKey]) : payload[0]?.payload[labelKey]}
{formatters?.label ? formatters.label(payload[0].payload[labelKey]) : payload[0].payload[labelKey]}
</Typography>
<Typography paragraph={false} sx={{ fontSize: 12, fontWeight: 600 }}>
{formatters?.data ? formatters.data(payload[0].value!) : payload[0].value}
Expand Down
42 changes: 42 additions & 0 deletions src/stories/Charts/PieChart.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Meta, StoryFn, StoryObj } from '@storybook/react'
import Card from '@mui/material/Card'
import { PieChart } from '../../app/components/charts/PieChart'

export default {
title: 'Example/Charts/PieChart',
component: PieChart,
} satisfies Meta<typeof PieChart>

interface DataItem {
x: string
y: number
}

const data: DataItem[] = [
{ x: 'Active', y: 50 },
{ x: 'Inactive', y: 10 },
{ x: 'Waiting', y: 39 },
]

const Template: StoryFn<typeof PieChart<DataItem>> = args => {
return (
<Card sx={{ width: '500px', height: '300px' }}>
<PieChart {...args} />
</Card>
)
}

type Story = StoryObj<typeof PieChart<DataItem>>

export const SamplePieChart: Story = {
render: Template,
args: {
compact: true,
data,
dataKey: 'y',
formatters: {
data: (value: number) => value.toLocaleString(),
label: (value: string) => `${value} validators`,
},
},
}

0 comments on commit af263ee

Please sign in to comment.