From 632c50b4c150e2a22d6595e5bd1c12e3f57c2d15 Mon Sep 17 00:00:00 2001 From: Michal Zielenkiewicz Date: Thu, 4 Jan 2024 11:14:13 +0100 Subject: [PATCH 1/6] Allow to pass data label key to Tooltip --- src/app/components/charts/Tooltip.tsx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/app/components/charts/Tooltip.tsx b/src/app/components/charts/Tooltip.tsx index 8283b59b3..816d53348 100644 --- a/src/app/components/charts/Tooltip.tsx +++ b/src/app/components/charts/Tooltip.tsx @@ -22,19 +22,28 @@ export type Formatters = { } } -type TooltipContentProps = TooltipProps & Formatters +type TooltipContentProps = TooltipProps & + 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 ( - {formatters?.label ? formatters.label(payload[0]?.payload[labelKey]) : payload[0]?.payload[labelKey]} + {formatters?.label ? formatters.label(payload[0].payload[labelKey]) : payload[0].payload[labelKey]} {formatters?.data ? formatters.data(payload[0].value!) : payload[0].value} From 5c02937281c964d09057a771c8ff0c56e9665a83 Mon Sep 17 00:00:00 2001 From: Michal Zielenkiewicz Date: Thu, 21 Dec 2023 11:31:09 +0100 Subject: [PATCH 2/6] Create pie chart component --- .changelog/1115.feature.md | 1 + src/app/components/charts/PieChart.tsx | 43 ++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 .changelog/1115.feature.md create mode 100644 src/app/components/charts/PieChart.tsx diff --git a/.changelog/1115.feature.md b/.changelog/1115.feature.md new file mode 100644 index 000000000..d8a3833af --- /dev/null +++ b/.changelog/1115.feature.md @@ -0,0 +1 @@ +Create pie chart component diff --git a/src/app/components/charts/PieChart.tsx b/src/app/components/charts/PieChart.tsx new file mode 100644 index 000000000..f359e2c3d --- /dev/null +++ b/src/app/components/charts/PieChart.tsx @@ -0,0 +1,43 @@ +import { memo } from 'react' +import { ResponsiveContainer, Tooltip, PieChart as RechartsPieChart, Pie, Cell } from 'recharts' +import { TooltipContent, type Formatters } from './Tooltip' +import { COLORS } from '../../../styles/theme/colors' +import { COLORS as TESTNET_COLORS } from '../../../styles/theme/testnet/colors' + +interface PieChartProps extends Formatters { + data: T[] + dataKey: Extract +} + +const colorPalette = [COLORS.brandDark, COLORS.brandMedium, TESTNET_COLORS.testnet, COLORS.grayMedium2] + +const PieChartCmp = ({ data, dataKey, formatters }: PieChartProps) => { + 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 ( + + + } + offset={15} + /> + + {data.map((item, index) => { + const label = item[labelKey as keyof T] as string + return + })} + + + + ) +} + +export const PieChart = memo(PieChartCmp) as typeof PieChartCmp From aa1f314d03897c9fee623040eda9f6d2f0745bd5 Mon Sep 17 00:00:00 2001 From: Michal Zielenkiewicz Date: Thu, 21 Dec 2023 11:31:29 +0100 Subject: [PATCH 3/6] Create story for PieChart --- src/stories/Charts/PieChart.stories.tsx | 41 +++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/stories/Charts/PieChart.stories.tsx diff --git a/src/stories/Charts/PieChart.stories.tsx b/src/stories/Charts/PieChart.stories.tsx new file mode 100644 index 000000000..903bcd469 --- /dev/null +++ b/src/stories/Charts/PieChart.stories.tsx @@ -0,0 +1,41 @@ +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 + +interface DataItem { + x: string + y: number +} + +const data: DataItem[] = [ + { x: 'Active', y: 50 }, + { x: 'Inactive', y: 10 }, + { x: 'Waiting', y: 39 }, +] + +const Template: StoryFn> = args => { + return ( + + + + ) +} + +type Story = StoryObj> + +export const SamplePieChart: Story = { + render: Template, + args: { + data, + dataKey: 'y', + formatters: { + data: (value: number) => value.toLocaleString(), + label: (value: string) => `${value} validators`, + }, + }, +} From 15dc9f4fcf836c6a55dcd1853b82050fcc0886e9 Mon Sep 17 00:00:00 2001 From: Michal Zielenkiewicz Date: Thu, 4 Jan 2024 11:15:51 +0100 Subject: [PATCH 4/6] Add custom legend to pie chart --- src/app/components/charts/PieChart.tsx | 105 +++++++++++++++++++++++-- 1 file changed, 100 insertions(+), 5 deletions(-) diff --git a/src/app/components/charts/PieChart.tsx b/src/app/components/charts/PieChart.tsx index f359e2c3d..bd639dada 100644 --- a/src/app/components/charts/PieChart.tsx +++ b/src/app/components/charts/PieChart.tsx @@ -1,8 +1,16 @@ -import { memo } from 'react' -import { ResponsiveContainer, Tooltip, PieChart as RechartsPieChart, Pie, Cell } from 'recharts' +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 extends Formatters { data: T[] @@ -11,7 +19,70 @@ interface PieChartProps extends Formatters { const colorPalette = [COLORS.brandDark, COLORS.brandMedium, TESTNET_COLORS.testnet, COLORS.grayMedium2] +type CustomLegendProps = Props & { + activeIndex?: number +} + +const CustomLegend = (props: CustomLegendProps) => { + const { activeIndex, payload } = props + const { t } = useTranslation() + + return ( + + {payload?.map((item, index) => { + if (!item.payload) { + return null + } + + const payload = item.payload as PieSectorDataItem + const isActive = activeIndex === index + const label = payload.name + return ( + + + + + + {label} ( + {t('common.valuePair', { + value: payload.percent, + formatParams: { + value: { + style: 'percent', + maximumFractionDigits: 2, + } satisfies Intl.NumberFormatOptions, + }, + })} + ) + + + ) + })} + + ) +} + const PieChartCmp = ({ data, dataKey, formatters }: PieChartProps) => { + const [activeIndex, setActiveIndex] = useState() if (!data.length) { return null } @@ -19,7 +90,6 @@ const PieChartCmp = ({ data, dataKey, formatters }: PieChartPr if (!labelKey) { throw new Error('Not able to determine label key') } - return ( @@ -29,10 +99,35 @@ const PieChartCmp = ({ data, dataKey, formatters }: PieChartPr content={} offset={15} /> - + } + /> + setActiveIndex(index)} + onMouseLeave={() => setActiveIndex(undefined)} + stroke="none" + data={data} + innerRadius={40} + outerRadius={90} + paddingAngle={0} + dataKey={dataKey} + > {data.map((item, index) => { const label = item[labelKey as keyof T] as string - return + return ( + + ) })} From 3884e6ce97360f9c46bdf7db6cd6b4ded7001703 Mon Sep 17 00:00:00 2001 From: Michal Zielenkiewicz Date: Thu, 4 Jan 2024 11:16:47 +0100 Subject: [PATCH 5/6] Add compact variant to pie chart --- src/app/components/charts/PieChart.tsx | 24 ++++++++++++++---------- src/stories/Charts/PieChart.stories.tsx | 1 + 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/app/components/charts/PieChart.tsx b/src/app/components/charts/PieChart.tsx index bd639dada..15176322c 100644 --- a/src/app/components/charts/PieChart.tsx +++ b/src/app/components/charts/PieChart.tsx @@ -13,6 +13,7 @@ import { Props } from 'recharts/types/component/DefaultLegendContent' import { PieSectorDataItem } from 'recharts/types/polar/Pie' interface PieChartProps extends Formatters { + compact: boolean data: T[] dataKey: Extract } @@ -21,10 +22,11 @@ const colorPalette = [COLORS.brandDark, COLORS.brandMedium, TESTNET_COLORS.testn type CustomLegendProps = Props & { activeIndex?: number + compact: boolean } const CustomLegend = (props: CustomLegendProps) => { - const { activeIndex, payload } = props + const { activeIndex, compact, payload } = props const { t } = useTranslation() return ( @@ -41,8 +43,8 @@ const CustomLegend = (props: CustomLegendProps) => { { backgroundColor: isActive ? `${item.color}40` : 'transparent', }} > - + { ) } -const PieChartCmp = ({ data, dataKey, formatters }: PieChartProps) => { +const PieChartCmp = ({ compact, data, dataKey, formatters }: PieChartProps) => { const [activeIndex, setActiveIndex] = useState() if (!data.length) { return null @@ -100,19 +102,21 @@ const PieChartCmp = ({ data, dataKey, formatters }: PieChartPr offset={15} /> } + content={props => ( + + )} /> setActiveIndex(index)} onMouseLeave={() => setActiveIndex(undefined)} stroke="none" data={data} - innerRadius={40} - outerRadius={90} + innerRadius={compact ? 25 : 40} + outerRadius={compact ? 50 : 90} paddingAngle={0} dataKey={dataKey} > diff --git a/src/stories/Charts/PieChart.stories.tsx b/src/stories/Charts/PieChart.stories.tsx index 903bcd469..70ec02f7f 100644 --- a/src/stories/Charts/PieChart.stories.tsx +++ b/src/stories/Charts/PieChart.stories.tsx @@ -31,6 +31,7 @@ type Story = StoryObj> export const SamplePieChart: Story = { render: Template, args: { + compact: true, data, dataKey: 'y', formatters: { From 08b06fc55f312b919723122b1c3ea91cfc08d0ac Mon Sep 17 00:00:00 2001 From: Michal Zielenkiewicz Date: Fri, 12 Jan 2024 11:25:34 +0100 Subject: [PATCH 6/6] Set active element using label instead of index --- src/app/components/charts/PieChart.tsx | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/app/components/charts/PieChart.tsx b/src/app/components/charts/PieChart.tsx index 15176322c..ab88b7232 100644 --- a/src/app/components/charts/PieChart.tsx +++ b/src/app/components/charts/PieChart.tsx @@ -21,26 +21,26 @@ interface PieChartProps extends Formatters { const colorPalette = [COLORS.brandDark, COLORS.brandMedium, TESTNET_COLORS.testnet, COLORS.grayMedium2] type CustomLegendProps = Props & { - activeIndex?: number + activeLabel?: string compact: boolean } const CustomLegend = (props: CustomLegendProps) => { - const { activeIndex, compact, payload } = props + const { activeLabel, compact, payload } = props const { t } = useTranslation() return ( - {payload?.map((item, index) => { + {payload?.map(item => { if (!item.payload) { return null } const payload = item.payload as PieSectorDataItem - const isActive = activeIndex === index const label = payload.name + const isActive = activeLabel === label return ( - + { } const PieChartCmp = ({ compact, data, dataKey, formatters }: PieChartProps) => { - const [activeIndex, setActiveIndex] = useState() + const [activeLabel, setActiveLabel] = useState() if (!data.length) { return null } @@ -107,12 +107,12 @@ const PieChartCmp = ({ compact, data, dataKey, formatters }: P align="left" verticalAlign="middle" content={props => ( - + )} /> setActiveIndex(index)} - onMouseLeave={() => setActiveIndex(undefined)} + onMouseEnter={(item, index) => setActiveLabel(item[labelKey])} + onMouseLeave={() => setActiveLabel(undefined)} stroke="none" data={data} innerRadius={compact ? 25 : 40} @@ -128,7 +128,7 @@ const PieChartCmp = ({ compact, data, dataKey, formatters }: P key={`${label}-${index}`} name={label} style={{ - filter: activeIndex === index ? `drop-shadow(0px 0px 5px ${COLORS.grayMedium}` : 'none', + filter: activeLabel === label ? `drop-shadow(0px 0px 5px ${COLORS.grayMedium}` : 'none', }} /> )