Skip to content

feat(staking): [LW-8684] add tooltip to piechart #616

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import cn from 'classnames';
import { Fragment, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { TranslationKey } from '../i18n';
// import { PERCENTAGE_SCALE_MAX } from '../store';
import * as styles from './DelegationCard.css';
import { DelegationTooltip } from './DelegationTooltip';
import { DistributionItem } from './types';

// TODO
const PERCENTAGE_SCALE_MAX = 100;
Expand All @@ -15,17 +16,11 @@ export type DelegationStatus =
| 'under-allocated'
| 'no-selection';

type Distribution = Array<{
name: string;
percentage: number;
color: PieChartColor;
}>;

type DelegationCardProps = {
arrangement?: 'vertical' | 'horizontal';
balance: string;
cardanoCoinSymbol: string;
distribution: Distribution;
distribution: DistributionItem[];
status: DelegationStatus;
showDistribution?: boolean;
};
Expand Down Expand Up @@ -72,7 +67,7 @@ export const DelegationCard = ({

const { data, colorSet = PIE_CHART_DEFAULT_COLOR_SET } = useMemo((): {
colorSet?: PieChartColor[];
data: Distribution;
data: DistributionItem[];
} => {
const GREY_COLOR: PieChartColor = '#C0C0C0';
const RED_COLOR: PieChartColor = '#FF5470';
Expand Down Expand Up @@ -118,7 +113,7 @@ export const DelegationCard = ({
data-testid="delegation-info-card"
>
<div className={styles.chart} data-testid="delegation-chart">
<PieChart data={data} nameKey="name" valueKey="percentage" colors={colorSet} />
<PieChart data={data} nameKey="name" valueKey="percentage" colors={colorSet} tooltip={DelegationTooltip} />
{showDistribution && <Text.SubHeading className={styles.counter}>{totalPercentage}%</Text.SubHeading>}
</div>
<div
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { style } from '@vanilla-extract/css';
import { theme } from '../theme';

export const tooltip = style({
background: theme.colors.$tooltipBgColor,
borderRadius: theme.radius.$small,
boxShadow: theme.elevation.$tooltip,
margin: theme.spacing.$10,
maxWidth: theme.spacing.$214,
padding: theme.spacing.$16,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Box, Flex, RichContentInner, TooltipContentRendererProps } from '@lace/ui';
import { ReactElement } from 'react';
import { useTranslation } from 'react-i18next';
import * as styles from './DelegationTooltip.css';
import { DistributionItem } from './types';

export const DelegationTooltip = ({
active,
name,
payload,
}: Readonly<TooltipContentRendererProps<DistributionItem & { fill?: string }>>): ReactElement | null => {
const { t } = useTranslation();
if (active && payload) {
const { apy, saturation, fill } = payload;

return (
<Box className={styles.tooltip}>
<RichContentInner
title={name || ''}
dotColor={fill}
description={
<Box w="$148">
<Flex justifyContent="space-between">
<Box>{t('browsePools.stakePoolTableBrowser.tableHeader.ros.title')}</Box>
<Box>{apy ? `${apy}%` : '-'}</Box>
</Flex>
<Flex justifyContent="space-between">
<Box>{t('browsePools.stakePoolTableBrowser.tableHeader.saturation.title')}</Box>
<Box>{saturation ? `${saturation}%` : '-'}</Box>
</Flex>
</Box>
}
/>
</Box>
);
}

// eslint-disable-next-line unicorn/no-null
return null;
};
9 changes: 9 additions & 0 deletions packages/staking/src/features/delegation-card/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { PieChartColor } from '@lace/ui';

export type DistributionItem = {
name: string;
percentage: number;
color: PieChartColor;
apy?: string;
saturation?: string;
};
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,20 @@ export const StepPreferencesContent = () => {

const displayData = draftPortfolio.map((draftPool, i) => {
const {
displayData: { name },
displayData: { name, apy, saturation },
id,
sliderIntegerPercentage,
} = draftPool;

return {
apy: apy ? String(apy) : undefined,
cardanoCoinSymbol,
color: PIE_CHART_DEFAULT_COLOR_SET[i] as PieChartColor,
id,
name: name || '-',
onChainPercentage: draftPool.basedOnCurrentPortfolio ? draftPool.onChainPercentage : undefined,
percentage: sliderIntegerPercentage,
saturation: saturation ? String(saturation) : undefined,
savedIntegerPercentage: draftPool.basedOnCurrentPortfolio ? draftPool.savedIntegerPercentage : undefined,
// TODO
sliderIntegerPercentage,
Expand Down
4 changes: 3 additions & 1 deletion packages/staking/src/features/overview/Overview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,12 @@ export const Overview = () => {
<DelegationCard
balance={compactNumber(balancesBalance.available.coinBalance)}
cardanoCoinSymbol={walletStoreWalletUICardanoCoin.symbol}
distribution={displayData.map(({ color, name = '-', onChainPercentage }) => ({
distribution={displayData.map(({ color, name = '-', onChainPercentage, apy, saturation }) => ({
apy: apy ? String(apy) : undefined,
color,
name,
percentage: onChainPercentage,
saturation: saturation ? String(saturation) : undefined,
}))}
status={currentPortfolio.length === 1 ? 'simple-delegation' : 'multi-delegation'}
/>
Expand Down
4 changes: 3 additions & 1 deletion packages/staking/src/features/overview/OverviewPopup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,12 @@ export const OverviewPopup = () => {
balance={compactNumber(balancesBalance.available.coinBalance)}
cardanoCoinSymbol={walletStoreWalletUICardanoCoin.symbol}
arrangement="vertical"
distribution={displayData.map(({ color, name = '-', onChainPercentage }) => ({
distribution={displayData.map(({ color, name = '-', onChainPercentage, apy, saturation }) => ({
apy: apy ? String(apy) : undefined,
color,
name,
percentage: onChainPercentage,
saturation: saturation ? String(saturation) : undefined,
}))}
status={currentPortfolio.length === 1 ? 'simple-delegation' : 'multi-delegation'}
/>
Expand Down
3 changes: 3 additions & 0 deletions packages/staking/src/features/theme/colors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const colorsContract = {
$sliderFillSecondary: '',
$sliderKnobFill: '',
$sliderRailFill: '',
$tooltipBgColor: '',
};

export const lightThemeColors: typeof colorsContract = {
Expand All @@ -48,6 +49,7 @@ export const lightThemeColors: typeof colorsContract = {
$sliderFillSecondary: lightColorScheme.$primary_dark_grey,
$sliderKnobFill: lightColorScheme.$primary_white,
$sliderRailFill: lightColorScheme.$primary_light_grey_plus,
$tooltipBgColor: lightColorScheme.$primary_white,
};

export const darkThemeColors: typeof colorsContract = {
Expand Down Expand Up @@ -78,4 +80,5 @@ export const darkThemeColors: typeof colorsContract = {
$sliderFillSecondary: darkColorScheme.$primary_light_grey,
$sliderKnobFill: lightColorScheme.$primary_black,
$sliderRailFill: darkColorScheme.$primary_dark_grey_plus,
$tooltipBgColor: darkColorScheme.$primary_mid_grey,
};
2 changes: 1 addition & 1 deletion packages/ui/src/design-system/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export * as FlowCard from './flow-card';
export * as IconButton from './icon-buttons';
export * as TransactionSummary from './transaction-summary';
export { ToastBar } from './toast-bar';
export { Tooltip } from './tooltip';
export * from './tooltip';
export { Message } from './message';
export { PasswordBox } from './password-box';
export { Metadata } from './metadata';
Expand Down
6 changes: 5 additions & 1 deletion packages/ui/src/design-system/pie-chart/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
export type { PieChartColor, PieChartProps } from './pie-chart.component';
export type {
PieChartColor,
PieChartProps,
TooltipContentRendererProps,
} from './pie-chart.component';
export { PieChart } from './pie-chart.component';
export {
PIE_CHART_DEFAULT_COLOR_SET,
Expand Down
80 changes: 75 additions & 5 deletions packages/ui/src/design-system/pie-chart/pie-chart.component.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/* eslint-disable functional/prefer-immutable-types */
import React from 'react';
import type { ReactElement, ReactNode } from 'react';
import React, { isValidElement, useMemo, useState } from 'react';

import isFunction from 'lodash/isFunction';
import {
Cell,
Pie,
Expand All @@ -15,20 +17,58 @@ import {
} from './pie-chart.data';

import type { ColorValueHex } from '../../types';
import type { CellProps, TooltipProps } from 'recharts';
import type { CellProps } from 'recharts';
import type { PickByValue } from 'utility-types';

type PieChartDataProps = Partial<{
overrides: CellProps;
}>;
export type PieChartColor = ColorValueHex | PieChartGradientColor;

export interface TooltipContentRendererProps<T> {
active?: boolean;
name?: string;
payload?: T;
}
export type TooltipContentRenderer<T> = (
props: TooltipContentRendererProps<T>,
) => ReactNode;
type TooltipContent<T> = ReactElement | TooltipContentRenderer<T>;

interface RechartTooltipContentRendererProps<T> {
name?: string;
active?: boolean;
payload?: { name?: string; payload?: T }[];
}

type RechartTooltipContentRenderer<T> = (
props: RechartTooltipContentRendererProps<T>,
) => ReactNode;

// Recharts passes to the renderer for some reason the payload as
// a list which is a bit cumbersome because in practice we care just about the
// first element and the adapter below removes this inconvenience
const transformTooltipContentRenderer =
<T extends object>(
tooltipContentRenderer: TooltipContentRenderer<T>,
): RechartTooltipContentRenderer<T> =>
({
active,
payload,
}: {
active?: boolean;
payload?: { name?: string; payload?: T }[];
}) =>
tooltipContentRenderer({ active, ...payload?.[0] });

interface PieChartBaseProps<T extends object> {
animate?: boolean;
colors?: PieChartColor[];
data: (PieChartDataProps & T)[];
direction?: 'clockwise' | 'counterclockwise';
tooltip?: TooltipProps<number, string>['content'];
tooltip?: TooltipContent<T>;
}

interface PieChartCustomKeyProps<T extends object>
extends PieChartBaseProps<T> {
nameKey: keyof PickByValue<T, string>;
Expand Down Expand Up @@ -63,6 +103,7 @@ const formatPieColor = (color: PieChartColor): string =>
* @param tooltip component accepted by Recharts Tooltip `content` prop
* @param valueKey object key of a `data` item that will be used as value (displayed in the tooltip)
*/

export const PieChart = <T extends object | { name: string; value: number }>({
animate = true,
colors = PIE_CHART_DEFAULT_COLOR_SET,
Expand All @@ -73,17 +114,46 @@ export const PieChart = <T extends object | { name: string; value: number }>({
valueKey = 'value',
}: PieChartProps<T>): JSX.Element => {
const data = inputData.slice(0, colors.length);
const [tooltipPosition, setTooltipPosition] = useState({ x: 0, y: 0 });

const tooltipContent = useMemo(() => {
if (!tooltip || isValidElement(tooltip)) {
return tooltip;
}

if (isFunction(tooltip)) {
return transformTooltipContentRenderer(tooltip);
}
}, [tooltip]);

const handleMouseMove = (event: React.MouseEvent<HTMLDivElement>): void => {
const clientWidth = window.innerWidth;
if (event.target instanceof SVGSVGElement && clientWidth > 360) {
const { x, y } = event.target.getBoundingClientRect();
setTooltipPosition({ x: event.clientX - x, y: event.clientY - y });
}
};

return (
<ResponsiveContainer aspect={1}>
<RechartsPieChart>
<RechartsPieChart
onMouseMove={(_, event): void => {
handleMouseMove(event as React.MouseEvent<HTMLDivElement>);
}}
>
<defs>
<linearGradient id={PieChartGradientColor.LaceLinearGradient}>
<stop offset="-18%" stopColor="#FDC300" />
<stop offset="120%" stopColor="#FF92E1" />
</linearGradient>
</defs>
{Boolean(tooltip) && <Tooltip content={tooltip} />}
{tooltipContent && (
<Tooltip
wrapperStyle={{ zIndex: 1 }}
content={tooltipContent}
position={tooltipPosition}
/>
)}
<Pie
data={data}
dataKey={valueKey}
Expand Down
14 changes: 12 additions & 2 deletions packages/ui/src/design-system/pie-chart/pie-chart.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { ReactElement } from 'react';
import React from 'react';

import type { Meta } from '@storybook/react';
Expand All @@ -7,6 +8,7 @@ import { ThemeColorScheme, LocalThemeProvider } from '../../design-tokens';
import { page, Section, Variants } from '../decorators';
import { Divider } from '../divider';
import { Cell, Grid } from '../grid';
import { Content, ContentInner } from '../tooltip';

import { PieChart } from './pie-chart.component';
import {
Expand Down Expand Up @@ -44,6 +46,12 @@ const meta: Meta<typeof PieChart> = {

export default meta;

const CustomTooltip = (): ReactElement => (
<Content>
<ContentInner label="This is an example tooltip" />
</Content>
);

export const Overview = (): JSX.Element => (
<Grid columns="$1">
<Cell>
Expand Down Expand Up @@ -231,12 +239,13 @@ export const Overview = (): JSX.Element => (

type ConfigurableStoryProps = Pick<
PieChartProps<{ name: string; value: number }>,
'colors' | 'data' | 'direction' | 'tooltip'
>;
'colors' | 'data' | 'direction'
> & { tooltip: boolean };

export const Controls = ({
colors,
data,
tooltip,
...props
}: Readonly<ConfigurableStoryProps>): JSX.Element => (
<Grid columns="$5">
Expand All @@ -245,6 +254,7 @@ export const Controls = ({
animate={isNotInChromatic}
colors={colors}
data={data}
tooltip={tooltip ? CustomTooltip : undefined}
{...props}
/>
</Cell>
Expand Down
Loading