From 01d8b99c386ff180c274e74b78cf1bbc5db02c52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?= <71827178+bosiraphael@users.noreply.github.com> Date: Mon, 27 Oct 2025 17:46:06 +0100 Subject: [PATCH] Update bar chart design (#15372) - Implement new dynamic color palettes - Create custom bar component with rounded edges https://github.com/user-attachments/assets/8246f16d-0239-4807-bb4a-9647367575f2 --- .../chart-settings/ChartSettingItem.tsx | 2 +- .../ChartColorGradientOption.tsx | 76 +++++++ .../ChartColorPaletteOption.tsx | 61 +++++ .../ChartColorSelectionDropdownContent.tsx | 86 ++++--- .../constants/settings/ColorsSetting.ts | 2 + .../page-layout/types/ChartSettingsGroup.ts | 1 + .../GraphGroupColorMinimumAlpha.constant.ts | 1 + .../GraphMaximumNumberOfGroupColors.ts | 1 + .../components/BarChartEndLines.tsx | 45 ---- .../components/CustomBarItem.tsx | 211 ++++++++++++++++++ .../components/GraphWidgetBarChart.tsx | 46 ++-- .../constants/BarChartHoverBrightness.ts | 1 + .../hooks/__tests__/useBarChartData.test.ts | 158 +++---------- .../hooks/useBarChartData.ts | 59 ++--- .../types/BarChartConfig.ts | 1 - .../utils/getBarChartColor.ts | 2 +- .../hooks/useGaugeChartData.ts | 6 +- .../hooks/__tests__/useLineChartData.test.ts | 26 +-- .../hooks/useLineChartData.ts | 7 +- .../hooks/usePieChartData.ts | 10 +- .../widgets/graph/types/GraphColor.ts | 1 + .../graph/utils/createGraphColorRegistry.ts | 20 +- .../widgets/graph/utils/generateGroupColor.ts | 32 +++ .../widgets/graph/utils/getColorScheme.ts | 35 ++- ...formTwoDimensionalGroupByToBarChartData.ts | 2 + .../get-page-layout-widget-data-seeds.util.ts | 4 + .../display/color/components/ColorSample.tsx | 26 ++- .../src/theme/constants/BorderDark.ts | 2 + .../src/theme/constants/BorderLight.ts | 2 + 29 files changed, 593 insertions(+), 333 deletions(-) create mode 100644 packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartColorGradientOption.tsx create mode 100644 packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartColorPaletteOption.tsx create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/constants/GraphGroupColorMinimumAlpha.constant.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/constants/GraphMaximumNumberOfGroupColors.ts delete mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/BarChartEndLines.tsx create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/CustomBarItem.tsx create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartHoverBrightness.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/utils/generateGroupColor.ts diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/chart-settings/ChartSettingItem.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/chart-settings/ChartSettingItem.tsx index 331b7efafe9..68eadc0b647 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/chart-settings/ChartSettingItem.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/chart-settings/ChartSettingItem.tsx @@ -117,7 +117,7 @@ export const ChartSettingItem = ({ id={item.id} dropdownId={item.id} dropdownComponents={ - + {item.DropdownContent && } } diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartColorGradientOption.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartColorGradientOption.tsx new file mode 100644 index 00000000000..2b9aa6b8b1b --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartColorGradientOption.tsx @@ -0,0 +1,76 @@ +import { createGraphColorRegistry } from '@/page-layout/widgets/graph/utils/createGraphColorRegistry'; +import { generateGroupColor } from '@/page-layout/widgets/graph/utils/generateGroupColor'; +import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { ColorSample } from 'twenty-ui/display'; +import { MenuItemSelect } from 'twenty-ui/navigation'; +import { type ThemeColor } from 'twenty-ui/theme'; + +type ChartColorGradientOptionProps = { + colorOption: { + id: string; + name: string; + colorName: ThemeColor | 'auto'; + }; + selectedItemId: string | null; + currentColor: string | null | undefined; + onSelectColor: (colorName: ThemeColor | 'auto') => void; +}; + +const StyledColorSamplesContainer = styled.div` + align-items: center; + display: flex; + gap: ${({ theme }) => theme.spacing(0.5)}; +`; + +const COLOR_GROUP_COUNT = 5; + +export const ChartColorGradientOption = ({ + colorOption, + selectedItemId, + currentColor, + onSelectColor, +}: ChartColorGradientOptionProps) => { + const colorName = colorOption.colorName as ThemeColor; + const theme = useTheme(); + const colorRegistry = createGraphColorRegistry(theme); + + const colorSamples = ( + + {Array.from({ length: COLOR_GROUP_COUNT }).map((_, index) => { + const colorScheme = colorRegistry[colorName]; + const reversedIndex = COLOR_GROUP_COUNT - 1 - index; + const groupColor = generateGroupColor({ + colorScheme, + groupIndex: reversedIndex, + totalGroups: COLOR_GROUP_COUNT, + }); + return ; + })} + + ); + + return ( + { + onSelectColor(colorOption.colorName); + }} + > + { + onSelectColor(colorOption.colorName); + }} + /> + + ); +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartColorPaletteOption.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartColorPaletteOption.tsx new file mode 100644 index 00000000000..4de92707920 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartColorPaletteOption.tsx @@ -0,0 +1,61 @@ +import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem'; +import styled from '@emotion/styled'; +import { ColorSample } from 'twenty-ui/display'; +import { MenuItemSelect } from 'twenty-ui/navigation'; +import { MAIN_COLORS, type ThemeColor } from 'twenty-ui/theme'; + +type ChartColorPaletteOptionProps = { + selectedItemId: string | null; + currentColor: string | null | undefined; + onSelectColor: (colorName: ThemeColor | 'auto') => void; +}; + +const StyledColorSamplesContainer = styled.div` + align-items: center; + display: flex; + gap: ${({ theme }) => theme.spacing(0.5)}; +`; + +export const ChartColorPaletteOption = ({ + selectedItemId, + currentColor, + onSelectColor, +}: ChartColorPaletteOptionProps) => { + const paletteColors: Array = [ + 'purple', + 'pink', + 'red', + 'orange', + 'yellow', + ]; + + const colorSamples = ( + + {paletteColors.map((paletteColorName) => { + const baseColor = MAIN_COLORS[paletteColorName]; + return ; + })} + + ); + + return ( + { + onSelectColor('auto'); + }} + > + { + onSelectColor('auto'); + }} + /> + + ); +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartColorSelectionDropdownContent.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartColorSelectionDropdownContent.tsx index e53c966f8bc..1f7b9b5d663 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartColorSelectionDropdownContent.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartColorSelectionDropdownContent.tsx @@ -1,24 +1,30 @@ +import { ChartColorGradientOption } from '@/command-menu/pages/page-layout/components/dropdown-content/ChartColorGradientOption'; +import { ChartColorPaletteOption } from '@/command-menu/pages/page-layout/components/dropdown-content/ChartColorPaletteOption'; import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord'; import { useUpdateCurrentWidgetConfig } from '@/command-menu/pages/page-layout/hooks/useUpdateCurrentWidgetConfig'; import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode'; import { type ChartConfiguration } from '@/command-menu/pages/page-layout/types/ChartConfiguration'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; +import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext'; import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown'; import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList'; -import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem'; import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { t } from '@lingui/core/macro'; import { useState } from 'react'; import { capitalize, isDefined } from 'twenty-shared/utils'; -import { ColorSample } from 'twenty-ui/display'; -import { MenuItemSelect } from 'twenty-ui/navigation'; import { MAIN_COLOR_NAMES, type ThemeColor } from 'twenty-ui/theme'; import { filterBySearchQuery } from '~/utils/filterBySearchQuery'; +type ColorOption = { + id: string; + name: string; + colorName: ThemeColor | 'auto'; +}; + export const ChartColorSelectionDropdownContent = () => { const [searchQuery, setSearchQuery] = useState(''); const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord(); @@ -54,11 +60,18 @@ export const ChartColorSelectionDropdownContent = () => { const currentColor = configuration.color; - const colorOptions = MAIN_COLOR_NAMES.map((colorName) => ({ - id: colorName, - name: capitalize(colorName), - colorName: colorName, - })); + const colorOptions: ColorOption[] = [ + { + id: 'auto', + name: 'Palette', + colorName: 'auto', + }, + ...MAIN_COLOR_NAMES.map((colorName) => ({ + id: colorName, + name: capitalize(colorName), + colorName: colorName, + })), + ]; const filteredColorOptions = filterBySearchQuery({ items: colorOptions, @@ -66,7 +79,7 @@ export const ChartColorSelectionDropdownContent = () => { getSearchableValues: (item) => [item.name], }); - const handleSelectColor = (colorName: ThemeColor) => { + const handleSelectColor = (colorName: ThemeColor | 'auto') => { updateCurrentWidgetConfig({ configToUpdate: { color: colorName, @@ -75,6 +88,10 @@ export const ChartColorSelectionDropdownContent = () => { closeDropdown(); }; + const regularColorOptions = filteredColorOptions.filter( + (option) => option.colorName !== 'auto', + ); + return ( <> { onChange={(event) => setSearchQuery(event.target.value)} value={searchQuery} /> + { (colorOption) => colorOption.id, )} > - {filteredColorOptions.map((colorOption) => ( - { - handleSelectColor(colorOption.colorName); - }} - > - ( - - )} - onClick={() => { - handleSelectColor(colorOption.colorName); - }} - /> - - ))} + + {regularColorOptions.length > 0 && ( + <> + + + colorOption.id, + )} + > + {regularColorOptions.map((colorOption) => ( + + ))} + + + + )} ); }; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/ColorsSetting.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/ColorsSetting.ts index 5c663567b8e..ede862ffec6 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/ColorsSetting.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/ColorsSetting.ts @@ -2,6 +2,7 @@ import { ChartColorSelectionDropdownContent } from '@/command-menu/pages/page-la import { CHART_CONFIGURATION_SETTING_LABELS } from '@/command-menu/pages/page-layout/constants/settings/ChartConfigurationSettingLabels'; import { CHART_CONFIGURATION_SETTING_IDS } from '@/command-menu/pages/page-layout/types/ChartConfigurationSettingIds'; import { type ChartSettingsItem } from '@/command-menu/pages/page-layout/types/ChartSettingsGroup'; +import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth'; import { IconColorSwatch } from 'twenty-ui/display'; export const COLORS_SETTING: ChartSettingsItem = { @@ -10,4 +11,5 @@ export const COLORS_SETTING: ChartSettingsItem = { label: CHART_CONFIGURATION_SETTING_LABELS.COLORS, id: CHART_CONFIGURATION_SETTING_IDS.COLORS, DropdownContent: ChartColorSelectionDropdownContent, + dropdownWidth: GenericDropdownContentWidth.ExtraLarge, }; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/types/ChartSettingsGroup.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/types/ChartSettingsGroup.ts index 1bf0ff644ca..f8d06e09d2a 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/types/ChartSettingsGroup.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/types/ChartSettingsGroup.ts @@ -16,6 +16,7 @@ export type ChartSettingsItem = { isBoolean: boolean; dependsOn?: CHART_CONFIGURATION_SETTING_IDS[]; DropdownContent?: ComponentType; + dropdownWidth?: number; isInput?: boolean; inputPlaceholder?: MessageDescriptor; }; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/constants/GraphGroupColorMinimumAlpha.constant.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/constants/GraphGroupColorMinimumAlpha.constant.ts new file mode 100644 index 00000000000..c4ffc23790a --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/constants/GraphGroupColorMinimumAlpha.constant.ts @@ -0,0 +1 @@ +export const GRAPH_GROUP_COLOR_MINIMUM_ALPHA = 0.2; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/constants/GraphMaximumNumberOfGroupColors.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/constants/GraphMaximumNumberOfGroupColors.ts new file mode 100644 index 00000000000..3ff67fb6813 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/constants/GraphMaximumNumberOfGroupColors.ts @@ -0,0 +1 @@ +export const GRAPH_MAXIMUM_NUMBER_OF_GROUP_COLORS = 10; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/BarChartEndLines.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/BarChartEndLines.tsx deleted file mode 100644 index 8b31f968bbe..00000000000 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/BarChartEndLines.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem'; -import { type BarChartEnrichedKey } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartEnrichedKey'; -import { calculateBarChartEndLineCoordinates } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/calculateBarChartEndLineCoordinates'; -import { type ComputedBarDatum } from '@nivo/bar'; -import { isDefined } from 'twenty-shared/utils'; - -type BarChartEndLinesProps = { - bars: readonly ComputedBarDatum[]; - enrichedKeysMap: Map; - layout: 'vertical' | 'horizontal'; -}; - -export const BarChartEndLines = ({ - bars, - enrichedKeysMap, - layout, -}: BarChartEndLinesProps) => { - return ( - - {bars.map((bar: ComputedBarDatum, index: number) => { - const enrichedKey = enrichedKeysMap.get(String(bar.data.id)); - if (!isDefined(enrichedKey)) { - return null; - } - const lineColor = enrichedKey.colorScheme.solid; - const { x1, y1, x2, y2 } = calculateBarChartEndLineCoordinates( - bar, - layout, - ); - - return ( - - ); - })} - - ); -}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/CustomBarItem.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/CustomBarItem.tsx new file mode 100644 index 00000000000..100057370a3 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/CustomBarItem.tsx @@ -0,0 +1,211 @@ +import { BAR_CHART_HOVER_BRIGHTNESS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartHoverBrightness'; +import { type BarDatum, type BarItemProps } from '@nivo/bar'; +import { Text } from '@nivo/text'; +import { useTheme } from '@nivo/theming'; +import { useTooltip } from '@nivo/tooltip'; +import { animated, to } from '@react-spring/web'; +import { isNumber } from '@sniptt/guards'; +import { createElement, useCallback, useMemo, type MouseEvent } from 'react'; +import styled from 'styled-components'; +import { isDefined } from 'twenty-shared/utils'; + +type CustomBarItemProps = BarItemProps & { + keys?: string[]; + groupMode?: 'grouped' | 'stacked'; + data?: readonly D[]; + indexBy?: string; + layout?: 'vertical' | 'horizontal'; +}; + +const StyledBarRect = styled(animated.rect)<{ $isInteractive?: boolean }>` + cursor: ${({ $isInteractive }) => ($isInteractive ? 'pointer' : 'default')}; + transition: filter 0.15s ease-in-out; + + &:hover { + filter: ${({ $isInteractive }) => + $isInteractive ? `brightness(${BAR_CHART_HOVER_BRIGHTNESS})` : 'none'}; + } +`; + +// This is a copy of the BarItem component from @nivo/bar with some design modifications +export const CustomBarItem = ({ + bar: { data: barData, ...bar }, + style: { + borderColor, + color, + height, + labelColor, + labelOpacity, + labelX, + labelY, + transform, + width, + textAnchor, + }, + borderRadius, + borderWidth, + label, + shouldRenderLabel, + isInteractive, + onClick, + onMouseEnter, + onMouseLeave, + tooltip, + isFocusable, + ariaLabel, + ariaLabelledBy, + ariaDescribedBy, + ariaDisabled, + ariaHidden, + keys, + groupMode = 'grouped', + data: chartData, + indexBy, + layout = 'vertical', +}: CustomBarItemProps) => { + const theme = useTheme(); + const { showTooltipFromEvent, showTooltipAt, hideTooltip } = useTooltip(); + + const renderTooltip = useMemo( + () => () => createElement(tooltip, { ...bar, ...barData }), + [tooltip, bar, barData], + ); + + const handleClick = useCallback( + (event: MouseEvent) => { + onClick?.({ color: bar.color, ...barData }, event); + }, + [bar, barData, onClick], + ); + const handleTooltip = useCallback( + (event: MouseEvent) => + showTooltipFromEvent(renderTooltip(), event), + [showTooltipFromEvent, renderTooltip], + ); + const handleMouseEnter = useCallback( + (event: MouseEvent) => { + onMouseEnter?.(barData, event); + showTooltipFromEvent(renderTooltip(), event); + }, + [barData, onMouseEnter, showTooltipFromEvent, renderTooltip], + ); + const handleMouseLeave = useCallback( + (event: MouseEvent) => { + onMouseLeave?.(barData, event); + hideTooltip(); + }, + [barData, hideTooltip, onMouseLeave], + ); + + const handleFocus = useCallback(() => { + showTooltipAt(renderTooltip(), [bar.absX + bar.width / 2, bar.absY]); + }, [showTooltipAt, renderTooltip, bar]); + + const handleBlur = useCallback(() => { + hideTooltip(); + }, [hideTooltip]); + + const isTopBar = useMemo(() => { + const isStackedAndValid = + groupMode === 'stacked' && + isDefined(keys) && + keys.length > 0 && + isDefined(chartData) && + isDefined(indexBy); + + if (!isStackedAndValid) { + return true; + } + + const dataPoint = chartData.find( + (data) => data[indexBy] === barData.indexValue, + ); + + if (!isDefined(dataPoint)) { + return true; + } + + const currentKeyIndex = keys.findIndex((key) => key === barData.id); + + if (currentKeyIndex === -1) { + return true; + } + + const keysAboveCurrentKey = keys.slice(currentKeyIndex + 1); + const hasBarAbove = keysAboveCurrentKey.some((key) => { + const value = dataPoint[key]; + return isNumber(value) && value > 0; + }); + + return !hasBarAbove; + }, [groupMode, keys, barData, chartData, indexBy]); + + const isHorizontal = layout === 'horizontal'; + + return ( + + {isTopBar && ( + + + + Math.max(value + (isHorizontal ? borderRadius : 0), 0), + )} + height={to(height, (value) => + Math.max(value + (isHorizontal ? 0 : borderRadius), 0), + )} + /> + + + )} + + Math.max(value, 0))} + height={to(height, (value) => Math.max(value, 0))} + fill={color} + strokeWidth={borderWidth} + stroke={borderColor} + focusable={isFocusable} + tabIndex={isFocusable ? 0 : undefined} + aria-label={ariaLabel ? ariaLabel(barData) : undefined} + aria-labelledby={ariaLabelledBy ? ariaLabelledBy(barData) : undefined} + aria-describedby={ + ariaDescribedBy ? ariaDescribedBy(barData) : undefined + } + aria-disabled={ariaDisabled ? ariaDisabled(barData) : undefined} + aria-hidden={ariaHidden ? ariaHidden(barData) : undefined} + onMouseEnter={isInteractive ? handleMouseEnter : undefined} + onMouseMove={isInteractive ? handleTooltip : undefined} + onMouseLeave={isInteractive ? handleMouseLeave : undefined} + onClick={isInteractive ? handleClick : undefined} + onFocus={isInteractive && isFocusable ? handleFocus : undefined} + onBlur={isInteractive && isFocusable ? handleBlur : undefined} + data-testid={`bar.item.${barData.id}.${barData.index}`} + /> + + {shouldRenderLabel && ( + + {label} + + )} + + ); +}; 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 f0149cfc9be..cc23e2fc50d 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 @@ -1,7 +1,7 @@ import { GraphWidgetChartContainer } from '@/page-layout/widgets/graph/components/GraphWidgetChartContainer'; 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 { CustomBarItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/components/CustomBarItem'; 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'; @@ -19,8 +19,8 @@ import { 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, useRef, useState } from 'react'; +import { ResponsiveBar } from '@nivo/bar'; +import { useMemo, useRef, useState } from 'react'; import { isDefined } from 'twenty-shared/utils'; const LEGEND_THRESHOLD = 10; @@ -76,7 +76,6 @@ export const GraphWidgetBarChart = ({ customFormatter, }: GraphWidgetBarChartProps) => { const theme = useTheme(); - const instanceId = useId(); const colorRegistry = createGraphColorRegistry(theme); const [chartWidth, setChartWidth] = useState(0); const [chartHeight, setChartHeight] = useState(0); @@ -98,17 +97,13 @@ export const GraphWidgetBarChart = ({ const chartTheme = useBarChartTheme(); - const { barConfigs, enrichedKeys, enrichedKeysMap, defs } = useBarChartData({ + const { barConfigs, enrichedKeys } = useBarChartData({ data, indexBy, keys, series, colorRegistry, - id, - instanceId, seriesLabels, - hoveredBar, - layout, }); const { renderTooltip: getTooltipData } = useBarChartTooltip({ @@ -152,22 +147,27 @@ export const GraphWidgetBarChart = ({ ); }; - const barEndLinesLayer = (props: BarCustomLayerProps) => { - return ( - (props: any) => ( + - ); - }; + ), + [keys, groupMode, data, indexBy, layout], + ); return ( getBarChartColor(datum, barConfigs, theme)} - defs={defs} - layers={[ - 'grid', - 'axes', - 'bars', - barEndLinesLayer, - 'markers', - 'legends', - ]} + layers={['grid', 'axes', 'bars', 'markers', 'legends']} axisTop={null} axisRight={null} axisBottom={axisBottomConfig} @@ -226,6 +219,7 @@ export const GraphWidgetBarChart = ({ }} onMouseLeave={() => setHoveredBar(null)} theme={chartTheme} + borderRadius={parseInt(theme.border.radius.sm)} /> { keys: ['sales', 'costs'], series: mockSeries, colorRegistry: mockColorRegistry, - id: 'test-chart', - instanceId: 'instance-1', - hoveredBar: null, - layout: 'vertical', }), ); @@ -66,10 +62,6 @@ describe('useBarChartData', () => { keys: ['sales', 'costs'], series: mockSeries, colorRegistry: mockColorRegistry, - id: 'test-chart', - instanceId: 'instance-1', - hoveredBar: null, - layout: 'vertical', }), ); @@ -77,14 +69,18 @@ describe('useBarChartData', () => { expect(result.current.barConfigs[0]).toMatchObject({ key: 'sales', indexValue: 'Jan', - gradientId: 'gradient-test-chart-instance-1-sales-0-0', - colorScheme: mockColorRegistry.green, + colorScheme: { + name: 'green', + gradient: mockColorRegistry.green.gradient, + }, }); expect(result.current.barConfigs[1]).toMatchObject({ key: 'costs', indexValue: 'Jan', - gradientId: 'gradient-test-chart-instance-1-costs-0-1', - colorScheme: mockColorRegistry.purple, + colorScheme: { + name: 'purple', + gradient: mockColorRegistry.purple.gradient, + }, }); }); @@ -96,25 +92,28 @@ describe('useBarChartData', () => { keys: ['sales', 'costs'], series: mockSeries, colorRegistry: mockColorRegistry, - id: 'test-chart', - instanceId: 'instance-1', - hoveredBar: null, - layout: 'vertical', }), ); - expect(result.current.enrichedKeys).toEqual([ - { - key: 'sales', - colorScheme: mockColorRegistry.green, - label: 'Sales', + expect(result.current.enrichedKeys).toHaveLength(2); + expect(result.current.enrichedKeys[0]).toMatchObject({ + key: 'sales', + label: 'Sales', + colorScheme: { + name: 'green', + gradient: mockColorRegistry.green.gradient, }, - { - key: 'costs', - colorScheme: mockColorRegistry.purple, - label: 'Costs', + }); + expect(result.current.enrichedKeys[0].colorScheme.solid).toBeDefined(); + expect(result.current.enrichedKeys[1]).toMatchObject({ + key: 'costs', + label: 'Costs', + colorScheme: { + name: 'purple', + gradient: mockColorRegistry.purple.gradient, }, - ]); + }); + expect(result.current.enrichedKeys[1].colorScheme.solid).toBeDefined(); }); it('should use series labels when series config is not provided', () => { @@ -125,11 +124,7 @@ describe('useBarChartData', () => { keys: ['sales', 'costs'], series: undefined, colorRegistry: mockColorRegistry, - id: 'test-chart', - instanceId: 'instance-1', seriesLabels: { sales: 'Revenue', costs: 'Expenses' }, - hoveredBar: null, - layout: 'vertical', }), ); @@ -137,70 +132,6 @@ describe('useBarChartData', () => { expect(result.current.enrichedKeys[1].label).toBe('Expenses'); }); - it('should handle hover state for bars', () => { - const { result } = renderHook(() => - useBarChartData({ - data: mockData, - indexBy: 'month', - keys: ['sales', 'costs'], - series: mockSeries, - colorRegistry: mockColorRegistry, - id: 'test-chart', - instanceId: 'instance-1', - hoveredBar: { key: 'sales', indexValue: 'Feb' }, - layout: 'vertical', - }), - ); - - const hoveredDef = result.current.defs.find( - (def) => def.id === 'gradient-test-chart-instance-1-sales-1-0', - ); - expect(hoveredDef?.colors).toEqual([ - { offset: 0, color: 'green4' }, - { offset: 100, color: 'green3' }, - ]); - }); - - it('should generate vertical gradients for vertical layout', () => { - const { result } = renderHook(() => - useBarChartData({ - data: mockData, - indexBy: 'month', - keys: ['sales'], - series: mockSeries, - colorRegistry: mockColorRegistry, - id: 'test-chart', - instanceId: 'instance-1', - hoveredBar: null, - layout: 'vertical', - }), - ); - - const def = result.current.defs[0]; - expect(def.y1).toBe('0%'); - expect(def.y2).toBe('100%'); - }); - - it('should generate horizontal gradients for horizontal layout', () => { - const { result } = renderHook(() => - useBarChartData({ - data: mockData, - indexBy: 'month', - keys: ['sales'], - series: mockSeries, - colorRegistry: mockColorRegistry, - id: 'test-chart', - instanceId: 'instance-1', - hoveredBar: null, - layout: 'horizontal', - }), - ); - - const def = result.current.defs[0]; - expect(def.x1).toBe('0%'); - expect(def.x2).toBe('100%'); - }); - it('should handle empty data', () => { const { result } = renderHook(() => useBarChartData({ @@ -209,15 +140,10 @@ describe('useBarChartData', () => { keys: ['sales', 'costs'], series: mockSeries, colorRegistry: mockColorRegistry, - id: 'test-chart', - instanceId: 'instance-1', - hoveredBar: null, - layout: 'vertical', }), ); expect(result.current.barConfigs).toEqual([]); - expect(result.current.defs).toEqual([]); }); it('should handle empty keys', () => { @@ -228,16 +154,11 @@ describe('useBarChartData', () => { keys: [], series: mockSeries, colorRegistry: mockColorRegistry, - id: 'test-chart', - instanceId: 'instance-1', - hoveredBar: null, - layout: 'vertical', }), ); expect(result.current.barConfigs).toEqual([]); expect(result.current.enrichedKeys).toEqual([]); - expect(result.current.defs).toEqual([]); }); it('should fall back to key name when no label is provided', () => { @@ -248,40 +169,11 @@ describe('useBarChartData', () => { keys: ['sales', 'costs'], series: undefined, colorRegistry: mockColorRegistry, - id: 'test-chart', - instanceId: 'instance-1', seriesLabels: undefined, - hoveredBar: null, - layout: 'vertical', }), ); expect(result.current.enrichedKeys[0].label).toBe('sales'); expect(result.current.enrichedKeys[1].label).toBe('costs'); }); - - it('should recalculate when instanceId changes', () => { - const { result, rerender } = renderHook( - ({ instanceId }) => - useBarChartData({ - data: mockData, - indexBy: 'month', - keys: ['sales', 'costs'], - series: mockSeries, - colorRegistry: mockColorRegistry, - id: 'test-chart', - instanceId, - hoveredBar: null, - layout: 'vertical', - }), - { initialProps: { instanceId: 'instance-1' } }, - ); - - const firstBarConfigs = result.current.barConfigs; - - rerender({ instanceId: 'instance-2' }); - - expect(result.current.barConfigs).not.toBe(firstBarConfigs); - expect(result.current.barConfigs[0].gradientId).toContain('instance-2'); - }); }); 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 a39328b70a3..8d5255fb25f 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 @@ -3,7 +3,6 @@ import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBa import { type BarChartEnrichedKey } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartEnrichedKey'; import { type BarChartSeries } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartSeries'; import { type GraphColorRegistry } from '@/page-layout/widgets/graph/types/GraphColorRegistry'; -import { createGradientDef } from '@/page-layout/widgets/graph/utils/createGradientDef'; import { getColorScheme } from '@/page-layout/widgets/graph/utils/getColorScheme'; import { useMemo } from 'react'; @@ -13,11 +12,8 @@ type UseBarChartDataProps = { keys: string[]; series?: BarChartSeries[]; colorRegistry: GraphColorRegistry; - id: string; - instanceId: string; seriesLabels?: Record; - hoveredBar: { key: string; indexValue: string | number } | null; - layout: 'vertical' | 'horizontal'; + groupMode?: 'grouped' | 'stacked'; }; export const useBarChartData = ({ @@ -26,11 +22,7 @@ export const useBarChartData = ({ keys, series, colorRegistry, - id, - instanceId, seriesLabels, - hoveredBar, - layout, }: UseBarChartDataProps) => { const seriesConfigMap = useMemo( () => new Map(series?.map((s) => [s.key, s]) || []), @@ -38,35 +30,35 @@ export const useBarChartData = ({ ); const barConfigs = useMemo((): BarChartConfig[] => { - return data.flatMap((dataPoint, dataIndex) => { + return data.flatMap((dataPoint) => { const indexValue = dataPoint[indexBy]; return keys.map((key, keyIndex): BarChartConfig => { const seriesConfig = seriesConfigMap.get(key); - const colorScheme = getColorScheme( - colorRegistry, - seriesConfig?.color, - keyIndex, - ); - const sanitizedKey = key.replace(/\s+/g, '-'); - const gradientId = `gradient-${id}-${instanceId}-${sanitizedKey}-${dataIndex}-${keyIndex}`; + const colorScheme = getColorScheme({ + registry: colorRegistry, + colorName: seriesConfig?.color, + fallbackIndex: keyIndex, + totalGroups: keys.length, + }); return { key, indexValue, - gradientId, colorScheme, }; }); }); - }, [data, indexBy, keys, colorRegistry, id, instanceId, seriesConfigMap]); + }, [data, indexBy, keys, colorRegistry, seriesConfigMap]); const enrichedKeys: BarChartEnrichedKey[] = keys.map((key, index) => { const seriesConfig = seriesConfigMap.get(key); - const colorScheme = getColorScheme( - colorRegistry, - seriesConfig?.color, - index, - ); + const colorScheme = getColorScheme({ + registry: colorRegistry, + colorName: seriesConfig?.color, + fallbackIndex: index, + totalGroups: keys.length, + }); + return { key, colorScheme, @@ -74,28 +66,9 @@ export const useBarChartData = ({ }; }); - const enrichedKeysMap = useMemo( - () => new Map(enrichedKeys.map((item) => [item.key, item])), - [enrichedKeys], - ); - - const defs = barConfigs.map((bar) => { - const isHovered = - hoveredBar?.key === bar.key && hoveredBar?.indexValue === bar.indexValue; - return createGradientDef( - bar.colorScheme, - bar.gradientId, - isHovered, - layout === 'horizontal' ? 0 : 90, - true, - ); - }); - return { seriesConfigMap, barConfigs, enrichedKeys, - enrichedKeysMap, - defs, }; }; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartConfig.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartConfig.ts index 784b78204c1..20425fbfa5a 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartConfig.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartConfig.ts @@ -3,6 +3,5 @@ import { type GraphColorScheme } from '@/page-layout/widgets/graph/types/GraphCo export type BarChartConfig = { key: string; indexValue: string | number; - gradientId: string; colorScheme: GraphColorScheme; }; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartColor.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartColor.ts index 5a1b0125a40..71ce1094b15 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartColor.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartColor.ts @@ -14,5 +14,5 @@ export const getBarChartColor = ( if (!isDefined(bar)) { return theme.border.color.light; } - return `url(#${bar.gradientId})`; + return bar.colorScheme.solid; }; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetGaugeChart/hooks/useGaugeChartData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetGaugeChart/hooks/useGaugeChartData.ts index 3e9ad6e03de..6f8946be5a4 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetGaugeChart/hooks/useGaugeChartData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetGaugeChart/hooks/useGaugeChartData.ts @@ -22,7 +22,11 @@ export const useGaugeChartData = ({ const { value, min, max, color = 'blue' } = data; const colorScheme = useMemo( - () => getColorScheme(colorRegistry, color), + () => + getColorScheme({ + registry: colorRegistry, + colorName: color, + }), [colorRegistry, color], ); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/hooks/__tests__/useLineChartData.test.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/hooks/__tests__/useLineChartData.test.ts index a4ce4a4fb30..fd4de091a1b 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/hooks/__tests__/useLineChartData.test.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/hooks/__tests__/useLineChartData.test.ts @@ -80,17 +80,11 @@ describe('useLineChartData', () => { }), ); - expect(result.current.enrichedSeries[0].colorScheme).toBe( - mockColorRegistry.red, - ); expect(result.current.enrichedSeries[0].gradientId).toBe( 'lineGradient-test-chart-instance-1-series1-0', ); expect(result.current.enrichedSeries[0].label).toBe('Sales'); - expect(result.current.enrichedSeries[1].colorScheme).toBe( - mockColorRegistry.blue, - ); expect(result.current.enrichedSeries[1].label).toBe('Costs'); }); @@ -268,22 +262,6 @@ describe('useLineChartData', () => { ]); }); - it('should extract colors for series', () => { - const { result } = renderHook(() => - useLineChartData({ - data: mockData, - colorRegistry: mockColorRegistry, - id: 'test-chart', - instanceId: 'instance-1', - enableArea: false, - theme: mockTheme, - formatOptions: mockFormatOptions, - }), - ); - - expect(result.current.colors).toEqual(['redSolid', 'blueSolid']); - }); - it('should calculate legend items with totals', () => { const { result } = renderHook(() => useLineChartData({ @@ -302,13 +280,13 @@ describe('useLineChartData', () => { id: 'series1', label: 'Sales', formattedValue: '370', - color: 'redSolid', + color: expect.any(String), }, { id: 'series2', label: 'Costs', formattedValue: '270', - color: 'blueSolid', + color: expect.any(String), }, ]); }); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/hooks/useLineChartData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/hooks/useLineChartData.ts index e312bebcb94..d11aff933ea 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/hooks/useLineChartData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/hooks/useLineChartData.ts @@ -34,7 +34,12 @@ export const useLineChartData = ({ const dataMap = Object.fromEntries(data.map((series) => [series.id, series])); const enrichedSeries = useMemo((): LineChartEnrichedSeries[] => { return data.map((series, index) => { - const colorScheme = getColorScheme(colorRegistry, series.color, index); + const colorScheme = getColorScheme({ + registry: colorRegistry, + colorName: series.color, + fallbackIndex: index, + totalGroups: data.length, + }); const shouldEnableArea = series.enableArea ?? enableArea; const gradientId = `lineGradient-${id}-${instanceId}-${series.id}-${index}`; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/hooks/usePieChartData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/hooks/usePieChartData.ts index 4092ef65d38..d1f1638eb42 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/hooks/usePieChartData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/hooks/usePieChartData.ts @@ -1,12 +1,12 @@ import { type PieChartDataItem } from '@/page-layout/widgets/graph/graphWidgetPieChart/types/PieChartDataItem'; import { type PieChartEnrichedData } from '@/page-layout/widgets/graph/graphWidgetPieChart/types/PieChartEnrichedData'; -import { useMemo } from 'react'; import { calculatePieChartAngles } from '@/page-layout/widgets/graph/graphWidgetPieChart/utils/calculatePieChartAngles'; import { calculatePieChartPercentage } from '@/page-layout/widgets/graph/graphWidgetPieChart/utils/calculatePieChartPercentage'; import { type GraphColorRegistry } from '@/page-layout/widgets/graph/types/GraphColorRegistry'; import { createGradientDef } from '@/page-layout/widgets/graph/utils/createGradientDef'; import { getColorScheme } from '@/page-layout/widgets/graph/utils/getColorScheme'; import { type DatumId } from '@nivo/pie'; +import { useMemo } from 'react'; type UsePieChartDataProps = { data: PieChartDataItem[]; @@ -26,7 +26,13 @@ export const usePieChartData = ({ let cumulativeAngle = 0; return data.map((item, index) => { - const colorScheme = getColorScheme(colorRegistry, item.color, index); + const colorScheme = getColorScheme({ + registry: colorRegistry, + colorName: item.color, + fallbackIndex: index, + totalGroups: data.length, + }); + const isHovered = hoveredSliceId === item.id; const gradientId = `${colorScheme.name}Gradient-${id}-${index}`; const percentage = calculatePieChartPercentage(item.value, totalValue); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/types/GraphColor.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/types/GraphColor.ts index 1c38c0acaf5..3d1b95a0a67 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/types/GraphColor.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/types/GraphColor.ts @@ -1,4 +1,5 @@ export type GraphColor = + | 'auto' | 'blue' | 'purple' | 'turquoise' diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/createGraphColorRegistry.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/createGraphColorRegistry.ts index 8d11745d13f..a232af28135 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/createGraphColorRegistry.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/createGraphColorRegistry.ts @@ -11,7 +11,7 @@ export const createGraphColorRegistry = ( normal: [theme.adaptiveColors.blue1, theme.adaptiveColors.blue2], hover: [theme.adaptiveColors.blue3, theme.adaptiveColors.blue4], }, - solid: theme.color.blue, + solid: theme.adaptiveColors.blue4, }, purple: { name: 'purple', @@ -19,7 +19,7 @@ export const createGraphColorRegistry = ( normal: [theme.adaptiveColors.purple1, theme.adaptiveColors.purple2], hover: [theme.adaptiveColors.purple3, theme.adaptiveColors.purple4], }, - solid: theme.color.purple, + solid: theme.adaptiveColors.purple4, }, turquoise: { name: 'turquoise', @@ -30,7 +30,7 @@ export const createGraphColorRegistry = ( ], hover: [theme.adaptiveColors.turquoise3, theme.adaptiveColors.turquoise4], }, - solid: theme.color.turquoise, + solid: theme.adaptiveColors.turquoise4, }, orange: { name: 'orange', @@ -38,7 +38,7 @@ export const createGraphColorRegistry = ( normal: [theme.adaptiveColors.orange1, theme.adaptiveColors.orange2], hover: [theme.adaptiveColors.orange3, theme.adaptiveColors.orange4], }, - solid: theme.color.orange, + solid: theme.adaptiveColors.orange4, }, pink: { name: 'pink', @@ -46,7 +46,7 @@ export const createGraphColorRegistry = ( normal: [theme.adaptiveColors.pink1, theme.adaptiveColors.pink2], hover: [theme.adaptiveColors.pink3, theme.adaptiveColors.pink4], }, - solid: theme.color.pink, + solid: theme.adaptiveColors.pink4, }, yellow: { name: 'yellow', @@ -54,7 +54,7 @@ export const createGraphColorRegistry = ( normal: [theme.adaptiveColors.yellow1, theme.adaptiveColors.yellow2], hover: [theme.adaptiveColors.yellow3, theme.adaptiveColors.yellow4], }, - solid: theme.color.yellow, + solid: theme.adaptiveColors.yellow4, }, red: { name: 'red', @@ -62,7 +62,7 @@ export const createGraphColorRegistry = ( normal: [theme.adaptiveColors.red1, theme.adaptiveColors.red2], hover: [theme.adaptiveColors.red3, theme.adaptiveColors.red4], }, - solid: theme.color.red, + solid: theme.adaptiveColors.red4, }, green: { name: 'green', @@ -70,7 +70,7 @@ export const createGraphColorRegistry = ( normal: [theme.adaptiveColors.green1, theme.adaptiveColors.green2], hover: [theme.adaptiveColors.green3, theme.adaptiveColors.green4], }, - solid: theme.color.green, + solid: theme.adaptiveColors.green4, }, sky: { name: 'sky', @@ -78,7 +78,7 @@ export const createGraphColorRegistry = ( normal: [theme.adaptiveColors.sky1, theme.adaptiveColors.sky2], hover: [theme.adaptiveColors.sky3, theme.adaptiveColors.sky4], }, - solid: theme.color.sky, + solid: theme.adaptiveColors.sky4, }, gray: { name: 'gray', @@ -86,6 +86,6 @@ export const createGraphColorRegistry = ( normal: [theme.adaptiveColors.gray1, theme.adaptiveColors.gray2], hover: [theme.adaptiveColors.gray3, theme.adaptiveColors.gray4], }, - solid: theme.color.gray, + solid: theme.adaptiveColors.gray4, }, }); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/generateGroupColor.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/generateGroupColor.ts new file mode 100644 index 00000000000..c3ec9874917 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/generateGroupColor.ts @@ -0,0 +1,32 @@ +import { RGBA } from 'twenty-ui/theme'; + +import { GRAPH_GROUP_COLOR_MINIMUM_ALPHA } from '@/page-layout/widgets/graph/constants/GraphGroupColorMinimumAlpha.constant'; +import { GRAPH_MAXIMUM_NUMBER_OF_GROUP_COLORS } from '@/page-layout/widgets/graph/constants/GraphMaximumNumberOfGroupColors'; +import { type GraphColorScheme } from '@/page-layout/widgets/graph/types/GraphColorScheme'; + +export const generateGroupColor = ({ + colorScheme, + groupIndex, + totalGroups, +}: { + colorScheme: GraphColorScheme; + groupIndex: number; + totalGroups: number; +}): string => { + if (totalGroups <= 1) { + return colorScheme.solid; + } + + const effectiveGroupIndex = groupIndex % GRAPH_MAXIMUM_NUMBER_OF_GROUP_COLORS; + const effectiveTotalGroups = Math.min( + totalGroups, + GRAPH_MAXIMUM_NUMBER_OF_GROUP_COLORS, + ); + + const ratio = (effectiveGroupIndex + 1) / effectiveTotalGroups; + const alpha = + GRAPH_GROUP_COLOR_MINIMUM_ALPHA + + (1 - GRAPH_GROUP_COLOR_MINIMUM_ALPHA) * ratio; + + return RGBA(colorScheme.solid, alpha); +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/getColorScheme.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/getColorScheme.ts index a98d7b47d27..00e7e03efc8 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/getColorScheme.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/getColorScheme.ts @@ -1,16 +1,35 @@ +import { generateGroupColor } from '@/page-layout/widgets/graph/utils/generateGroupColor'; +import { getColorSchemeByIndex } from '@/page-layout/widgets/graph/utils/getColorSchemeByIndex'; import { isDefined } from 'twenty-shared/utils'; import { type GraphColor } from '../types/GraphColor'; import { type GraphColorRegistry } from '../types/GraphColorRegistry'; import { type GraphColorScheme } from '../types/GraphColorScheme'; -import { getColorSchemeByIndex } from './getColorSchemeByIndex'; -export const getColorScheme = ( - registry: GraphColorRegistry, - colorName?: GraphColor, - fallbackIndex?: number, -): GraphColorScheme => { - if (isDefined(colorName) && isDefined(registry[colorName])) { +export const getColorScheme = ({ + registry, + colorName, + fallbackIndex, + totalGroups, +}: { + registry: GraphColorRegistry; + colorName?: GraphColor; + fallbackIndex?: number; + totalGroups?: number; +}): GraphColorScheme => { + if (!isDefined(colorName) || !isDefined(registry[colorName])) { + return getColorSchemeByIndex(registry, fallbackIndex ?? 0); + } + + if (!isDefined(totalGroups)) { return registry[colorName]; } - return getColorSchemeByIndex(registry, fallbackIndex || 0); + + return { + ...registry[colorName], + solid: generateGroupColor({ + colorScheme: registry[colorName], + groupIndex: fallbackIndex ?? 0, + totalGroups, + }), + }; }; 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 1bb0913f876..5547705194e 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 @@ -4,6 +4,7 @@ import { type ExtendedAggregateOperations } from '@/object-record/record-table/t 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'; import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult'; import { computeAggregateValueFromGroupByResult } from '@/page-layout/widgets/graph/utils/computeAggregateValueFromGroupByResult'; import { formatDimensionValue } from '@/page-layout/widgets/graph/utils/formatDimensionValue'; @@ -115,6 +116,7 @@ export const transformTwoDimensionalGroupByToBarChartData = ({ const series: BarChartSeries[] = keys.map((key) => ({ key, label: key, + color: configuration.color as GraphColor, })); return { diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/get-page-layout-widget-data-seeds.util.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/get-page-layout-widget-data-seeds.util.ts index 9c6ebdcad21..42ea10ff586 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/get-page-layout-widget-data-seeds.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/get-page-layout-widget-data-seeds.util.ts @@ -176,6 +176,7 @@ export const getPageLayoutWidgetDataSeeds = ( primaryAxisOrderBy: 'FIELD_ASC', axisNameDisplay: AxisNameDisplay.BOTH, displayDataLabel: false, + color: 'auto', } : null, objectMetadataId: opportunityObject?.id ?? null, @@ -204,6 +205,7 @@ export const getPageLayoutWidgetDataSeeds = ( primaryAxisOrderBy: 'FIELD_ASC', axisNameDisplay: AxisNameDisplay.NONE, displayDataLabel: false, + color: 'auto', } : null, objectMetadataId: rocketObject?.id ?? null, @@ -306,6 +308,7 @@ export const getPageLayoutWidgetDataSeeds = ( primaryAxisOrderBy: 'FIELD_ASC', axisNameDisplay: AxisNameDisplay.BOTH, displayDataLabel: false, + color: 'auto', } : null, objectMetadataId: companyObject?.id ?? null, @@ -471,6 +474,7 @@ export const getPageLayoutWidgetDataSeeds = ( primaryAxisOrderBy: 'VALUE_DESC', axisNameDisplay: AxisNameDisplay.NONE, displayDataLabel: false, + color: 'auto', } : null, objectMetadataId: personObject?.id ?? null, diff --git a/packages/twenty-ui/src/display/color/components/ColorSample.tsx b/packages/twenty-ui/src/display/color/components/ColorSample.tsx index be63b7e45c1..e693f19b331 100644 --- a/packages/twenty-ui/src/display/color/components/ColorSample.tsx +++ b/packages/twenty-ui/src/display/color/components/ColorSample.tsx @@ -1,24 +1,36 @@ import { css } from '@emotion/react'; import styled from '@emotion/styled'; -import { type ThemeColor } from '@ui/theme'; +import { type ThemeColor, type ThemeType } from '@ui/theme'; +import { isDefined } from 'twenty-shared/utils'; export type ColorSampleVariant = 'default' | 'pipeline'; export type ColorSampleProps = { - colorName: ThemeColor; + colorName?: ThemeColor; + color?: string; variant?: ColorSampleVariant; }; +const getColor = (theme: ThemeType, colorName?: ThemeColor, color?: string) => { + if (isDefined(color)) { + return color; + } + if (isDefined(colorName)) { + return theme.tag.background[colorName]; + } + return 'transparent'; +}; + const StyledColorSample = styled.div` - background-color: ${({ theme, colorName }) => - theme.tag.background[colorName]}; - border: 1px solid ${({ theme, colorName }) => theme.tag.text[colorName]}; + background-color: ${({ theme, colorName, color }) => + getColor(theme, colorName, color)}; + border: 1px solid ${({ theme }) => theme.border.color.transparentStrong}; border-radius: 60px; height: ${({ theme }) => theme.spacing(4)}; width: ${({ theme }) => theme.spacing(3)}; - ${({ colorName, theme, variant }) => { + ${({ colorName, color, theme, variant }) => { if (variant === 'pipeline') return css` align-items: center; @@ -27,7 +39,7 @@ const StyledColorSample = styled.div` justify-content: center; &:after { - background-color: ${theme.tag.text[colorName]}; + background-color: ${getColor(theme, colorName, color)}; border-radius: ${theme.border.radius.rounded}; content: ''; display: block; diff --git a/packages/twenty-ui/src/theme/constants/BorderDark.ts b/packages/twenty-ui/src/theme/constants/BorderDark.ts index fedc96841a0..af94312799e 100644 --- a/packages/twenty-ui/src/theme/constants/BorderDark.ts +++ b/packages/twenty-ui/src/theme/constants/BorderDark.ts @@ -1,3 +1,4 @@ +import { RGBA } from '@ui/theme/constants/Rgba'; import { BORDER_COMMON } from './BorderCommon'; import { COLOR } from './Colors'; import { GRAY_SCALE } from './GrayScale'; @@ -11,6 +12,7 @@ export const BORDER_DARK = { inverted: GRAY_SCALE.gray20, danger: COLOR.red70, blue: COLOR.blue30, + transparentStrong: RGBA(GRAY_SCALE.gray100, 0.16), }, ...BORDER_COMMON, }; diff --git a/packages/twenty-ui/src/theme/constants/BorderLight.ts b/packages/twenty-ui/src/theme/constants/BorderLight.ts index 0096c21c6c0..916c5002fd0 100644 --- a/packages/twenty-ui/src/theme/constants/BorderLight.ts +++ b/packages/twenty-ui/src/theme/constants/BorderLight.ts @@ -1,3 +1,4 @@ +import { RGBA } from '@ui/theme/constants/Rgba'; import { BORDER_COMMON } from './BorderCommon'; import { COLOR } from './Colors'; import { GRAY_SCALE } from './GrayScale'; @@ -11,6 +12,7 @@ export const BORDER_LIGHT = { inverted: GRAY_SCALE.gray60, danger: COLOR.red20, blue: COLOR.blue30, + transparentStrong: RGBA(GRAY_SCALE.gray100, 0.16), }, ...BORDER_COMMON, };