[Dashboards] - refactor - gauge and number chart (#14550)
This is the fourth and last PR from the split of https://github.com/twentyhq/twenty/pull/14458 - refactoring PieChart and NumberChart widgets.
This commit is contained in:
-216
@@ -1,216 +0,0 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
type RadialBarCustomLayerProps,
|
||||
ResponsiveRadialBar,
|
||||
} from '@nivo/radial-bar';
|
||||
import { useId, useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { H1Title, H1TitleFontColor } from 'twenty-ui/display';
|
||||
import { type GraphColor } from '../types/GraphColor';
|
||||
import { createGradientDef } from '../utils/createGradientDef';
|
||||
import { createGraphColorRegistry } from '../utils/createGraphColorRegistry';
|
||||
import { getColorScheme } from '../utils/getColorScheme';
|
||||
import {
|
||||
formatGraphValue,
|
||||
type GraphValueFormatOptions,
|
||||
} from '../utils/graphFormatters';
|
||||
import { GraphWidgetChartContainer } from './GraphWidgetChartContainer';
|
||||
import { GraphWidgetLegend } from './GraphWidgetLegend';
|
||||
import { GraphWidgetTooltip } from './GraphWidgetTooltip';
|
||||
|
||||
type GaugeChartData = {
|
||||
value: number;
|
||||
min: number;
|
||||
max: number;
|
||||
color?: GraphColor;
|
||||
to?: string;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
type GraphWidgetGaugeChartProps = {
|
||||
data: GaugeChartData;
|
||||
showValue?: boolean;
|
||||
showLegend?: boolean;
|
||||
id: string;
|
||||
} & GraphValueFormatOptions;
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledH1Title = styled(H1Title)`
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -150%);
|
||||
`;
|
||||
|
||||
export const GraphWidgetGaugeChart = ({
|
||||
data,
|
||||
showValue = true,
|
||||
showLegend = true,
|
||||
id,
|
||||
displayType,
|
||||
decimals,
|
||||
prefix,
|
||||
suffix,
|
||||
customFormatter,
|
||||
}: GraphWidgetGaugeChartProps) => {
|
||||
const { value, min, max, color = 'blue', to, label = 'Value' } = data;
|
||||
const theme = useTheme();
|
||||
const instanceId = useId();
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const colorRegistry = createGraphColorRegistry(theme);
|
||||
const colorScheme = getColorScheme(colorRegistry, color);
|
||||
|
||||
const formatOptions: GraphValueFormatOptions = {
|
||||
displayType,
|
||||
decimals,
|
||||
prefix,
|
||||
suffix,
|
||||
customFormatter,
|
||||
};
|
||||
|
||||
const formattedValue = formatGraphValue(value, formatOptions);
|
||||
|
||||
const normalizedValue = max === min ? 0 : ((value - min) / (max - min)) * 100;
|
||||
const clampedNormalizedValue = Math.max(0, Math.min(100, normalizedValue));
|
||||
|
||||
const chartData = [
|
||||
{
|
||||
id: 'gauge',
|
||||
data: [
|
||||
{ x: 'value', y: clampedNormalizedValue },
|
||||
{ x: 'empty', y: 100 - clampedNormalizedValue },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const gradientId = `gaugeGradient-${id}-${instanceId}`;
|
||||
const gaugeAngle = -90 + (clampedNormalizedValue / 100) * 90;
|
||||
const gradientDef = createGradientDef(
|
||||
colorScheme,
|
||||
gradientId,
|
||||
isHovered,
|
||||
gaugeAngle,
|
||||
);
|
||||
const defs = [gradientDef];
|
||||
|
||||
const handleClick = () => {
|
||||
if (isDefined(to)) {
|
||||
window.location.href = to;
|
||||
}
|
||||
};
|
||||
|
||||
const renderTooltip = () => {
|
||||
const formattedWithPercentage = `${formattedValue} (${normalizedValue.toFixed(1)}%)`;
|
||||
|
||||
return (
|
||||
<GraphWidgetTooltip
|
||||
items={[
|
||||
{
|
||||
label: label,
|
||||
formattedValue: formattedWithPercentage,
|
||||
dotColor: colorScheme.solid,
|
||||
},
|
||||
]}
|
||||
showClickHint={isDefined(to)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderValueEndLine = (props: RadialBarCustomLayerProps) => {
|
||||
if (clampedNormalizedValue === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { center, bars } = props;
|
||||
|
||||
const valueBar = bars?.find((bar) => bar.data.x === 'value');
|
||||
if (!valueBar) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const endAngle = valueBar.arc.endAngle - Math.PI / 2;
|
||||
const arcInnerRadius = valueBar.arc.innerRadius;
|
||||
const arcOuterRadius = valueBar.arc.outerRadius;
|
||||
|
||||
const [centerX, centerY] = center;
|
||||
const x1 = centerX + Math.cos(endAngle) * arcInnerRadius;
|
||||
const y1 = centerY + Math.sin(endAngle) * arcInnerRadius;
|
||||
const x2 = centerX + Math.cos(endAngle) * arcOuterRadius;
|
||||
const y2 = centerY + Math.sin(endAngle) * arcOuterRadius;
|
||||
|
||||
return (
|
||||
<g>
|
||||
<line
|
||||
x1={x1}
|
||||
y1={y1}
|
||||
x2={x2}
|
||||
y2={y2}
|
||||
stroke={colorScheme.solid}
|
||||
strokeWidth={1}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<GraphWidgetChartContainer
|
||||
$isClickable={isDefined(to)}
|
||||
$cursorSelector='svg g path[fill^="url(#"]'
|
||||
>
|
||||
<ResponsiveRadialBar
|
||||
data={chartData}
|
||||
startAngle={-90}
|
||||
endAngle={90}
|
||||
innerRadius={0.7}
|
||||
padding={0.2}
|
||||
colors={[`url(#${gradientId})`, theme.background.tertiary]}
|
||||
defs={defs}
|
||||
fill={[
|
||||
{
|
||||
match: (d: { x: string }) => d.x === 'value',
|
||||
id: gradientId,
|
||||
},
|
||||
]}
|
||||
enableTracks={false}
|
||||
enableRadialGrid={false}
|
||||
enableCircularGrid={false}
|
||||
enableLabels={false}
|
||||
isInteractive={true}
|
||||
tooltip={renderTooltip}
|
||||
onClick={handleClick}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
layers={['bars', renderValueEndLine]}
|
||||
/>
|
||||
{showValue && (
|
||||
<StyledH1Title
|
||||
title={formattedValue}
|
||||
fontColor={H1TitleFontColor.Primary}
|
||||
/>
|
||||
)}
|
||||
</GraphWidgetChartContainer>
|
||||
<GraphWidgetLegend
|
||||
show={showLegend}
|
||||
items={[
|
||||
{
|
||||
id: 'gauge',
|
||||
label: label,
|
||||
formattedValue: formattedValue,
|
||||
color: colorScheme.solid,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
-67
@@ -1,67 +0,0 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
H1Title,
|
||||
H1TitleFontColor,
|
||||
IconTrendingDown,
|
||||
IconTrendingUp,
|
||||
} from 'twenty-ui/display';
|
||||
|
||||
// props are subjected to change
|
||||
type GraphWidgetNumberChartProps = {
|
||||
value: string;
|
||||
trendPercentage: number;
|
||||
};
|
||||
|
||||
const StyledTrendPercentageValue = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
font-size: ${({ theme }) => theme.font.size.xs};
|
||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||
margin-right: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledTrendIconContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const StyledH1Title = styled(H1Title)`
|
||||
margin: 0;
|
||||
`;
|
||||
|
||||
export const GraphWidgetNumberChart = ({
|
||||
value,
|
||||
trendPercentage,
|
||||
}: GraphWidgetNumberChartProps) => {
|
||||
const theme = useTheme();
|
||||
const formattedPercentage =
|
||||
trendPercentage >= 0 ? `+${trendPercentage}` : `${trendPercentage}`;
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledH1Title title={value} fontColor={H1TitleFontColor.Primary} />
|
||||
<StyledTrendIconContainer>
|
||||
<StyledTrendPercentageValue>
|
||||
{formattedPercentage}%
|
||||
</StyledTrendPercentageValue>
|
||||
{trendPercentage >= 0 ? (
|
||||
<IconTrendingUp
|
||||
color={theme.color.turquoise40}
|
||||
size={theme.icon.size.md}
|
||||
/>
|
||||
) : (
|
||||
// question for product - whats the exact red here? cant see it on figma
|
||||
<IconTrendingDown color={theme.color.red} size={theme.icon.size.md} />
|
||||
)}
|
||||
</StyledTrendIconContainer>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
+6
-6
@@ -1,7 +1,7 @@
|
||||
import { GraphType } from '@/page-layout/mocks/mockWidgets';
|
||||
import { getDefaultWidgetData } from '@/page-layout/utils/getDefaultWidgetData';
|
||||
import { ChartSkeletonLoader } from '@/page-layout/widgets/graph/components/ChartSkeletonLoader';
|
||||
import { GraphWidgetNumberChart } from '@/page-layout/widgets/graph/components/GraphWidgetNumberChart';
|
||||
import { GraphWidgetNumberChart } from '@/page-layout/widgets/graph/graphWidgetNumberChart/components/GraphWidgetNumberChart';
|
||||
import { type GraphWidget } from '@/page-layout/widgets/graph/types/GraphWidget';
|
||||
import { lazy, Suspense } from 'react';
|
||||
|
||||
@@ -30,11 +30,11 @@ const GraphWidgetPieChart = lazy(() =>
|
||||
);
|
||||
|
||||
const GraphWidgetGaugeChart = lazy(() =>
|
||||
import('@/page-layout/widgets/graph/components/GraphWidgetGaugeChart').then(
|
||||
(module) => ({
|
||||
default: module.GraphWidgetGaugeChart,
|
||||
}),
|
||||
),
|
||||
import(
|
||||
'@/page-layout/widgets/graph/graphWidgetGaugeChart/components/GraphWidgetGaugeChart'
|
||||
).then((module) => ({
|
||||
default: module.GraphWidgetGaugeChart,
|
||||
})),
|
||||
);
|
||||
|
||||
type GraphWidgetRendererProps = {
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/react';
|
||||
|
||||
import { CatalogDecorator, ComponentDecorator } from 'twenty-ui/testing';
|
||||
import { GraphWidgetGaugeChart } from '../GraphWidgetGaugeChart';
|
||||
import { GraphWidgetGaugeChart } from '@/page-layout/widgets/graph/graphWidgetGaugeChart/components/GraphWidgetGaugeChart';
|
||||
|
||||
const meta: Meta<typeof GraphWidgetGaugeChart> = {
|
||||
title: 'Modules/PageLayout/Widgets/GraphWidgetGaugeChart',
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/react';
|
||||
|
||||
import { ComponentDecorator } from 'twenty-ui/testing';
|
||||
import { GraphWidgetNumberChart } from '../GraphWidgetNumberChart';
|
||||
import { GraphWidgetNumberChart } from '@/page-layout/widgets/graph/graphWidgetNumberChart/components/GraphWidgetNumberChart';
|
||||
|
||||
const meta: Meta<typeof GraphWidgetNumberChart> = {
|
||||
title: 'Modules/PageLayout/Widgets/GraphWidgetNumberChart',
|
||||
|
||||
Reference in New Issue
Block a user