From 50448c9b16c23e16a6a3036c90e58a2a963e3a5e Mon Sep 17 00:00:00 2001 From: nitin <142569587+ehconitin@users.noreply.github.com> Date: Tue, 26 Aug 2025 16:53:15 +0530 Subject: [PATCH] [Dashboards] Graph pie chart component + refactor (#14045) closes https://github.com/twentyhq/core-team-issues/issues/1373 closes https://github.com/twentyhq/core-team-issues/issues/1368 figma https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?node-id=72808-199932&t=c7GEJKFZKI0JhRv0-0 TODO: - ~~refactor to have a color registry~~ - ~~refactor legend and tooltips into a separate component for reuse across the graphs~~ - also refactor the border in between to be a common logic between pie and gauge graphs - ***open question*** - How should we code this border in between (renderValueEndLine), which is a layer - it's a function that returns JSX I could not figure out if it should be a util/component/hook? None of them fit into the category -- hence kept them inline in both gauge and pie charts for simplicity --- packages/twenty-front/package.json | 1 + .../components/GraphWidgetGaugeChart.tsx | 320 ++++++++--------- .../graphs/components/GraphWidgetLegend.tsx | 66 ++++ .../graphs/components/GraphWidgetPieChart.tsx | 207 +++++++++++ .../graphs/components/GraphWidgetTooltip.tsx | 80 +++++ .../GraphWidgetGaugeChart.stories.tsx | 109 ++++-- .../__stories__/GraphWidgetLegend.stories.tsx | 64 ++++ .../GraphWidgetPieChart.stories.tsx | 332 ++++++++++++++++++ .../GraphWidgetTooltip.stories.tsx | 65 ++++ .../graphs/types/GraphColorRegistry.ts | 5 + .../graphs/types/GraphColorScheme.ts | 8 + .../graphs/utils/calculateAngularGradient.ts | 13 + .../graphs/utils/createGradientDef.ts | 28 ++ .../graphs/utils/createGraphColorRegistry.ts | 51 +++ .../graphs/utils/getColorSchemeByIndex.ts | 10 + .../graphs/utils/getColorSchemeByName.ts | 9 + .../graphs/utils/graphFormatters.ts | 45 +++ packages/twenty-website/package.json | 2 +- yarn.lock | 175 ++------- 19 files changed, 1256 insertions(+), 334 deletions(-) create mode 100644 packages/twenty-front/src/modules/dashboards/graphs/components/GraphWidgetLegend.tsx create mode 100644 packages/twenty-front/src/modules/dashboards/graphs/components/GraphWidgetPieChart.tsx create mode 100644 packages/twenty-front/src/modules/dashboards/graphs/components/GraphWidgetTooltip.tsx create mode 100644 packages/twenty-front/src/modules/dashboards/graphs/components/__stories__/GraphWidgetLegend.stories.tsx create mode 100644 packages/twenty-front/src/modules/dashboards/graphs/components/__stories__/GraphWidgetPieChart.stories.tsx create mode 100644 packages/twenty-front/src/modules/dashboards/graphs/components/__stories__/GraphWidgetTooltip.stories.tsx create mode 100644 packages/twenty-front/src/modules/dashboards/graphs/types/GraphColorRegistry.ts create mode 100644 packages/twenty-front/src/modules/dashboards/graphs/types/GraphColorScheme.ts create mode 100644 packages/twenty-front/src/modules/dashboards/graphs/utils/calculateAngularGradient.ts create mode 100644 packages/twenty-front/src/modules/dashboards/graphs/utils/createGradientDef.ts create mode 100644 packages/twenty-front/src/modules/dashboards/graphs/utils/createGraphColorRegistry.ts create mode 100644 packages/twenty-front/src/modules/dashboards/graphs/utils/getColorSchemeByIndex.ts create mode 100644 packages/twenty-front/src/modules/dashboards/graphs/utils/getColorSchemeByName.ts create mode 100644 packages/twenty-front/src/modules/dashboards/graphs/utils/graphFormatters.ts 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"