diff --git a/packages/twenty-front/package.json b/packages/twenty-front/package.json
index 8e8a21d849..9a7bb4d181 100644
--- a/packages/twenty-front/package.json
+++ b/packages/twenty-front/package.json
@@ -47,6 +47,7 @@
"@lingui/react": "^5.1.2",
"@nivo/core": "^0.99.0",
"@nivo/line": "^0.99.0",
+ "@nivo/pie": "^0.99.0",
"@nivo/radial-bar": "^0.99.0",
"@react-pdf/renderer": "^4.1.6",
"@scalar/api-reference-react": "^0.4.36",
diff --git a/packages/twenty-front/src/modules/dashboards/graphs/components/GraphWidgetGaugeChart.tsx b/packages/twenty-front/src/modules/dashboards/graphs/components/GraphWidgetGaugeChart.tsx
index 7f3b013e30..884b92fbaf 100644
--- a/packages/twenty-front/src/modules/dashboards/graphs/components/GraphWidgetGaugeChart.tsx
+++ b/packages/twenty-front/src/modules/dashboards/graphs/components/GraphWidgetGaugeChart.tsx
@@ -1,26 +1,33 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
-import { t } from '@lingui/core/macro';
-import { ResponsiveRadialBar } from '@nivo/radial-bar';
-import { useState } from 'react';
import {
- AppTooltip,
- H1Title,
- H1TitleFontColor,
- IconArrowUpRight,
- TooltipDelay,
-} from 'twenty-ui/display';
+ type RadialBarCustomLayerProps,
+ ResponsiveRadialBar,
+} from '@nivo/radial-bar';
+import { useState } from 'react';
+import { isDefined } from 'twenty-shared/utils';
+import { H1Title, H1TitleFontColor } from 'twenty-ui/display';
+
+import { createGradientDef } from '../utils/createGradientDef';
+import { createGraphColorRegistry } from '../utils/createGraphColorRegistry';
+import { getColorSchemeByName } from '../utils/getColorSchemeByName';
+import {
+ formatGraphValue,
+ type GraphValueFormatOptions,
+} from '../utils/graphFormatters';
+import { GraphWidgetLegend } from './GraphWidgetLegend';
+import { GraphWidgetTooltip } from './GraphWidgetTooltip';
type GraphWidgetGaugeChartProps = {
value: number;
min: number;
max: number;
- unit: string;
showValue?: boolean;
+ showLegend?: boolean;
legendLabel: string;
- tooltipHref: string;
+ tooltipHref?: string;
id: string;
-};
+} & GraphValueFormatOptions;
const StyledContainer = styled.div`
align-items: center;
@@ -44,88 +51,37 @@ const StyledH1Title = styled(H1Title)`
transform: translate(-50%, -150%);
`;
-const StyledLegendContainer = styled.div`
- align-items: center;
- display: flex;
- gap: ${({ theme }) => theme.spacing(3)};
- justify-content: center;
-`;
-
-const StyledLegendItem = styled.div`
- align-items: center;
- display: flex;
- gap: ${({ theme }) => theme.spacing(1)};
- font-size: ${({ theme }) => theme.font.size.xs};
- font-weight: ${({ theme }) => theme.font.weight.semiBold};
-`;
-
-const StyledLegendLabel = styled.span`
- color: ${({ theme }) => theme.font.color.light};
-`;
-
-const StyledLegendValue = styled.span`
- color: ${({ theme }) => theme.font.color.primary};
-`;
-
-const StyledTooltipContent = styled.div`
- display: flex;
- flex-direction: column;
- gap: ${({ theme }) => theme.spacing(2)};
-`;
-
-const StyledTooltipRow = styled.div`
- align-items: center;
- color: ${({ theme }) => theme.font.color.extraLight};
- display: flex;
- font-size: ${({ theme }) => theme.font.size.xs};
- gap: ${({ theme }) => theme.spacing(2)};
-`;
-
-const StyledDot = styled.div`
- background: ${({ theme }) => theme.color.blue};
- border-radius: 50%;
- height: 6px;
- width: 6px;
- flex-shrink: 0;
-`;
-
-const StyledTooltipValue = styled.span`
- margin-left: auto;
- white-space: nowrap;
-`;
-
-const StyledTooltipLink = styled.a`
- display: flex;
- align-items: center;
- color: ${({ theme }) => theme.font.color.light};
- cursor: pointer;
- text-decoration: none;
- &:hover {
- color: ${({ theme }) => theme.font.color.secondary};
- }
-`;
-
export const GraphWidgetGaugeChart = ({
value,
min,
max,
- unit,
showValue = true,
+ showLegend = true,
legendLabel,
tooltipHref,
id,
+ displayType,
+ decimals,
+ prefix,
+ suffix,
+ customFormatter,
}: GraphWidgetGaugeChartProps) => {
const theme = useTheme();
const [isHovered, setIsHovered] = useState(false);
- const formatValue = (val: number): string => {
- if (val % 1 !== 0) {
- return val.toFixed(1);
- }
- return val.toString();
+ const colorRegistry = createGraphColorRegistry(theme);
+ const colorScheme =
+ getColorSchemeByName(colorRegistry, 'blue') || colorRegistry.blue;
+
+ const formatOptions: GraphValueFormatOptions = {
+ displayType,
+ decimals,
+ prefix,
+ suffix,
+ customFormatter,
};
- const displayValue = formatValue(value);
+ const formattedValue = formatGraphValue(value, formatOptions);
const normalizedValue = max === min ? 0 : ((value - min) / (max - min)) * 100;
const clampedNormalizedValue = Math.max(0, Math.min(100, normalizedValue));
@@ -140,99 +96,125 @@ export const GraphWidgetGaugeChart = ({
},
];
- const gradientColors = isHovered
- ? {
- start: theme.adaptiveColors.blue4,
- end: theme.adaptiveColors.blue3,
- }
- : {
- start: theme.adaptiveColors.blue2,
- end: theme.adaptiveColors.blue1,
- };
-
const gradientId = `gaugeGradient-${id}`;
-
- const defs = [
- {
- id: gradientId,
- type: 'linearGradient',
- colors: [
- { offset: 0, color: gradientColors.start },
- { offset: 100, color: gradientColors.end },
- ],
- },
- ];
-
- const tooltipContent = (
-
-
-
- {legendLabel}
- {`${displayValue}${unit}`}
-
-
- {t`Click to see data`}
-
-
-
+ const gaugeAngle = -90 + (clampedNormalizedValue / 100) * 90;
+ const gradientDef = createGradientDef(
+ colorScheme,
+ gradientId,
+ isHovered,
+ gaugeAngle,
);
+ const defs = [gradientDef];
+
+ const handleClick = () => {
+ if (isDefined(tooltipHref)) {
+ window.location.href = tooltipHref;
+ }
+ };
+
+ const renderTooltip = () => {
+ const formattedWithPercentage = `${formattedValue} (${normalizedValue.toFixed(1)}%)`;
+
+ return (
+
+ );
+ };
+
+ 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 (
+
+
+
+ );
+ };
return (
- <>
- setIsHovered(true)}
- onMouseLeave={() => setIsHovered(false)}
- >
-
- d.x === 'value',
- id: gradientId,
- },
- ]}
- enableTracks={false}
- enableRadialGrid={false}
- enableCircularGrid={false}
- enableLabels={false}
- isInteractive={false}
- radialAxisStart={null}
- radialAxisEnd={null}
- circularAxisInner={null}
- circularAxisOuter={null}
+
+
+ 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)}
+ radialAxisStart={null}
+ radialAxisEnd={null}
+ circularAxisInner={null}
+ circularAxisOuter={null}
+ layers={['bars', renderValueEndLine]}
+ />
+ {showValue && (
+
- {showValue && (
-
- )}
-
-
-
-
- {legendLabel}
- {`${displayValue}${unit}`}
-
-
-
-
- {tooltipContent}
-
- >
+ )}
+
+
+
);
};
diff --git a/packages/twenty-front/src/modules/dashboards/graphs/components/GraphWidgetLegend.tsx b/packages/twenty-front/src/modules/dashboards/graphs/components/GraphWidgetLegend.tsx
new file mode 100644
index 0000000000..00db64c38d
--- /dev/null
+++ b/packages/twenty-front/src/modules/dashboards/graphs/components/GraphWidgetLegend.tsx
@@ -0,0 +1,66 @@
+import styled from '@emotion/styled';
+
+export type GraphWidgetLegendItem = {
+ id: string;
+ label: string;
+ formattedValue: string;
+ color: string;
+};
+
+type GraphWidgetLegendProps = {
+ items: GraphWidgetLegendItem[];
+ show?: boolean;
+};
+
+const StyledLegendContainer = styled.div`
+ display: flex;
+ flex-wrap: wrap;
+ gap: ${({ theme }) => theme.spacing(3)};
+ justify-content: center;
+ padding: ${({ theme }) => theme.spacing(2)} 0;
+`;
+
+const StyledLegendItem = styled.div`
+ align-items: center;
+ display: flex;
+ gap: ${({ theme }) => theme.spacing(1)};
+ font-size: ${({ theme }) => theme.font.size.xs};
+ font-weight: ${({ theme }) => theme.font.weight.semiBold};
+`;
+
+const StyledLegendLabel = styled.span`
+ color: ${({ theme }) => theme.font.color.light};
+`;
+
+const StyledLegendValue = styled.span`
+ color: ${({ theme }) => theme.font.color.primary};
+`;
+
+const StyledDot = styled.div<{ color: string }>`
+ background: ${({ color }) => color};
+ border-radius: 50%;
+ height: 6px;
+ width: 6px;
+ flex-shrink: 0;
+`;
+
+export const GraphWidgetLegend = ({
+ items,
+ show = true,
+}: GraphWidgetLegendProps) => {
+ if (!show || items.length === 0) {
+ return null;
+ }
+
+ return (
+
+ {items.map((item) => (
+
+
+ {item.label}
+ {item.formattedValue}
+
+ ))}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/dashboards/graphs/components/GraphWidgetPieChart.tsx b/packages/twenty-front/src/modules/dashboards/graphs/components/GraphWidgetPieChart.tsx
new file mode 100644
index 0000000000..7dcd791e7d
--- /dev/null
+++ b/packages/twenty-front/src/modules/dashboards/graphs/components/GraphWidgetPieChart.tsx
@@ -0,0 +1,207 @@
+import { useTheme } from '@emotion/react';
+import styled from '@emotion/styled';
+import {
+ ResponsivePie,
+ type ComputedDatum,
+ type DatumId,
+ type PieCustomLayerProps,
+} from '@nivo/pie';
+import { useState } from 'react';
+import { isDefined } from 'twenty-shared/utils';
+
+import { createGradientDef } from '../utils/createGradientDef';
+import { createGraphColorRegistry } from '../utils/createGraphColorRegistry';
+import { getColorSchemeByIndex } from '../utils/getColorSchemeByIndex';
+import {
+ formatGraphValue,
+ type GraphValueFormatOptions,
+} from '../utils/graphFormatters';
+import { GraphWidgetLegend } from './GraphWidgetLegend';
+import { GraphWidgetTooltip } from './GraphWidgetTooltip';
+
+type GraphWidgetPieChartProps = {
+ data: Array<{ id: string; value: number; label?: string }>;
+ showLegend?: boolean;
+ tooltipHref?: string;
+ id: string;
+} & GraphValueFormatOptions;
+
+const StyledContainer = styled.div`
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ justify-content: center;
+ width: 100%;
+`;
+
+const StyledChartContainer = styled.div`
+ flex: 1;
+ position: relative;
+ width: 100%;
+`;
+
+export const GraphWidgetPieChart = ({
+ data,
+ showLegend = true,
+ tooltipHref,
+ id,
+ displayType,
+ decimals,
+ prefix,
+ suffix,
+ customFormatter,
+}: GraphWidgetPieChartProps) => {
+ const theme = useTheme();
+ const [hoveredSliceId, setHoveredSliceId] = useState(null);
+
+ const colorRegistry = createGraphColorRegistry(theme);
+
+ const formatOptions: GraphValueFormatOptions = {
+ displayType,
+ decimals,
+ prefix,
+ suffix,
+ customFormatter,
+ };
+
+ const totalValue = data.reduce((sum, item) => sum + item.value, 0);
+
+ let cumulativeAngle = 0;
+ const enrichedData = data.map((item, index) => {
+ const colorScheme = getColorSchemeByIndex(colorRegistry, index);
+ const isHovered = hoveredSliceId === item.id;
+ const gradientId = `${colorScheme.name}Gradient-${id}-${index}`;
+ const percentage = totalValue > 0 ? (item.value / totalValue) * 100 : 0;
+
+ const sliceAngle = (percentage / 100) * 360;
+ const middleAngle = cumulativeAngle + sliceAngle / 2;
+ cumulativeAngle += sliceAngle;
+
+ return {
+ ...item,
+ gradientId,
+ colorScheme,
+ isHovered,
+ percentage,
+ middleAngle,
+ };
+ });
+
+ const defs = enrichedData.map((item) =>
+ createGradientDef(
+ item.colorScheme,
+ item.gradientId,
+ item.isHovered,
+ item.middleAngle,
+ ),
+ );
+
+ const fill = enrichedData.map((item) => ({
+ match: { id: item.id },
+ id: item.gradientId,
+ }));
+
+ const handleSliceClick = () => {
+ if (isDefined(tooltipHref)) {
+ window.location.href = tooltipHref;
+ }
+ };
+
+ const renderTooltip = (
+ datum: ComputedDatum<{ id: string; value: number; label?: string }>,
+ ) => {
+ const item = enrichedData.find((d) => d.id === datum.id);
+ if (!item) return null;
+
+ const formattedValue = formatGraphValue(item.value, formatOptions);
+ const formattedWithPercentage = `${formattedValue} (${item.percentage.toFixed(1)}%)`;
+
+ return (
+
+ );
+ };
+
+ const renderSliceEndLines = (
+ layerProps: PieCustomLayerProps<{
+ id: string;
+ value: number;
+ label?: string;
+ }>,
+ ) => {
+ const { dataWithArc, centerX, centerY, innerRadius, radius } = layerProps;
+
+ if (!dataWithArc || !Array.isArray(dataWithArc) || dataWithArc.length < 2) {
+ return null;
+ }
+
+ return (
+
+ {dataWithArc.map((datum) => {
+ const enrichedItem = enrichedData.find((d) => d.id === datum.id);
+ const lineColor = enrichedItem
+ ? enrichedItem.colorScheme.solid
+ : theme.border.color.strong;
+
+ const angle = datum.arc.endAngle - Math.PI / 2;
+ const x1 = centerX + Math.cos(angle) * innerRadius;
+ const y1 = centerY + Math.sin(angle) * innerRadius;
+ const x2 = centerX + Math.cos(angle) * radius;
+ const y2 = centerY + Math.sin(angle) * radius;
+
+ return (
+
+ );
+ })}
+
+ );
+ };
+
+ return (
+
+
+ `url(#${item.gradientId})`)}
+ borderWidth={0}
+ enableArcLinkLabels={false}
+ enableArcLabels={false}
+ tooltip={({ datum }) => renderTooltip(datum)}
+ onClick={handleSliceClick}
+ onMouseEnter={(datum) => setHoveredSliceId(datum.id)}
+ onMouseLeave={() => setHoveredSliceId(null)}
+ defs={defs}
+ fill={fill}
+ layers={['arcs', renderSliceEndLines]}
+ />
+
+ ({
+ id: item.id,
+ label: item.label || item.id,
+ formattedValue: formatGraphValue(item.value, formatOptions),
+ color: item.colorScheme.solid,
+ }))}
+ />
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/dashboards/graphs/components/GraphWidgetTooltip.tsx b/packages/twenty-front/src/modules/dashboards/graphs/components/GraphWidgetTooltip.tsx
new file mode 100644
index 0000000000..c2bb2ffd85
--- /dev/null
+++ b/packages/twenty-front/src/modules/dashboards/graphs/components/GraphWidgetTooltip.tsx
@@ -0,0 +1,80 @@
+import { useTheme } from '@emotion/react';
+import styled from '@emotion/styled';
+import { t } from '@lingui/core/macro';
+import { IconArrowUpRight } from 'twenty-ui/display';
+
+const StyledTooltipContent = styled.div`
+ background: ${({ theme }) => theme.background.primary};
+ border: 1px solid ${({ theme }) => theme.border.color.medium};
+ border-radius: ${({ theme }) => theme.border.radius.sm};
+ box-shadow: ${({ theme }) => theme.boxShadow.strong};
+ display: flex;
+ flex-direction: column;
+ gap: ${({ theme }) => theme.spacing(2)};
+ padding: ${({ theme }) => theme.spacing(2)};
+ pointer-events: none;
+`;
+
+const StyledTooltipRow = styled.div`
+ align-items: center;
+ color: ${({ theme }) => theme.font.color.extraLight};
+ display: flex;
+ font-size: ${({ theme }) => theme.font.size.xs};
+ gap: ${({ theme }) => theme.spacing(2)};
+`;
+
+const StyledDot = styled.div<{ $color: string }>`
+ background: ${({ $color }) => $color};
+ border-radius: 50%;
+ height: 6px;
+ width: 6px;
+ flex-shrink: 0;
+`;
+
+const StyledTooltipValue = styled.span`
+ margin-left: auto;
+ white-space: nowrap;
+`;
+
+const StyledTooltipLink = styled.div`
+ align-items: center;
+ color: ${({ theme }) => theme.font.color.light};
+ cursor: default;
+ display: flex;
+`;
+
+export type GraphWidgetTooltipItem = {
+ label: string;
+ formattedValue: string;
+ dotColor: string;
+};
+
+type GraphWidgetTooltipProps = {
+ items: GraphWidgetTooltipItem[];
+ showClickHint?: boolean;
+};
+
+export const GraphWidgetTooltip = ({
+ items,
+ showClickHint = false,
+}: GraphWidgetTooltipProps) => {
+ const theme = useTheme();
+
+ return (
+
+ {items.map((item, index) => (
+
+
+ {item.label}
+ {item.formattedValue}
+
+ ))}
+ {showClickHint && (
+
+ {t`Click to see data`}
+
+
+ )}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/dashboards/graphs/components/__stories__/GraphWidgetGaugeChart.stories.tsx b/packages/twenty-front/src/modules/dashboards/graphs/components/__stories__/GraphWidgetGaugeChart.stories.tsx
index a307c69fd5..cd89c49302 100644
--- a/packages/twenty-front/src/modules/dashboards/graphs/components/__stories__/GraphWidgetGaugeChart.stories.tsx
+++ b/packages/twenty-front/src/modules/dashboards/graphs/components/__stories__/GraphWidgetGaugeChart.stories.tsx
@@ -20,12 +20,25 @@ const meta: Meta = {
max: {
control: { type: 'number' },
},
- unit: {
+ displayType: {
+ control: 'select',
+ options: ['percentage', 'number', 'shortNumber', 'currency', 'custom'],
+ },
+ prefix: {
control: 'text',
},
+ suffix: {
+ control: 'text',
+ },
+ decimals: {
+ control: 'number',
+ },
showValue: {
control: 'boolean',
},
+ showLegend: {
+ control: 'boolean',
+ },
legendLabel: {
control: 'text',
},
@@ -47,10 +60,10 @@ const Container = ({ children }: { children: React.ReactNode }) => (
export const Default: Story = {
args: {
- value: 50,
+ value: 0.5,
min: 0,
- max: 100,
- unit: '%',
+ max: 1,
+ displayType: 'percentage',
showValue: true,
legendLabel: 'Conversion rate',
tooltipHref: 'https://example.com',
@@ -62,8 +75,12 @@ export const Default: Story = {
value={args.value}
min={args.min}
max={args.max}
- unit={args.unit}
+ displayType={args.displayType}
+ decimals={args.decimals}
+ prefix={args.prefix}
+ suffix={args.suffix}
showValue={args.showValue}
+ showLegend={args.showLegend}
legendLabel={args.legendLabel}
tooltipHref={args.tooltipHref}
id={args.id}
@@ -81,10 +98,10 @@ export const Catalog: Story = {
name: 'value',
values: [0, 25, 50, 75, 100],
props: (value: number) => ({
- value,
+ value: value / 100,
min: 0,
- max: 100,
- unit: '%',
+ max: 1,
+ displayType: 'percentage' as const,
id: `gauge-chart-catalog-${value}`,
}),
labels: (value: number) => {
@@ -107,11 +124,11 @@ export const Catalog: Story = {
value={args.value}
min={args.min}
max={args.max}
- unit={args.unit}
+ displayType={args.displayType}
showValue={true}
legendLabel="Percentage"
tooltipHref="https://example.com"
- id={args.id}
+ id="gauge-chart-catalog"
/>
),
@@ -122,7 +139,7 @@ export const WithoutValue: Story = {
value: 65,
min: 0,
max: 100,
- unit: '%',
+ displayType: 'percentage',
showValue: false,
legendLabel: 'Conversion rate',
tooltipHref: 'https://example.com',
@@ -134,8 +151,12 @@ export const WithoutValue: Story = {
value={args.value}
min={args.min}
max={args.max}
- unit={args.unit}
+ displayType={args.displayType}
+ decimals={args.decimals}
+ prefix={args.prefix}
+ suffix={args.suffix}
showValue={args.showValue}
+ showLegend={args.showLegend}
legendLabel={args.legendLabel}
tooltipHref={args.tooltipHref}
id={args.id}
@@ -149,7 +170,7 @@ export const Revenue: Story = {
value: 750,
min: 0,
max: 1000,
- unit: 'K',
+ displayType: 'shortNumber',
showValue: true,
legendLabel: 'Revenue',
tooltipHref: 'https://example.com',
@@ -161,8 +182,12 @@ export const Revenue: Story = {
value={args.value}
min={args.min}
max={args.max}
- unit={args.unit}
+ displayType={args.displayType}
+ decimals={args.decimals}
+ prefix={args.prefix}
+ suffix={args.suffix}
showValue={args.showValue}
+ showLegend={args.showLegend}
legendLabel={args.legendLabel}
tooltipHref={args.tooltipHref}
id={args.id}
@@ -176,7 +201,7 @@ export const Temperature: Story = {
value: 22,
min: -10,
max: 40,
- unit: '°C',
+ suffix: '°C',
showValue: true,
legendLabel: 'Temperature',
tooltipHref: 'https://example.com',
@@ -188,8 +213,12 @@ export const Temperature: Story = {
value={args.value}
min={args.min}
max={args.max}
- unit={args.unit}
+ displayType={args.displayType}
+ decimals={args.decimals}
+ prefix={args.prefix}
+ suffix={args.suffix}
showValue={args.showValue}
+ showLegend={args.showLegend}
legendLabel={args.legendLabel}
tooltipHref={args.tooltipHref}
id={args.id}
@@ -203,7 +232,7 @@ export const Storage: Story = {
value: 384,
min: 0,
max: 512,
- unit: 'GB',
+ suffix: ' GB',
showValue: true,
legendLabel: 'Storage Used',
tooltipHref: 'https://example.com',
@@ -215,8 +244,12 @@ export const Storage: Story = {
value={args.value}
min={args.min}
max={args.max}
- unit={args.unit}
+ displayType={args.displayType}
+ decimals={args.decimals}
+ prefix={args.prefix}
+ suffix={args.suffix}
showValue={args.showValue}
+ showLegend={args.showLegend}
legendLabel={args.legendLabel}
tooltipHref={args.tooltipHref}
id={args.id}
@@ -230,7 +263,8 @@ export const Rating: Story = {
value: 4.2,
min: 0,
max: 5,
- unit: ' ⭐',
+ suffix: ' ⭐',
+ decimals: 1,
showValue: true,
legendLabel: 'Average Rating',
tooltipHref: 'https://example.com',
@@ -242,8 +276,43 @@ export const Rating: Story = {
value={args.value}
min={args.min}
max={args.max}
- unit={args.unit}
+ displayType={args.displayType}
+ decimals={args.decimals}
+ prefix={args.prefix}
+ suffix={args.suffix}
showValue={args.showValue}
+ showLegend={args.showLegend}
+ legendLabel={args.legendLabel}
+ tooltipHref={args.tooltipHref}
+ id={args.id}
+ />
+
+ ),
+};
+
+export const WithoutLegend: Story = {
+ args: {
+ value: 65,
+ min: 0,
+ max: 100,
+ displayType: 'percentage',
+ showValue: true,
+ legendLabel: 'Conversion rate',
+ tooltipHref: 'https://example.com',
+ id: 'gauge-chart-without-legend',
+ },
+ render: (args) => (
+
+ = {
+ title: 'Modules/Dashboards/Graphs/GraphWidgetLegend',
+ component: GraphWidgetLegend,
+ decorators: [ComponentDecorator],
+ parameters: {
+ layout: 'centered',
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {
+ render: () => {
+ return (
+
+ );
+ },
+};
+
+export const SingleItem: Story = {
+ render: () => {
+ return (
+
+ );
+ },
+};
diff --git a/packages/twenty-front/src/modules/dashboards/graphs/components/__stories__/GraphWidgetPieChart.stories.tsx b/packages/twenty-front/src/modules/dashboards/graphs/components/__stories__/GraphWidgetPieChart.stories.tsx
new file mode 100644
index 0000000000..2b476d80f3
--- /dev/null
+++ b/packages/twenty-front/src/modules/dashboards/graphs/components/__stories__/GraphWidgetPieChart.stories.tsx
@@ -0,0 +1,332 @@
+import { type Meta, type StoryObj } from '@storybook/react';
+
+import { CatalogDecorator, ComponentDecorator } from 'twenty-ui/testing';
+import { GraphWidgetPieChart } from '../GraphWidgetPieChart';
+
+const meta: Meta = {
+ title: 'Modules/Dashboards/Graphs/GraphWidgetPieChart',
+ component: GraphWidgetPieChart,
+ decorators: [ComponentDecorator],
+ parameters: {
+ layout: 'centered',
+ },
+ argTypes: {
+ data: {
+ control: 'object',
+ },
+ displayType: {
+ control: 'select',
+ options: ['percentage', 'number', 'shortNumber', 'currency', 'custom'],
+ },
+ prefix: {
+ control: 'text',
+ },
+ suffix: {
+ control: 'text',
+ },
+ decimals: {
+ control: 'number',
+ },
+ showLegend: {
+ control: 'boolean',
+ },
+ tooltipHref: {
+ control: 'text',
+ },
+ id: {
+ control: 'text',
+ },
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+const Container = ({ children }: { children: React.ReactNode }) => (
+ {children}
+);
+
+export const Default: Story = {
+ args: {
+ data: [
+ { id: 'qualified', value: 35, label: 'Qualified' },
+ { id: 'contacted', value: 25, label: 'Contacted' },
+ { id: 'unqualified', value: 20, label: 'Unqualified' },
+ { id: 'proposal', value: 15, label: 'Proposal' },
+ { id: 'negotiation', value: 5, label: 'Negotiation' },
+ ],
+ showLegend: true,
+ tooltipHref: 'https://example.com/leads',
+ id: 'pie-chart-default',
+ },
+ render: (args) => (
+
+
+
+ ),
+};
+
+export const Revenue: Story = {
+ args: {
+ data: [
+ { id: 'product-a', value: 420000, label: 'Product A' },
+ { id: 'product-b', value: 380000, label: 'Product B' },
+ { id: 'product-c', value: 250000, label: 'Product C' },
+ { id: 'product-d', value: 180000, label: 'Product D' },
+ ],
+ prefix: '$',
+ displayType: 'shortNumber',
+ showLegend: true,
+ tooltipHref: 'https://example.com/revenue',
+ id: 'pie-chart-revenue',
+ },
+ render: (args) => (
+
+
+
+ ),
+};
+
+export const TaskStatus: Story = {
+ args: {
+ data: [
+ { id: 'completed', value: 45, label: 'Completed' },
+ { id: 'in-progress', value: 30, label: 'In Progress' },
+ { id: 'todo', value: 25, label: 'To Do' },
+ ],
+ displayType: 'percentage',
+ showLegend: true,
+ tooltipHref: 'https://example.com/tasks',
+ id: 'pie-chart-task-status',
+ },
+ render: (args) => (
+
+
+
+ ),
+};
+
+export const TwoSlices: Story = {
+ args: {
+ data: [
+ { id: 'active', value: 75, label: 'Active' },
+ { id: 'inactive', value: 25, label: 'Inactive' },
+ ],
+ displayType: 'percentage',
+ showLegend: true,
+ tooltipHref: 'https://example.com/status',
+ id: 'pie-chart-two-slices',
+ },
+ render: (args) => (
+
+
+
+ ),
+};
+
+export const ManySlices: Story = {
+ args: {
+ data: [
+ { id: 'category-1', value: 20, label: 'Category 1' },
+ { id: 'category-2', value: 18, label: 'Category 2' },
+ { id: 'category-3', value: 16, label: 'Category 3' },
+ { id: 'category-4', value: 14, label: 'Category 4' },
+ { id: 'category-5', value: 12, label: 'Category 5' },
+ { id: 'category-6', value: 10, label: 'Category 6' },
+ { id: 'category-7', value: 6, label: 'Category 7' },
+ { id: 'category-8', value: 4, label: 'Category 8' },
+ ],
+ showLegend: true,
+ tooltipHref: 'https://example.com/categories',
+ id: 'pie-chart-many-slices',
+ },
+ render: (args) => (
+
+
+
+ ),
+};
+
+export const WithoutLegend: Story = {
+ args: {
+ data: [
+ { id: 'web', value: 45, label: 'Web' },
+ { id: 'mobile', value: 35, label: 'Mobile' },
+ { id: 'desktop', value: 20, label: 'Desktop' },
+ ],
+ displayType: 'percentage',
+ showLegend: false,
+ tooltipHref: 'https://example.com/platforms',
+ id: 'pie-chart-without-legend',
+ },
+ render: (args) => (
+
+
+
+ ),
+};
+
+export const MarketShare: Story = {
+ args: {
+ data: [
+ { id: 'brand-a', value: 35.5, label: 'Brand A' },
+ { id: 'brand-b', value: 28.2, label: 'Brand B' },
+ { id: 'brand-c', value: 18.7, label: 'Brand C' },
+ { id: 'others', value: 17.6, label: 'Others' },
+ ],
+ displayType: 'percentage',
+ showLegend: true,
+ tooltipHref: 'https://example.com/market',
+ id: 'pie-chart-market-share',
+ },
+ render: (args) => (
+
+
+
+ ),
+};
+
+export const Storage: Story = {
+ args: {
+ data: [
+ { id: 'documents', value: 125, label: 'Documents' },
+ { id: 'media', value: 280, label: 'Media' },
+ { id: 'applications', value: 95, label: 'Applications' },
+ { id: 'system', value: 50, label: 'System' },
+ ],
+ suffix: ' GB',
+ showLegend: true,
+ tooltipHref: 'https://example.com/storage',
+ id: 'pie-chart-storage',
+ },
+ render: (args) => (
+
+
+
+ ),
+};
+
+export const Catalog: Story = {
+ decorators: [CatalogDecorator],
+ parameters: {
+ catalog: {
+ dimensions: [
+ {
+ name: 'slices',
+ values: [2, 3, 5],
+ props: (sliceCount: number) => {
+ const dataMap: Record<
+ number,
+ Array<{ id: string; value: number; label?: string }>
+ > = {
+ 2: [
+ { id: 'yes', value: 65, label: 'Yes' },
+ { id: 'no', value: 35, label: 'No' },
+ ],
+ 3: [
+ { id: 'gold', value: 45, label: 'Gold' },
+ { id: 'silver', value: 35, label: 'Silver' },
+ { id: 'bronze', value: 20, label: 'Bronze' },
+ ],
+ 5: [
+ { id: 'item-1', value: 30, label: 'Item 1' },
+ { id: 'item-2', value: 25, label: 'Item 2' },
+ { id: 'item-3', value: 20, label: 'Item 3' },
+ { id: 'item-4', value: 15, label: 'Item 4' },
+ { id: 'item-5', value: 10, label: 'Item 5' },
+ ],
+ };
+
+ return {
+ data: dataMap[sliceCount] || dataMap[3],
+ id: `pie-chart-catalog-${sliceCount}`,
+ };
+ },
+ labels: (sliceCount: number) => `${sliceCount} slices`,
+ },
+ ],
+ },
+ },
+ render: (args) => (
+
+
+
+ ),
+};
diff --git a/packages/twenty-front/src/modules/dashboards/graphs/components/__stories__/GraphWidgetTooltip.stories.tsx b/packages/twenty-front/src/modules/dashboards/graphs/components/__stories__/GraphWidgetTooltip.stories.tsx
new file mode 100644
index 0000000000..1162255621
--- /dev/null
+++ b/packages/twenty-front/src/modules/dashboards/graphs/components/__stories__/GraphWidgetTooltip.stories.tsx
@@ -0,0 +1,65 @@
+import { type Meta, type StoryObj } from '@storybook/react';
+
+import { ComponentDecorator } from 'twenty-ui/testing';
+import { GraphWidgetTooltip } from '../GraphWidgetTooltip';
+
+const meta: Meta = {
+ title: 'Modules/Dashboards/Graphs/GraphWidgetTooltip',
+ component: GraphWidgetTooltip,
+ decorators: [ComponentDecorator],
+ parameters: {
+ layout: 'centered',
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ items: [
+ {
+ label: 'Revenue',
+ formattedValue: '$45,231',
+ dotColor: 'blue',
+ },
+ ],
+ showClickHint: false,
+ },
+};
+
+export const WithClickHint: Story = {
+ args: {
+ items: [
+ {
+ label: 'Sales',
+ formattedValue: '1,234 units',
+ dotColor: 'green',
+ },
+ ],
+ showClickHint: true,
+ },
+};
+
+export const MultipleItems: Story = {
+ args: {
+ items: [
+ {
+ label: 'Q1',
+ formattedValue: '$12,345',
+ dotColor: 'blue',
+ },
+ {
+ label: 'Q2',
+ formattedValue: '$23,456',
+ dotColor: 'green',
+ },
+ {
+ label: 'Q3',
+ formattedValue: '$34,567',
+ dotColor: 'red',
+ },
+ ],
+ showClickHint: false,
+ },
+};
diff --git a/packages/twenty-front/src/modules/dashboards/graphs/types/GraphColorRegistry.ts b/packages/twenty-front/src/modules/dashboards/graphs/types/GraphColorRegistry.ts
new file mode 100644
index 0000000000..afd246da79
--- /dev/null
+++ b/packages/twenty-front/src/modules/dashboards/graphs/types/GraphColorRegistry.ts
@@ -0,0 +1,5 @@
+import { type GraphColorScheme } from './GraphColorScheme';
+
+export type GraphColorRegistry = {
+ [key: string]: GraphColorScheme;
+};
diff --git a/packages/twenty-front/src/modules/dashboards/graphs/types/GraphColorScheme.ts b/packages/twenty-front/src/modules/dashboards/graphs/types/GraphColorScheme.ts
new file mode 100644
index 0000000000..b8adee8b5d
--- /dev/null
+++ b/packages/twenty-front/src/modules/dashboards/graphs/types/GraphColorScheme.ts
@@ -0,0 +1,8 @@
+export type GraphColorScheme = {
+ name: string;
+ gradient: {
+ normal: [string, string];
+ hover: [string, string];
+ };
+ solid: string;
+};
diff --git a/packages/twenty-front/src/modules/dashboards/graphs/utils/calculateAngularGradient.ts b/packages/twenty-front/src/modules/dashboards/graphs/utils/calculateAngularGradient.ts
new file mode 100644
index 0000000000..60e1fd995e
--- /dev/null
+++ b/packages/twenty-front/src/modules/dashboards/graphs/utils/calculateAngularGradient.ts
@@ -0,0 +1,13 @@
+export const calculateAngularGradient = (angle: number) => {
+ const gradientAngle = (angle * Math.PI) / 180 + Math.PI / 2;
+
+ const dx = Math.sin(gradientAngle);
+ const dy = -Math.cos(gradientAngle);
+
+ return {
+ x1: `${50 - dx * 50}%`,
+ y1: `${50 - dy * 50}%`,
+ x2: `${50 + dx * 50}%`,
+ y2: `${50 + dy * 50}%`,
+ };
+};
diff --git a/packages/twenty-front/src/modules/dashboards/graphs/utils/createGradientDef.ts b/packages/twenty-front/src/modules/dashboards/graphs/utils/createGradientDef.ts
new file mode 100644
index 0000000000..ad323ec0c5
--- /dev/null
+++ b/packages/twenty-front/src/modules/dashboards/graphs/utils/createGradientDef.ts
@@ -0,0 +1,28 @@
+import { type GraphColorScheme } from '../types/GraphColorScheme';
+import { calculateAngularGradient } from './calculateAngularGradient';
+
+export const createGradientDef = (
+ colorScheme: GraphColorScheme,
+ id: string,
+ isHovered: boolean = false,
+ angle?: number,
+) => {
+ const colors = isHovered
+ ? colorScheme.gradient.hover
+ : colorScheme.gradient.normal;
+
+ const coords =
+ angle !== undefined
+ ? calculateAngularGradient(angle)
+ : { x1: '0%', y1: '0%', x2: '0%', y2: '100%' };
+
+ return {
+ id,
+ type: 'linearGradient' as const,
+ ...coords,
+ colors: [
+ { offset: 0, color: colors[0] },
+ { offset: 100, color: colors[1] },
+ ],
+ };
+};
diff --git a/packages/twenty-front/src/modules/dashboards/graphs/utils/createGraphColorRegistry.ts b/packages/twenty-front/src/modules/dashboards/graphs/utils/createGraphColorRegistry.ts
new file mode 100644
index 0000000000..565c4310b4
--- /dev/null
+++ b/packages/twenty-front/src/modules/dashboards/graphs/utils/createGraphColorRegistry.ts
@@ -0,0 +1,51 @@
+import { type ThemeType } from 'twenty-ui/theme';
+
+import { type GraphColorRegistry } from '../types/GraphColorRegistry';
+
+export const createGraphColorRegistry = (
+ theme: ThemeType,
+): GraphColorRegistry => ({
+ blue: {
+ name: 'blue',
+ gradient: {
+ normal: [theme.adaptiveColors.blue1, theme.adaptiveColors.blue2],
+ hover: [theme.adaptiveColors.blue3, theme.adaptiveColors.blue4],
+ },
+ solid: theme.color.blue,
+ },
+ purple: {
+ name: 'purple',
+ gradient: {
+ normal: [theme.adaptiveColors.purple1, theme.adaptiveColors.purple2],
+ hover: [theme.adaptiveColors.purple3, theme.adaptiveColors.purple4],
+ },
+ solid: theme.color.purple,
+ },
+ turquoise: {
+ name: 'turquoise',
+ gradient: {
+ normal: [
+ theme.adaptiveColors.turquoise1,
+ theme.adaptiveColors.turquoise2,
+ ],
+ hover: [theme.adaptiveColors.turquoise3, theme.adaptiveColors.turquoise4],
+ },
+ solid: theme.color.turquoise,
+ },
+ orange: {
+ name: 'orange',
+ gradient: {
+ normal: [theme.adaptiveColors.orange1, theme.adaptiveColors.orange2],
+ hover: [theme.adaptiveColors.orange3, theme.adaptiveColors.orange4],
+ },
+ solid: theme.color.orange,
+ },
+ pink: {
+ name: 'pink',
+ gradient: {
+ normal: [theme.adaptiveColors.pink1, theme.adaptiveColors.pink2],
+ hover: [theme.adaptiveColors.pink3, theme.adaptiveColors.pink4],
+ },
+ solid: theme.color.pink,
+ },
+});
diff --git a/packages/twenty-front/src/modules/dashboards/graphs/utils/getColorSchemeByIndex.ts b/packages/twenty-front/src/modules/dashboards/graphs/utils/getColorSchemeByIndex.ts
new file mode 100644
index 0000000000..378aa27458
--- /dev/null
+++ b/packages/twenty-front/src/modules/dashboards/graphs/utils/getColorSchemeByIndex.ts
@@ -0,0 +1,10 @@
+import { type GraphColorRegistry } from '../types/GraphColorRegistry';
+import { type GraphColorScheme } from '../types/GraphColorScheme';
+
+export const getColorSchemeByIndex = (
+ registry: GraphColorRegistry,
+ index: number,
+): GraphColorScheme => {
+ const schemes = Object.values(registry);
+ return schemes[index % schemes.length];
+};
diff --git a/packages/twenty-front/src/modules/dashboards/graphs/utils/getColorSchemeByName.ts b/packages/twenty-front/src/modules/dashboards/graphs/utils/getColorSchemeByName.ts
new file mode 100644
index 0000000000..aed17adf46
--- /dev/null
+++ b/packages/twenty-front/src/modules/dashboards/graphs/utils/getColorSchemeByName.ts
@@ -0,0 +1,9 @@
+import { type GraphColorRegistry } from '../types/GraphColorRegistry';
+import { type GraphColorScheme } from '../types/GraphColorScheme';
+
+export const getColorSchemeByName = (
+ registry: GraphColorRegistry,
+ name: string,
+): GraphColorScheme | undefined => {
+ return registry[name];
+};
diff --git a/packages/twenty-front/src/modules/dashboards/graphs/utils/graphFormatters.ts b/packages/twenty-front/src/modules/dashboards/graphs/utils/graphFormatters.ts
new file mode 100644
index 0000000000..91fb55b81e
--- /dev/null
+++ b/packages/twenty-front/src/modules/dashboards/graphs/utils/graphFormatters.ts
@@ -0,0 +1,45 @@
+import { isDefined } from 'twenty-shared/utils';
+import { formatAmount } from '~/utils/format/formatAmount';
+import { formatNumber } from '~/utils/format/number';
+
+export type GraphValueFormatOptions = {
+ displayType?: 'percentage' | 'number' | 'shortNumber' | 'currency' | 'custom';
+ decimals?: number;
+ prefix?: string;
+ suffix?: string;
+ customFormatter?: (value: number) => string;
+};
+
+export const formatGraphValue = (
+ value: number,
+ options?: GraphValueFormatOptions,
+): string => {
+ const {
+ displayType = 'number',
+ decimals,
+ prefix = '',
+ suffix = '',
+ customFormatter,
+ } = options || {};
+
+ if (displayType === 'custom' && isDefined(customFormatter)) {
+ return customFormatter(value);
+ }
+
+ switch (displayType) {
+ case 'percentage':
+ return `${formatNumber(value * 100, decimals)}%`;
+
+ case 'shortNumber':
+ return `${prefix}${formatAmount(value)}${suffix}`;
+
+ case 'currency': {
+ const currencyPrefix = prefix || '$';
+ return `${currencyPrefix}${formatNumber(value, decimals)}${suffix}`;
+ }
+
+ case 'number':
+ default:
+ return `${prefix}${formatNumber(value, decimals)}${suffix}`;
+ }
+};
diff --git a/packages/twenty-website/package.json b/packages/twenty-website/package.json
index 615ed91c5b..0c7a0c3454 100644
--- a/packages/twenty-website/package.json
+++ b/packages/twenty-website/package.json
@@ -18,7 +18,7 @@
"@keystatic/core": "^0.5.45",
"@keystatic/next": "^5.0.3",
"@markdoc/markdoc": "^0.5.1",
- "@nivo/calendar": "^0.87.0",
+ "@nivo/calendar": "^0.99.0",
"date-fns": "^2.30.0",
"drizzle-kit": "^0.20.14",
"facepaint": "^1.2.1",
diff --git a/yarn.lock b/yarn.lock
index 4f0dfca35c..9c6ed657c5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9718,13 +9718,15 @@ __metadata:
languageName: node
linkType: hard
-"@nivo/calendar@npm:^0.87.0":
- version: 0.87.0
- resolution: "@nivo/calendar@npm:0.87.0"
+"@nivo/calendar@npm:^0.99.0":
+ version: 0.99.0
+ resolution: "@nivo/calendar@npm:0.99.0"
dependencies:
- "@nivo/core": "npm:0.87.0"
- "@nivo/legends": "npm:0.87.0"
- "@nivo/tooltip": "npm:0.87.0"
+ "@nivo/core": "npm:0.99.0"
+ "@nivo/legends": "npm:0.99.0"
+ "@nivo/text": "npm:0.99.0"
+ "@nivo/theming": "npm:0.99.0"
+ "@nivo/tooltip": "npm:0.99.0"
"@types/d3-scale": "npm:^4.0.8"
"@types/d3-time": "npm:^1.0.10"
"@types/d3-time-format": "npm:^3.0.0"
@@ -9733,28 +9735,8 @@ __metadata:
d3-time-format: "npm:^3.0.0"
lodash: "npm:^4.17.21"
peerDependencies:
- react: ">= 16.14.0 < 19.0.0"
- checksum: 10c0/5cb0f4ceb45109695f1b86e4215ef464b7c33449c50d45a436f71a30029bcfca6b91956d6be9862d17cd0d8bf4dd80e2ce281cb85ace83ee07159a0e9f603242
- languageName: node
- linkType: hard
-
-"@nivo/colors@npm:0.87.0":
- version: 0.87.0
- resolution: "@nivo/colors@npm:0.87.0"
- dependencies:
- "@nivo/core": "npm:0.87.0"
- "@types/d3-color": "npm:^3.0.0"
- "@types/d3-scale": "npm:^4.0.8"
- "@types/d3-scale-chromatic": "npm:^3.0.0"
- "@types/prop-types": "npm:^15.7.2"
- d3-color: "npm:^3.1.0"
- d3-scale: "npm:^4.0.2"
- d3-scale-chromatic: "npm:^3.0.0"
- lodash: "npm:^4.17.21"
- prop-types: "npm:^15.7.2"
- peerDependencies:
- react: ">= 16.14.0 < 19.0.0"
- checksum: 10c0/872f4e2d8392f89633531250e0474a365e0456dff146d2e8ba8576226effd1eda7e721ce1dd15a792b05d06caa28a11510a5ca8e55fb84aedb8ba3d964f357f5
+ react: ^16.14 || ^17.0 || ^18.0 || ^19.0
+ checksum: 10c0/b647566eb890e7fc839bc36de99c5426f62c11b35841fcb54afca167c45150ea159ef4a7a1706c3b1ca2ca12da99d6b8b26fe5499656d037deea92e35a4cfa9a
languageName: node
linkType: hard
@@ -9777,28 +9759,6 @@ __metadata:
languageName: node
linkType: hard
-"@nivo/core@npm:0.87.0":
- version: 0.87.0
- resolution: "@nivo/core@npm:0.87.0"
- dependencies:
- "@nivo/tooltip": "npm:0.87.0"
- "@react-spring/web": "npm:9.4.5 || ^9.7.2"
- "@types/d3-shape": "npm:^3.1.6"
- d3-color: "npm:^3.1.0"
- d3-format: "npm:^1.4.4"
- d3-interpolate: "npm:^3.0.1"
- d3-scale: "npm:^4.0.2"
- d3-scale-chromatic: "npm:^3.0.0"
- d3-shape: "npm:^3.2.0"
- d3-time-format: "npm:^3.0.0"
- lodash: "npm:^4.17.21"
- prop-types: "npm:^15.7.2"
- peerDependencies:
- react: ">= 16.14.0 < 19.0.0"
- checksum: 10c0/75bb5e48cf57c8e31fbd9b9f33121fb4bffb98d47a967e5135527f8fde51537b68da3d894dc7231bf3c969523985eac6a5fc6d50827b958d51e6907a6391ed84
- languageName: node
- linkType: hard
-
"@nivo/core@npm:0.99.0, @nivo/core@npm:^0.99.0":
version: 0.99.0
resolution: "@nivo/core@npm:0.99.0"
@@ -9823,20 +9783,6 @@ __metadata:
languageName: node
linkType: hard
-"@nivo/legends@npm:0.87.0":
- version: 0.87.0
- resolution: "@nivo/legends@npm:0.87.0"
- dependencies:
- "@nivo/colors": "npm:0.87.0"
- "@nivo/core": "npm:0.87.0"
- "@types/d3-scale": "npm:^4.0.8"
- d3-scale: "npm:^4.0.2"
- peerDependencies:
- react: ">= 16.14.0 < 19.0.0"
- checksum: 10c0/1501bf698cefa2695d1124c38da5fc7712a5ce9911683c6694cbea1a481b9daef8ca7b0fc49eb85530c50347c571ea5b686caff3d6a81174b66bc38318bd317d
- languageName: node
- linkType: hard
-
"@nivo/legends@npm:0.99.0":
version: 0.99.0
resolution: "@nivo/legends@npm:0.99.0"
@@ -9875,6 +9821,24 @@ __metadata:
languageName: node
linkType: hard
+"@nivo/pie@npm:^0.99.0":
+ version: 0.99.0
+ resolution: "@nivo/pie@npm:0.99.0"
+ dependencies:
+ "@nivo/arcs": "npm:0.99.0"
+ "@nivo/colors": "npm:0.99.0"
+ "@nivo/core": "npm:0.99.0"
+ "@nivo/legends": "npm:0.99.0"
+ "@nivo/theming": "npm:0.99.0"
+ "@nivo/tooltip": "npm:0.99.0"
+ "@types/d3-shape": "npm:^3.1.6"
+ d3-shape: "npm:^3.2.0"
+ peerDependencies:
+ react: ^16.14 || ^17.0 || ^18.0 || ^19.0
+ checksum: 10c0/402a791f101337c5c1254ad68c46efeee41956372e75dcf93f2cf9b4e79c03f75a464c817782c65741000ddcf2c7ca6423dcce646072991a97b4e46d344520ec
+ languageName: node
+ linkType: hard
+
"@nivo/polar-axes@npm:0.99.0":
version: 0.99.0
resolution: "@nivo/polar-axes@npm:0.99.0"
@@ -9955,18 +9919,6 @@ __metadata:
languageName: node
linkType: hard
-"@nivo/tooltip@npm:0.87.0":
- version: 0.87.0
- resolution: "@nivo/tooltip@npm:0.87.0"
- dependencies:
- "@nivo/core": "npm:0.87.0"
- "@react-spring/web": "npm:9.4.5 || ^9.7.2"
- peerDependencies:
- react: ">= 16.14.0 < 19.0.0"
- checksum: 10c0/ac6b1b0bb0a09017c0e5055432e4c5ee771301615db3ee3d34abb55c900f765af78830eb2b18c8d94ff49ddeaa19895e907c793fb431943ade11cc2d7369b7e4
- languageName: node
- linkType: hard
-
"@nivo/tooltip@npm:0.99.0":
version: 0.99.0
resolution: "@nivo/tooltip@npm:0.99.0"
@@ -15249,18 +15201,6 @@ __metadata:
languageName: node
linkType: hard
-"@react-spring/animated@npm:~9.7.4":
- version: 9.7.4
- resolution: "@react-spring/animated@npm:9.7.4"
- dependencies:
- "@react-spring/shared": "npm:~9.7.4"
- "@react-spring/types": "npm:~9.7.4"
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- checksum: 10c0/7620fe7f058ca81d321710dd391062eda8bd298d2abdf63c5e230348aecd1b80730e5b96bdf052807838a20c8961efe1f9e2840e6cce3cacae5b74a28622eff8
- languageName: node
- linkType: hard
-
"@react-spring/core@npm:9.4.5 || ^9.7.2 || ^10.0, @react-spring/core@npm:~10.0.1":
version: 10.0.1
resolution: "@react-spring/core@npm:10.0.1"
@@ -15274,19 +15214,6 @@ __metadata:
languageName: node
linkType: hard
-"@react-spring/core@npm:~9.7.4":
- version: 9.7.4
- resolution: "@react-spring/core@npm:9.7.4"
- dependencies:
- "@react-spring/animated": "npm:~9.7.4"
- "@react-spring/shared": "npm:~9.7.4"
- "@react-spring/types": "npm:~9.7.4"
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- checksum: 10c0/cdbc5e222edc5f25746cad36086acf20563157543557a3436b55bf0527460f68e5eb98e2927e186937eba37abe3067c227b685c07b9dc393f987a8ff7abe2455
- languageName: node
- linkType: hard
-
"@react-spring/rafz@npm:~10.0.1":
version: 10.0.1
resolution: "@react-spring/rafz@npm:10.0.1"
@@ -15294,13 +15221,6 @@ __metadata:
languageName: node
linkType: hard
-"@react-spring/rafz@npm:~9.7.4":
- version: 9.7.4
- resolution: "@react-spring/rafz@npm:9.7.4"
- checksum: 10c0/975e27d6c19ed055dea91e97e473831a862aad2dc882f5402e98ac1f3f636f4ec05ee5cff623e5f6ece25388bb50a024bbc6dcdb6b750e43d53f0321140c5e52
- languageName: node
- linkType: hard
-
"@react-spring/shared@npm:~10.0.1":
version: 10.0.1
resolution: "@react-spring/shared@npm:10.0.1"
@@ -15313,18 +15233,6 @@ __metadata:
languageName: node
linkType: hard
-"@react-spring/shared@npm:~9.7.4":
- version: 9.7.4
- resolution: "@react-spring/shared@npm:9.7.4"
- dependencies:
- "@react-spring/rafz": "npm:~9.7.4"
- "@react-spring/types": "npm:~9.7.4"
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- checksum: 10c0/ae5cbb41f4876ebf365e0b2293466678d421eb68e736bfb4a0836a550518eb916fee763274f81c5af8a6a25f776df1480031e39f2981639aa79ac6cb6d290a15
- languageName: node
- linkType: hard
-
"@react-spring/types@npm:~10.0.1":
version: 10.0.1
resolution: "@react-spring/types@npm:10.0.1"
@@ -15332,28 +15240,6 @@ __metadata:
languageName: node
linkType: hard
-"@react-spring/types@npm:~9.7.4":
- version: 9.7.4
- resolution: "@react-spring/types@npm:9.7.4"
- checksum: 10c0/2ad43c0463dadb8caa58a7eeafc9ba447fa66cd7d9b9ea2241d58c619353155ea94d7e6a89fc6da0f071d500216c654b35a64a12a3a586d85e304ed6c93546fd
- languageName: node
- linkType: hard
-
-"@react-spring/web@npm:9.4.5 || ^9.7.2":
- version: 9.7.4
- resolution: "@react-spring/web@npm:9.7.4"
- dependencies:
- "@react-spring/animated": "npm:~9.7.4"
- "@react-spring/core": "npm:~9.7.4"
- "@react-spring/shared": "npm:~9.7.4"
- "@react-spring/types": "npm:~9.7.4"
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- checksum: 10c0/6312b612e53ae7a7cd0ab5fe16aaccbc6fba5d71248e30373a1f46e6aec997723095eff3c1a47f59aa8d5788b7092a898dee2120f841814f1d1295e5a4fcf5fb
- languageName: node
- linkType: hard
-
"@react-spring/web@npm:9.4.5 || ^9.7.2 || ^10.0":
version: 10.0.1
resolution: "@react-spring/web@npm:10.0.1"
@@ -20655,7 +20541,7 @@ __metadata:
languageName: node
linkType: hard
-"@types/prop-types@npm:*, @types/prop-types@npm:^15.7.2":
+"@types/prop-types@npm:*":
version: 15.7.12
resolution: "@types/prop-types@npm:15.7.12"
checksum: 10c0/1babcc7db6a1177779f8fde0ccc78d64d459906e6ef69a4ed4dd6339c920c2e05b074ee5a92120fe4e9d9f1a01c952f843ebd550bee2332fc2ef81d1706878f8
@@ -50963,6 +50849,7 @@ __metadata:
"@lingui/vite-plugin": "npm:^5.1.2"
"@nivo/core": "npm:^0.99.0"
"@nivo/line": "npm:^0.99.0"
+ "@nivo/pie": "npm:^0.99.0"
"@nivo/radial-bar": "npm:^0.99.0"
"@react-pdf/renderer": "npm:^4.1.6"
"@scalar/api-reference-react": "npm:^0.4.36"
@@ -51333,7 +51220,7 @@ __metadata:
"@keystatic/next": "npm:^5.0.3"
"@markdoc/markdoc": "npm:^0.5.1"
"@next/eslint-plugin-next": "npm:^14.1.4"
- "@nivo/calendar": "npm:^0.87.0"
+ "@nivo/calendar": "npm:^0.99.0"
"@types/facepaint": "npm:^1.2.5"
date-fns: "npm:^2.30.0"
drizzle-kit: "npm:^0.20.14"