[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:
nitin
2025-09-17 20:14:50 +05:30
committed by GitHub
parent 09b9071182
commit bc471deaba
15 changed files with 884 additions and 229 deletions
@@ -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>
);
};
@@ -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>
);
};
@@ -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,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,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',