From 168e7b16ec701bd5397463ffb96f18c434425ec3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?= <71827178+bosiraphael@users.noreply.github.com> Date: Mon, 13 Oct 2025 11:43:06 +0200 Subject: [PATCH] Design adjustments on Bar chart (#15028) - Make the ticks dynamic when we resize the graph - Fix formatting - Fix gradient color not working when the index have a space in it - Fix the maximum number of groups for a grouped by graph - Update the tooltip design and display the group Video: https://github.com/user-attachments/assets/9f304b6c-3dec-4ce2-9127-41d27f393d90 --------- Co-authored-by: Marie Stoppa --- ...onForAggregateOperationDropdownContent.tsx | 9 +- .../graph/components/GraphWidgetTooltip.tsx | 13 +- .../components/GraphWidgetBarChart.tsx | 58 ++++++--- .../GraphWidgetBarChartRenderer.tsx | 1 + .../constants/BarChartMargins.ts | 6 + .../hooks/useBarChartData.ts | 3 +- .../hooks/useBarChartTooltip.ts | 1 + .../computeBarChartCategoryTickValues.ts | 32 +++++ .../utils/computeBarChartValueTickCount.ts | 14 +++ .../utils/getBarChartAxisBottomConfig.ts | 30 ----- .../utils/getBarChartAxisConfigs.ts | 112 ++++++++++++++++++ .../utils/getBarChartAxisLeftConfig.ts | 30 ----- .../utils/truncateTickLabel.ts | 10 ++ .../components/GraphWidgetLineChart.tsx | 2 + .../hooks/useLineChartTooltip.ts | 12 ++ .../computeAggregateNumericValueForGraph.ts | 6 +- .../graph/utils/formatDimensionValue.ts | 25 +++- .../transformGroupByDataToBarChartData.ts | 8 +- ...formOneDimensionalGroupByToBarChartData.ts | 7 +- ...formTwoDimensionalGroupByToBarChartData.ts | 21 ++++ .../dimensions/components/NodeDimension.tsx | 32 ++--- .../components/NodeDimensionEffect.tsx | 32 +++++ 22 files changed, 351 insertions(+), 113 deletions(-) create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartMargins.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/computeBarChartCategoryTickValues.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/computeBarChartValueTickCount.ts delete mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartAxisBottomConfig.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartAxisConfigs.ts delete mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartAxisLeftConfig.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/truncateTickLabel.ts create mode 100644 packages/twenty-front/src/modules/ui/utilities/dimensions/components/NodeDimensionEffect.tsx diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartFieldSelectionForAggregateOperationDropdownContent.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartFieldSelectionForAggregateOperationDropdownContent.tsx index ba5348e232..399001bae7 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartFieldSelectionForAggregateOperationDropdownContent.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartFieldSelectionForAggregateOperationDropdownContent.tsx @@ -14,7 +14,7 @@ import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/com import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { t } from '@lingui/core/macro'; import { useState } from 'react'; -import { isDefined } from 'twenty-shared/utils'; +import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils'; import { useIcons } from 'twenty-ui/display'; import { MenuItemSelect } from 'twenty-ui/navigation'; import { filterBySearchQuery } from '~/utils/filterBySearchQuery'; @@ -59,7 +59,12 @@ export const ChartFieldSelectionForAggregateOperationDropdownContent = () => { searchQuery, getSearchableValues: (item) => [item.label, item.name], // TODO: remove the relation filter once group by is supported for relation fields - }).filter((field) => !isFieldRelation(field) && !field.isSystem); + }).filter( + (field) => + !isFieldRelation(field) && + !field.isSystem && + !isFieldMetadataDateKind(field.type), + ); const { getIcon } = useIcons(); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidgetTooltip.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidgetTooltip.tsx index c2bb2ffd85..44e34ab540 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidgetTooltip.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidgetTooltip.tsx @@ -10,8 +10,8 @@ const StyledTooltipContent = styled.div` box-shadow: ${({ theme }) => theme.boxShadow.strong}; display: flex; flex-direction: column; - gap: ${({ theme }) => theme.spacing(2)}; - padding: ${({ theme }) => theme.spacing(2)}; + gap: ${({ theme }) => theme.spacing(3)}; + padding: ${({ theme }) => theme.spacing(3)}; pointer-events: none; `; @@ -43,6 +43,12 @@ const StyledTooltipLink = styled.div` display: flex; `; +const StyledTooltipHeader = styled.div` + color: ${({ theme }) => theme.font.color.primary}; + font-size: ${({ theme }) => theme.font.size.sm}; + font-weight: ${({ theme }) => theme.font.weight.medium}; +`; + export type GraphWidgetTooltipItem = { label: string; formattedValue: string; @@ -52,16 +58,19 @@ export type GraphWidgetTooltipItem = { type GraphWidgetTooltipProps = { items: GraphWidgetTooltipItem[]; showClickHint?: boolean; + title?: string; }; export const GraphWidgetTooltip = ({ items, showClickHint = false, + title, }: GraphWidgetTooltipProps) => { const theme = useTheme(); return ( + {title && {title}} {items.map((item, index) => ( diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChart.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChart.tsx index cf1e413987..125a50d119 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChart.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChart.tsx @@ -2,26 +2,30 @@ import { GraphWidgetChartContainer } from '@/page-layout/widgets/graph/component import { GraphWidgetLegend } from '@/page-layout/widgets/graph/components/GraphWidgetLegend'; import { GraphWidgetTooltip } from '@/page-layout/widgets/graph/components/GraphWidgetTooltip'; import { BarChartEndLines } from '@/page-layout/widgets/graph/graphWidgetBarChart/components/BarChartEndLines'; +import { BAR_CHART_MARGINS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartMargins'; import { useBarChartData } from '@/page-layout/widgets/graph/graphWidgetBarChart/hooks/useBarChartData'; import { useBarChartHandlers } from '@/page-layout/widgets/graph/graphWidgetBarChart/hooks/useBarChartHandlers'; import { useBarChartTheme } from '@/page-layout/widgets/graph/graphWidgetBarChart/hooks/useBarChartTheme'; import { useBarChartTooltip } from '@/page-layout/widgets/graph/graphWidgetBarChart/hooks/useBarChartTooltip'; import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem'; import { type BarChartSeries } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartSeries'; -import { getBarChartAxisBottomConfig } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartAxisBottomConfig'; -import { getBarChartAxisLeftConfig } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartAxisLeftConfig'; +import { getBarChartAxisConfigs } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartAxisConfigs'; import { getBarChartColor } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartColor'; import { createGraphColorRegistry } from '@/page-layout/widgets/graph/utils/createGraphColorRegistry'; import { formatGraphValue, type GraphValueFormatOptions, } from '@/page-layout/widgets/graph/utils/graphFormatters'; +import { NodeDimensionEffect } from '@/ui/utilities/dimensions/components/NodeDimensionEffect'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { ResponsiveBar, type BarCustomLayerProps } from '@nivo/bar'; -import { useId } from 'react'; +import { useId, useRef, useState } from 'react'; import { isDefined } from 'twenty-shared/utils'; +const LEGEND_THRESHOLD = 10; +const LABEL_THRESHOLD = 15; + type GraphWidgetBarChartProps = { data: BarChartDataItem[]; indexBy: string; @@ -70,6 +74,9 @@ export const GraphWidgetBarChart = ({ const theme = useTheme(); const instanceId = useId(); const colorRegistry = createGraphColorRegistry(theme); + const [chartWidth, setChartWidth] = useState(0); + const [chartHeight, setChartHeight] = useState(0); + const containerRef = useRef(null); const formatOptions: GraphValueFormatOptions = { displayType, @@ -108,19 +115,25 @@ export const GraphWidgetBarChart = ({ formatOptions, }); - const axisBottomConfig = getBarChartAxisBottomConfig( - layout, - xAxisLabel, - yAxisLabel, - formatOptions, - ); + const isLargeChart = data.length * keys.length > LABEL_THRESHOLD; + const areThereTooManyKeys = keys.length > LEGEND_THRESHOLD; - const axisLeftConfig = getBarChartAxisLeftConfig( - layout, - xAxisLabel, - yAxisLabel, - formatOptions, - ); + const shouldShowLabels = showValues && !isLargeChart; + + const shouldShowLegend = showLegend && !areThereTooManyKeys; + + const { axisBottom: axisBottomConfig, axisLeft: axisLeftConfig } = + getBarChartAxisConfigs({ + width: chartWidth, + height: chartHeight, + data, + layout, + indexBy, + xAxisLabel, + yAxisLabel, + formatOptions, + axisFontSize: chartTheme.axis.ticks.text.fontSize, + }); const renderTooltip = (datum: Parameters[0]) => { const tooltipData = getTooltipData(datum); @@ -130,6 +143,7 @@ export const GraphWidgetBarChart = ({ ); }; @@ -147,14 +161,22 @@ export const GraphWidgetBarChart = ({ return ( + { + setChartWidth(width); + setChartHeight(height); + }} + /> { const total = data.reduce( (sum, d) => sum + Number(d[item.key] || 0), diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChartRenderer.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChartRenderer.tsx index 33484ccb41..27478f7bf8 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChartRenderer.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChartRenderer.tsx @@ -54,6 +54,7 @@ export const GraphWidgetBarChartRenderer = ({ yAxisLabel={yAxisLabel} showValues={showDataLabels} id={widget.id} + displayType="shortNumber" /> ); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartMargins.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartMargins.ts new file mode 100644 index 0000000000..a9505be244 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartMargins.ts @@ -0,0 +1,6 @@ +export const BAR_CHART_MARGINS = { + top: 20, + right: 20, + bottom: 60, + left: 70, +} as const; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/hooks/useBarChartData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/hooks/useBarChartData.ts index 774464cd2b..a39328b70a 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/hooks/useBarChartData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/hooks/useBarChartData.ts @@ -47,7 +47,8 @@ export const useBarChartData = ({ seriesConfig?.color, keyIndex, ); - const gradientId = `gradient-${id}-${instanceId}-${key}-${dataIndex}-${keyIndex}`; + const sanitizedKey = key.replace(/\s+/g, '-'); + const gradientId = `gradient-${id}-${instanceId}-${sanitizedKey}-${dataIndex}-${keyIndex}`; return { key, diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/hooks/useBarChartTooltip.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/hooks/useBarChartTooltip.ts index 56d24d1b2b..bca10a6169 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/hooks/useBarChartTooltip.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/hooks/useBarChartTooltip.ts @@ -40,6 +40,7 @@ export const useBarChartTooltip = ({ return { tooltipItem, showClickHint: isDefined(dataItem?.to), + title: String(datum.indexValue), }; }; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/computeBarChartCategoryTickValues.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/computeBarChartCategoryTickValues.ts new file mode 100644 index 0000000000..1ec4d371f2 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/computeBarChartCategoryTickValues.ts @@ -0,0 +1,32 @@ +import { BAR_CHART_MARGINS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartMargins'; +import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem'; + +const MINIMUM_WIDTH_PER_TICK = 100; + +export const computeBarChartCategoryTickValues = ({ + width, + data, + indexBy, +}: { + width: number; + data: BarChartDataItem[]; + indexBy: string; +}): (string | number)[] => { + if (width === 0 || data.length === 0) return []; + + const horizontalMargins = BAR_CHART_MARGINS.left + BAR_CHART_MARGINS.right; + const availableWidth = width - horizontalMargins; + const numberOfTicks = Math.floor(availableWidth / MINIMUM_WIDTH_PER_TICK); + + if (numberOfTicks <= 0) return []; + if (numberOfTicks === 1) return [data[0][indexBy] as string | number]; + if (numberOfTicks >= data.length) + return data.map((item) => item[indexBy] as string | number); + + const step = (data.length - 1) / (numberOfTicks - 1); + + return Array.from({ length: numberOfTicks }, (_, i) => { + const index = Math.min(Math.round(i * step), data.length - 1); + return data[index][indexBy] as string | number; + }); +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/computeBarChartValueTickCount.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/computeBarChartValueTickCount.ts new file mode 100644 index 0000000000..f65368dbc0 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/computeBarChartValueTickCount.ts @@ -0,0 +1,14 @@ +const MIN_TICK_SPACING_HEIGHT_RATIO = 2.5; + +type ComputeBarChartValueTickCountProps = { + height: number; + axisFontSize: number; +}; + +export const computeBarChartValueTickCount = ({ + height, + axisFontSize, +}: ComputeBarChartValueTickCountProps): number => { + const minHeightPerTick = axisFontSize * MIN_TICK_SPACING_HEIGHT_RATIO; + return Math.max(1, Math.floor(height / minHeightPerTick)); +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartAxisBottomConfig.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartAxisBottomConfig.ts deleted file mode 100644 index e41616fa83..0000000000 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartAxisBottomConfig.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { - formatGraphValue, - type GraphValueFormatOptions, -} from '@/page-layout/widgets/graph/utils/graphFormatters'; - -export const getBarChartAxisBottomConfig = ( - layout: 'vertical' | 'horizontal', - xAxisLabel?: string, - yAxisLabel?: string, - formatOptions?: GraphValueFormatOptions, -) => { - return layout === 'vertical' - ? { - tickSize: 0, - tickPadding: 5, - tickRotation: 0, - legend: xAxisLabel, - legendPosition: 'middle' as const, - legendOffset: 40, - } - : { - tickSize: 0, - tickPadding: 5, - tickRotation: 0, - legend: yAxisLabel, - legendPosition: 'middle' as const, - legendOffset: 40, - format: (value: number) => formatGraphValue(value, formatOptions || {}), - }; -}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartAxisConfigs.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartAxisConfigs.ts new file mode 100644 index 0000000000..7897df8ffc --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartAxisConfigs.ts @@ -0,0 +1,112 @@ +import { BAR_CHART_MARGINS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartMargins'; +import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem'; +import { computeBarChartCategoryTickValues } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/computeBarChartCategoryTickValues'; +import { computeBarChartValueTickCount } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/computeBarChartValueTickCount'; +import { truncateTickLabel } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/truncateTickLabel'; +import { + formatGraphValue, + type GraphValueFormatOptions, +} from '@/page-layout/widgets/graph/utils/graphFormatters'; + +const AVERAGE_CHARACTER_WIDTH_RATIO = 0.6; +const MIN_TICK_LABEL_LENGTH = 5; +const MAX_LEFT_AXIS_LABEL_LENGTH = 20; + +type GetBarChartAxisConfigsProps = { + width: number; + height: number; + data: BarChartDataItem[]; + layout: 'vertical' | 'horizontal'; + indexBy: string; + xAxisLabel?: string; + yAxisLabel?: string; + formatOptions?: GraphValueFormatOptions; + axisFontSize?: number; +}; + +export const getBarChartAxisConfigs = ({ + width, + height, + data, + layout, + indexBy, + xAxisLabel, + yAxisLabel, + formatOptions, + axisFontSize = 11, +}: GetBarChartAxisConfigsProps) => { + const categoryTickValues = computeBarChartCategoryTickValues({ + width, + data, + indexBy, + }); + + const availableWidth = + width - (BAR_CHART_MARGINS.left + BAR_CHART_MARGINS.right); + const availableHeight = + height - (BAR_CHART_MARGINS.top + BAR_CHART_MARGINS.bottom); + const widthPerTick = + categoryTickValues.length > 0 + ? availableWidth / categoryTickValues.length + : 0; + const averageCharacterWidth = axisFontSize * AVERAGE_CHARACTER_WIDTH_RATIO; + const maxLabelLength = Math.max( + MIN_TICK_LABEL_LENGTH, + Math.floor(widthPerTick / averageCharacterWidth), + ); + + const numberOfValueTicks = computeBarChartValueTickCount({ + height: availableHeight, + axisFontSize, + }); + + if (layout === 'vertical') { + return { + axisBottom: { + tickSize: 0, + tickPadding: 5, + tickRotation: 0, + tickValues: categoryTickValues, + legend: xAxisLabel, + legendPosition: 'middle' as const, + legendOffset: 40, + format: (value: string | number) => + truncateTickLabel(String(value), maxLabelLength), + }, + axisLeft: { + tickSize: 0, + tickPadding: 5, + tickRotation: 0, + tickValues: numberOfValueTicks, + legend: yAxisLabel, + legendPosition: 'middle' as const, + legendOffset: -50, + format: (value: number) => formatGraphValue(value, formatOptions || {}), + }, + }; + } + + return { + axisBottom: { + tickSize: 0, + tickPadding: 5, + tickRotation: 0, + tickValues: numberOfValueTicks, + legend: yAxisLabel, + legendPosition: 'middle' as const, + legendOffset: 40, + format: (value: number) => formatGraphValue(value, formatOptions || {}), + }, + axisLeft: { + tickSize: 0, + tickPadding: 5, + tickRotation: 0, + tickValues: categoryTickValues, + legend: xAxisLabel, + legendPosition: 'middle' as const, + legendOffset: -50, + format: (value: string | number) => + truncateTickLabel(String(value), MAX_LEFT_AXIS_LABEL_LENGTH), + }, + }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartAxisLeftConfig.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartAxisLeftConfig.ts deleted file mode 100644 index 751bd21b5a..0000000000 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartAxisLeftConfig.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { - formatGraphValue, - type GraphValueFormatOptions, -} from '@/page-layout/widgets/graph/utils/graphFormatters'; - -export const getBarChartAxisLeftConfig = ( - layout: 'vertical' | 'horizontal', - xAxisLabel?: string, - yAxisLabel?: string, - formatOptions?: GraphValueFormatOptions, -) => { - return layout === 'vertical' - ? { - tickSize: 0, - tickPadding: 5, - tickRotation: 0, - legend: yAxisLabel, - legendPosition: 'middle' as const, - legendOffset: -50, - format: (value: number) => formatGraphValue(value, formatOptions || {}), - } - : { - tickSize: 0, - tickPadding: 5, - tickRotation: 0, - legend: xAxisLabel, - legendPosition: 'middle' as const, - legendOffset: -50, - }; -}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/truncateTickLabel.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/truncateTickLabel.ts new file mode 100644 index 0000000000..5871a287b7 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/truncateTickLabel.ts @@ -0,0 +1,10 @@ +export const truncateTickLabel = ( + value: string | number, + maxLength: number, +): string => { + const stringValue = String(value); + if (maxLength < 4 || stringValue.length <= maxLength) { + return stringValue; + } + return `${stringValue.slice(0, maxLength - 3)}...`; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/components/GraphWidgetLineChart.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/components/GraphWidgetLineChart.tsx index 31fdd6b0a5..411a9c6738 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/components/GraphWidgetLineChart.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/components/GraphWidgetLineChart.tsx @@ -144,6 +144,7 @@ export const GraphWidgetLineChart = ({ ); }; @@ -157,6 +158,7 @@ export const GraphWidgetLineChart = ({ ); }; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/hooks/useLineChartTooltip.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/hooks/useLineChartTooltip.ts index 224829a031..a79378afc5 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/hooks/useLineChartTooltip.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/hooks/useLineChartTooltip.ts @@ -27,6 +27,14 @@ export const useLineChartTooltip = ({ ); const createSliceTooltipData = ({ slice }: SliceTooltipProps) => { + if (!isDefined(slice.points) || slice.points.length === 0) { + return { + items: [], + showClickHint: false, + title: undefined, + }; + } + const tooltipItems = slice.points .map((point) => { const enrichedSeriesItem = enrichedSeriesMap.get( @@ -54,9 +62,12 @@ export const useLineChartTooltip = ({ return false; }); + const xValue = slice.points[0]?.data?.x; + return { items: tooltipItems, showClickHint: hasClickablePoint, + title: isDefined(xValue) ? String(xValue) : undefined, }; }; @@ -79,6 +90,7 @@ export const useLineChartTooltip = ({ }, ], showClickHint: isDefined(dataPoint?.to), + title: isDefined(point.data.x) ? String(point.data.x) : undefined, }; }; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/computeAggregateNumericValueForGraph.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/computeAggregateNumericValueForGraph.ts index 7cf475d7a1..6cd09a247e 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/computeAggregateNumericValueForGraph.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/computeAggregateNumericValueForGraph.ts @@ -61,7 +61,11 @@ export const computeAggregateNumericValueForGraph = ({ } switch (field.type) { - case FieldMetadataType.CURRENCY: + case FieldMetadataType.CURRENCY: { + // Convert from micros (millionths) to actual currency amount + return Number(aggregateValue) / 1_000_000; + } + case FieldMetadataType.NUMBER: { return Number(aggregateValue); } diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/formatDimensionValue.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/formatDimensionValue.ts index 3a5d8039f4..8549ccc16f 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/formatDimensionValue.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/formatDimensionValue.ts @@ -2,16 +2,19 @@ import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataIte import { GRAPH_DEFAULT_DATE_GRANULARITY } from '@/page-layout/widgets/graph/constants/GraphDefaultDateGranularity.constant'; import { formatDateByGranularity } from '@/page-layout/widgets/graph/utils/formatDateByGranularity'; import { t } from '@lingui/core/macro'; +import { isNonEmptyString } from '@sniptt/guards'; import { FieldMetadataType, ObjectRecordGroupByDateGranularity, } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; +import { formatToShortNumber } from '~/utils/format/formatToShortNumber'; type FormatDimensionValueParams = { value: unknown; fieldMetadata: FieldMetadataItem; dateGranularity?: ObjectRecordGroupByDateGranularity; + subFieldName?: string; }; const normalizeMultiSelectValue = (value: unknown): unknown[] => { @@ -39,9 +42,10 @@ export const formatDimensionValue = ({ value, fieldMetadata, dateGranularity = GRAPH_DEFAULT_DATE_GRANULARITY as ObjectRecordGroupByDateGranularity, + subFieldName, }: FormatDimensionValueParams): string => { if (!isDefined(value)) { - return ''; + return t`Not Set`; } switch (fieldMetadata.type) { @@ -85,6 +89,25 @@ export const formatDimensionValue = ({ return formatDateByGranularity(new Date(String(value)), dateGranularity); } + case FieldMetadataType.NUMBER: + case FieldMetadataType.CURRENCY: { + if ( + fieldMetadata.type === FieldMetadataType.CURRENCY && + subFieldName === 'currencyCode' + ) { + if (!isNonEmptyString(value)) { + return t`Not Set`; + } + + return String(value); + } + const numericValue = typeof value === 'number' ? value : Number(value); + if (isNaN(numericValue)) { + return String(value); + } + return formatToShortNumber(numericValue); + } + default: return String(value); } diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformGroupByDataToBarChartData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformGroupByDataToBarChartData.ts index ee5fa13c26..de0eefc2fa 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformGroupByDataToBarChartData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformGroupByDataToBarChartData.ts @@ -2,7 +2,6 @@ import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataIte import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel'; import { getGroupByQueryName } from '@/page-layout/utils/getGroupByQueryName'; -import { GRAPH_MAXIMUM_NUMBER_OF_GROUPS } from '@/page-layout/widgets/graph/constants/GraphMaximumNumberOfGroups.constant'; import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem'; import { type BarChartSeries } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartSeries'; import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult'; @@ -77,18 +76,15 @@ export const transformGroupByDataToBarChartData = ({ }); const queryName = getGroupByQueryName(objectMetadataItem); - const queryResults = groupByData[queryName]; + const rawResults = groupByData[queryName]; - if (!isDefined(queryResults) || !Array.isArray(queryResults)) { + if (!isDefined(rawResults) || !Array.isArray(rawResults)) { return { ...EMPTY_BAR_CHART_RESULT, indexBy: indexByKey, }; } - // TODO: Add a limit to the query instead of slicing here (issue: twentyhq/core-team-issues#1600) - const rawResults = queryResults.slice(0, GRAPH_MAXIMUM_NUMBER_OF_GROUPS); - const showXAxis = configuration.axisNameDisplay === AxisNameDisplay.X || configuration.axisNameDisplay === AxisNameDisplay.BOTH; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformOneDimensionalGroupByToBarChartData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformOneDimensionalGroupByToBarChartData.ts index 78efcd9a2a..f919b87546 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformOneDimensionalGroupByToBarChartData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformOneDimensionalGroupByToBarChartData.ts @@ -2,6 +2,7 @@ import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataIte import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations'; import { GRAPH_DEFAULT_COLOR } from '@/page-layout/widgets/graph/constants/GraphDefaultColor.constant'; +import { GRAPH_MAXIMUM_NUMBER_OF_GROUPS } from '@/page-layout/widgets/graph/constants/GraphMaximumNumberOfGroups.constant'; import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem'; import { type BarChartSeries } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartSeries'; import { type GraphColor } from '@/page-layout/widgets/graph/types/GraphColor'; @@ -41,13 +42,17 @@ export const transformOneDimensionalGroupByToBarChartData = ({ subFieldName: configuration.groupBySubFieldNameX, }); - const data: BarChartDataItem[] = rawResults.map((result) => { + // TODO: Add a limit to the query instead of slicing here (issue: twentyhq/core-team-issues#1600) + const limitedResults = rawResults.slice(0, GRAPH_MAXIMUM_NUMBER_OF_GROUPS); + + const data: BarChartDataItem[] = limitedResults.map((result) => { const dimensionValues = result.groupByDimensionValues; const xValue = isDefined(dimensionValues?.[0]) ? formatDimensionValue({ value: dimensionValues[0], fieldMetadata: groupByFieldX, + subFieldName: configuration.groupBySubFieldNameX ?? undefined, }) : ''; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformTwoDimensionalGroupByToBarChartData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformTwoDimensionalGroupByToBarChartData.ts index fc6edf88ce..36cd99749c 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformTwoDimensionalGroupByToBarChartData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformTwoDimensionalGroupByToBarChartData.ts @@ -1,6 +1,7 @@ import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations'; +import { GRAPH_MAXIMUM_NUMBER_OF_GROUPS } from '@/page-layout/widgets/graph/constants/GraphMaximumNumberOfGroups.constant'; import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem'; import { type BarChartSeries } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartSeries'; import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult'; @@ -42,6 +43,7 @@ export const transformTwoDimensionalGroupByToBarChartData = ({ }); const dataMap = new Map(); + const xValues = new Set(); const yValues = new Set(); rawResults.forEach((result) => { @@ -51,12 +53,28 @@ export const transformTwoDimensionalGroupByToBarChartData = ({ const xValue = formatDimensionValue({ value: dimensionValues[0], fieldMetadata: groupByFieldX, + subFieldName: configuration.groupBySubFieldNameX ?? undefined, }); const yValue = formatDimensionValue({ value: dimensionValues[1], fieldMetadata: groupByFieldY, + subFieldName: configuration.groupBySubFieldNameY ?? undefined, }); + // TODO: Add a limit to the query instead of checking here (issue: twentyhq/core-team-issues#1600) + const isNewX = !xValues.has(xValue); + const isNewY = !yValues.has(yValue); + const totalUniqueDimensions = xValues.size * yValues.size; + const additionalDimensions = + (isNewX ? 1 : 0) * yValues.size + (isNewY ? 1 : 0) * xValues.size; + + if ( + totalUniqueDimensions + additionalDimensions > + GRAPH_MAXIMUM_NUMBER_OF_GROUPS + ) { + return; + } + const aggregateValue = computeAggregateValueFromGroupByResult({ rawResult: result, aggregateField, @@ -66,6 +84,9 @@ export const transformTwoDimensionalGroupByToBarChartData = ({ objectMetadataItem, }); + if (!isDefined(aggregateValue)) return; + + xValues.add(xValue); yValues.add(yValue); if (!dataMap.has(xValue)) { diff --git a/packages/twenty-front/src/modules/ui/utilities/dimensions/components/NodeDimension.tsx b/packages/twenty-front/src/modules/ui/utilities/dimensions/components/NodeDimension.tsx index ba937a13f7..08c8efc2d4 100644 --- a/packages/twenty-front/src/modules/ui/utilities/dimensions/components/NodeDimension.tsx +++ b/packages/twenty-front/src/modules/ui/utilities/dimensions/components/NodeDimension.tsx @@ -1,5 +1,5 @@ -import { type ReactNode, useEffect, useRef } from 'react'; -import { isDefined } from 'twenty-shared/utils'; +import { NodeDimensionEffect } from '@/ui/utilities/dimensions/components/NodeDimensionEffect'; +import { type ReactNode, useRef } from 'react'; type NodeDimensionProps = { children: ReactNode; @@ -12,23 +12,13 @@ export const NodeDimension = ({ }: NodeDimensionProps) => { const elementRef = useRef(null); - useEffect(() => { - if (!elementRef.current) return; - - const resizeObserver = new ResizeObserver((entries) => { - const entry = entries[0]; - if (isDefined(entry)) { - onDimensionChange({ - width: entry.contentRect.width, - height: entry.contentRect.height, - }); - } - }); - - resizeObserver.observe(elementRef.current); - - return () => resizeObserver.disconnect(); - }, [onDimensionChange]); - - return
{children}
; + return ( + <> + +
{children}
+ + ); }; diff --git a/packages/twenty-front/src/modules/ui/utilities/dimensions/components/NodeDimensionEffect.tsx b/packages/twenty-front/src/modules/ui/utilities/dimensions/components/NodeDimensionEffect.tsx new file mode 100644 index 0000000000..d848e9810e --- /dev/null +++ b/packages/twenty-front/src/modules/ui/utilities/dimensions/components/NodeDimensionEffect.tsx @@ -0,0 +1,32 @@ +import { type RefObject, useEffect } from 'react'; +import { isDefined } from 'twenty-shared/utils'; + +type NodeDimensionEffectProps = { + elementRef: RefObject; + onDimensionChange: (dimensions: { width: number; height: number }) => void; +}; + +export const NodeDimensionEffect = ({ + elementRef, + onDimensionChange, +}: NodeDimensionEffectProps) => { + useEffect(() => { + if (!elementRef.current) return; + + const resizeObserver = new ResizeObserver((entries) => { + const entry = entries[0]; + if (isDefined(entry)) { + onDimensionChange({ + width: entry.contentRect.width, + height: entry.contentRect.height, + }); + } + }); + + resizeObserver.observe(elementRef.current); + + return () => resizeObserver.disconnect(); + }, [elementRef, onDimensionChange]); + + return null; +};