Skip to content

Commit

Permalink
πŸ˜΅β€πŸ’« Crt holders widget and pie chart (#4897)
Browse files Browse the repository at this point in the history
* Initial work

* Add hover effect to pie chart

* Initial work on CrtHoldersWidget.tsx

* Add multiple holders entry

* CR fixes
  • Loading branch information
WRadoslaw authored Sep 26, 2023
1 parent de8d815 commit c61d014
Show file tree
Hide file tree
Showing 8 changed files with 396 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/atlas/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"@loadable/component": "^5.15.2",
"@lottiefiles/react-lottie-player": "^3.5.0",
"@nivo/line": "^0.83.0",
"@nivo/pie": "^0.83.0",
"@segment/analytics-next": "^1.53.0",
"@sentry/react": "^7.53.1",
"@talismn/connect-wallets": "^1.2.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Meta, StoryFn } from '@storybook/react'

import { PieChart, PieChartProps } from './PieChart'

export default {
title: 'charts/PieChart',
component: PieChart,
} as Meta<PieChartProps>

const Template: StoryFn<PieChartProps> = (args) => (
<div style={{ height: 400 }}>
<PieChart {...args} />
</div>
)

const data = [
{
index: 0,
id: 'japan',
value: 40,
},
{
index: 1,
id: 'korea',
value: 60,
},
]

export const Default = Template.bind({})
Default.args = {
data,
}
74 changes: 74 additions & 0 deletions packages/atlas/src/components/_charts/PieChart/PieChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { ComputedDatum, PieSvgProps, ResponsivePie } from '@nivo/pie'
import { MayHaveLabel } from '@nivo/pie/dist/types/types'
import { animated } from '@react-spring/web'
import { useState } from 'react'

import { Text } from '@/components/Text'
import { cVar } from '@/styles'

export type PieDatum = {
id: string
value: number
index: number
} & MayHaveLabel
type ReponsiveProps = Omit<PieSvgProps<PieDatum>, 'width' | 'height'>

export const joystreamColors = ['#9FACFF', '#7174FF', '#BECAFF', '#1B186C']

const defaultJoystreamProps: Omit<ReponsiveProps, 'data'> = {
isInteractive: true,
enableArcLinkLabels: false,
arcLabelsRadiusOffset: 0.5,
arcLabelsComponent: (datum) => {
return (
<animated.g transform={datum.style.transform}>
<foreignObject height={15} width={40}>
<Text variant="h100" as="h1">
{datum.datum.formattedValue}
</Text>
</foreignObject>
</animated.g>
)
},
theme: {
tooltip: {
container: {
background: cVar('colorBackgroundStrong'),
},
},
},
}

export type PieChartProps = {
onDataHover?: (data: PieDatum | null) => void
hoveredData: PieDatum | null
hoverOpacity?: boolean
} & ReponsiveProps
export const PieChart = (props: PieChartProps) => {
const [hoveredEntry, setHoveredEntry] = useState<ComputedDatum<PieDatum> | null>(null)

const getColor = (entry: Omit<ComputedDatum<PieDatum>, 'color' | 'fill' | 'arc'>) => {
const color = joystreamColors[entry.data.index % joystreamColors.length]
if (!props.hoverOpacity || entry.id === (props.hoveredData ? props.hoveredData.id : hoveredEntry?.id)) {
return color
} else {
return `${color}4D`
}
}

return (
<ResponsivePie
onMouseEnter={(entry) => {
setHoveredEntry(entry)
props.onDataHover?.(entry.data)
}}
onMouseLeave={() => {
setHoveredEntry(null)
props.onDataHover?.(null)
}}
colors={getColor}
{...defaultJoystreamProps}
{...props}
/>
)
}
1 change: 1 addition & 0 deletions packages/atlas/src/components/_charts/PieChart/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './PieChart'
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { ApolloProvider } from '@apollo/client'
import { Meta, StoryFn } from '@storybook/react'

import { createApolloClient } from '@/api'
import { CrtHoldersWidget, CrtHoldersWidgetProps } from '@/components/_crt/CrtHoldersWidget/CrtHoldersWidget'
import { AuthProvider } from '@/providers/auth/auth.provider'
import { JoystreamProvider } from '@/providers/joystream/joystream.provider'
import { OverlayManagerProvider } from '@/providers/overlayManager'
import { SegmentAnalyticsProvider } from '@/providers/segmentAnalytics/segment.provider'
import { UserProvider } from '@/providers/user/user.provider'
import { WalletProvider } from '@/providers/wallet/wallet.provider'

export default {
title: 'crt/CrtHoldersWidget',
component: CrtHoldersWidget,
args: {
holders: [
{
value: 50,
name: 'Bedeho',
members: [
{
handle: 'Bedeho',
avatarUrls: [],
},
],
},
{
value: 30,
name: 'Dima',
members: [
{
handle: 'Dima',
avatarUrls: [],
},
],
},
{
name: 'Others',
value: 20,
members: [
{
handle: 'Radek',
avatarUrls: [],
},
{
handle: 'Theo',
avatarUrls: [],
},
],
},
],
},
decorators: [
(Story) => {
const apolloClient = createApolloClient()

return (
<JoystreamProvider>
<SegmentAnalyticsProvider>
<WalletProvider>
<ApolloProvider client={apolloClient}>
<AuthProvider>
<OverlayManagerProvider>
<UserProvider>
<Story />
</UserProvider>
</OverlayManagerProvider>
</AuthProvider>
</ApolloProvider>
</WalletProvider>
</SegmentAnalyticsProvider>
</JoystreamProvider>
)
},
],
} as Meta<CrtHoldersWidgetProps>

export const Default: StoryFn<CrtHoldersWidgetProps> = (args) => <CrtHoldersWidget {...args} />
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import styled from '@emotion/styled'
import { useMemo, useState } from 'react'

import { SvgActionChevronR } from '@/assets/icons'
import { Avatar } from '@/components/Avatar'
import { AvatarGroup } from '@/components/Avatar/AvatarGroup'
import { FlexBox } from '@/components/FlexBox'
import { Text } from '@/components/Text'
import { TextButton } from '@/components/_buttons/Button'
import { PieChart, PieDatum, joystreamColors } from '@/components/_charts/PieChart'
import { Widget } from '@/components/_crt/CrtStatusWidget/CrtStatusWidget.styles'
import { useUser } from '@/providers/user/user.hooks'
import { cVar } from '@/styles'

export type HolderDatum = {
value: number
name: string
members: {
handle: string
avatarUrls: string[]
}[]
}

export type CrtHoldersWidgetProps = {
holders: HolderDatum[]
}

export const CrtHoldersWidget = ({ holders }: CrtHoldersWidgetProps) => {
const { activeMembership } = useUser()
const [hoveredHolder, setHoveredHolder] = useState<PieDatum | null>(null)
const chartData = useMemo(
() =>
holders.map((holder, index) => ({
id: holder.name,
value: holder.value,
members: holder.members,
index,
})),
[holders]
)
const owner = useMemo(
() => chartData.find((holder) => holder.id === activeMembership?.handle),
[chartData, activeMembership?.handle]
)
return (
<Widget
title="Holders"
titleVariant="h500"
titleColor="colorTextStrong"
customTopRightNode={
<TextButton icon={<SvgActionChevronR />} iconPlacement="right">
Show more
</TextButton>
}
customNode={
<FlexBox width="100%" gap={12} equalChildren>
<FlexBox flow="column" width="100%">
<Text variant="h100" as="h1" color="colorTextMuted">
TOTAL SUPPLY
</Text>
<ChartWrapper>
<PieChart
data={chartData}
onDataHover={setHoveredHolder}
hoverOpacity
hoveredData={hoveredHolder}
valueFormat={(value) => `${value}%`}
/>
</ChartWrapper>
</FlexBox>
<FlexBox flow="column" gap={6}>
<FlexBox flow="column" gap={2}>
<Text variant="h100" as="h1" margin={{ bottom: 4 }} color="colorTextMuted">
YOU OWN
</Text>
{owner && (
<HoldersLegendEntry
key={owner.id}
name={owner.id}
members={owner.members}
color={joystreamColors[owner.index]}
value={owner.value}
isActive={owner.id === hoveredHolder?.id}
onMouseEnter={() => setHoveredHolder(owner)}
onMouseExit={() => setHoveredHolder(null)}
/>
)}
</FlexBox>
<FlexBox flow="column" gap={2}>
<Text variant="h100" as="h1" margin={{ bottom: 4 }} color="colorTextMuted">
TOP HOLDERS
</Text>
{chartData.map((row) =>
row.id === activeMembership?.handle ? null : (
<HoldersLegendEntry
key={row.id}
name={row.id}
members={row.members}
color={joystreamColors[row.index]}
value={row.value}
isActive={row.id === hoveredHolder?.id}
onMouseEnter={() => setHoveredHolder(row)}
onMouseExit={() => setHoveredHolder(null)}
/>
)
)}
</FlexBox>
</FlexBox>
</FlexBox>
}
/>
)
}

type HoldersLegendEntryProps = {
name: string
value: number
color: string
isActive: boolean
onMouseEnter: () => void
onMouseExit: () => void
members: {
handle: string
avatarUrls: string[]
}[]
}

const HoldersLegendEntry = ({
name,
value,
color,
isActive,
onMouseExit,
onMouseEnter,
members,
}: HoldersLegendEntryProps) => {
return (
<FlexBox
gap={2}
alignItems="center"
style={{ opacity: isActive ? 1 : 0.3 }}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseExit}
>
<ColorBox color={color} />
<FlexBox alignItems="center">
{members.length === 1 ? (
<Avatar assetUrls={members[0].avatarUrls} />
) : (
<AvatarGroup
avatars={members.map((member) => ({ urls: member.avatarUrls, tooltipText: member.handle }))}
avatarStrokeColor={cVar('colorBackgroundMuted')}
/>
)}
<Text variant="t100" as="p">
{name}
</Text>
</FlexBox>
<Text variant="t100" as="p">
{value}%
</Text>
</FlexBox>
)
}

const ColorBox = styled.div<{ color: string }>`
min-width: 24px;
min-height: 24px;
background-color: ${(props) => props.color};
`

const ChartWrapper = styled.div`
height: 300px;
width: 100%;
`
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './CrtHoldersWidget'
Loading

0 comments on commit c61d014

Please sign in to comment.