From d1cb046ebba8667d506d293c5b83bf45cd5f8d17 Mon Sep 17 00:00:00 2001 From: nitin <142569587+ehconitin@users.noreply.github.com> Date: Mon, 15 Sep 2025 17:52:40 +0530 Subject: [PATCH] [Dashboards] - refactor - bar chart (#14496) This is the first PR from the split of #14458 - refactoring only the BarChart widget. --- .../graph/components/GraphWidgetBarChart.tsx | 444 ------------------ .../graph/components/GraphWidgetRenderer.tsx | 10 +- .../GraphWidgetBarChart.stories.tsx | 2 +- .../components/BarChartEndLines.tsx | 45 ++ .../components/GraphWidgetBarChart.tsx | 227 +++++++++ .../hooks/__tests__/useBarChartData.test.ts | 287 +++++++++++ .../hooks/useBarChartData.ts | 93 ++++ .../hooks/useBarChartHandlers.ts | 35 ++ .../hooks/useBarChartTheme.ts | 46 ++ .../hooks/useBarChartTooltip.ts | 47 ++ .../types/BarChartConfig.ts | 8 + .../types/BarChartDataItem.ts | 5 + .../types/BarChartEnrichedKey.ts | 7 + .../types/BarChartSeries.ts | 7 + ...alculateBarChartEndLineCoordinates.test.ts | 96 ++++ .../calculateBarChartEndLineCoordinates.ts | 22 + .../utils/getBarChartAxisBottomConfig.ts | 30 ++ .../utils/getBarChartAxisLeftConfig.ts | 30 ++ .../utils/getBarChartColor.ts | 18 + 19 files changed, 1009 insertions(+), 450 deletions(-) delete mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidgetBarChart.tsx create 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/GraphWidgetBarChart.tsx create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/hooks/__tests__/useBarChartData.test.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/hooks/useBarChartData.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/hooks/useBarChartHandlers.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/hooks/useBarChartTheme.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/hooks/useBarChartTooltip.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartConfig.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartEnrichedKey.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartSeries.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/__tests__/calculateBarChartEndLineCoordinates.test.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/calculateBarChartEndLineCoordinates.ts create 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/getBarChartAxisLeftConfig.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartColor.ts diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidgetBarChart.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidgetBarChart.tsx deleted file mode 100644 index 0a0acc46d9..0000000000 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidgetBarChart.tsx +++ /dev/null @@ -1,444 +0,0 @@ -import styled from '@emotion/styled'; -import { - ResponsiveBar, - type BarCustomLayerProps, - type BarDatum, - type ComputedBarDatum, - type ComputedDatum, -} from '@nivo/bar'; -import { useId, useMemo, useState } from 'react'; -import { isDefined } from 'twenty-shared/utils'; - -import { useTheme } from '@emotion/react'; -import { type GraphColor } from '../types/GraphColor'; -import { type GraphColorScheme } from '../types/GraphColorScheme'; -import { createGradientDef } from '../utils/createGradientDef'; -import { createGraphColorRegistry } from '../utils/createGraphColorRegistry'; -import { getColorScheme } from '../utils/getColorScheme'; -import { - formatGraphValue, - type GraphValueFormatOptions, -} from '../utils/graphFormatters'; -import { GraphWidgetLegend } from './GraphWidgetLegend'; -import { GraphWidgetTooltip } from './GraphWidgetTooltip'; - -type BarChartDataItem = BarDatum & { - to?: string; -}; - -type BarChartSeries = { - key: string; - label?: string; - color?: GraphColor; -}; - -type BarConfig = { - key: string; - indexValue: string | number; - gradientId: string; - colorScheme: GraphColorScheme; -}; - -type GraphWidgetBarChartProps = { - data: BarChartDataItem[]; - indexBy: string; - keys: string[]; - series?: BarChartSeries[]; - showLegend?: boolean; - showGrid?: boolean; - showValues?: boolean; - xAxisLabel?: string; - yAxisLabel?: string; - id: string; - layout?: 'vertical' | 'horizontal'; - groupMode?: 'grouped' | 'stacked'; - seriesLabels?: Record; -} & GraphValueFormatOptions; - -const StyledContainer = styled.div` - align-items: center; - display: flex; - flex-direction: column; - height: 100%; - justify-content: center; - width: 100%; -`; - -const StyledChartContainer = styled.div<{ $isClickable?: boolean }>` - flex: 1; - position: relative; - width: 100%; - - ${({ $isClickable }) => - $isClickable && - ` - svg g[transform] rect[fill^="url(#gradient-"] { - cursor: pointer; - } - `} -`; - -const getAxisBottomConfig = ( - 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 || {}), - }; -}; - -const getAxisLeftConfig = ( - 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, - }; -}; - -const calculateBarEndLineCoordinates = ( - bar: ComputedBarDatum, - layout: 'vertical' | 'horizontal', -) => { - if (layout === 'vertical') { - return { - x1: bar.x, - x2: bar.x + bar.width, - y1: bar.y, - y2: bar.y, - }; - } else { - return { - x1: bar.x + bar.width, - x2: bar.x + bar.width, - y1: bar.y, - y2: bar.y + bar.height, - }; - } -}; - -export const GraphWidgetBarChart = ({ - data, - indexBy, - keys, - series, - showLegend = true, - showGrid = true, - showValues = false, - xAxisLabel, - yAxisLabel, - id, - layout = 'vertical', - groupMode = 'grouped', - seriesLabels, - displayType, - decimals, - prefix, - suffix, - customFormatter, -}: GraphWidgetBarChartProps) => { - const theme = useTheme(); - const instanceId = useId(); - const [hoveredBar, setHoveredBar] = useState<{ - key: string; - indexValue: string | number; - } | null>(null); - - const colorRegistry = createGraphColorRegistry(theme); - - const formatOptions: GraphValueFormatOptions = { - displayType, - decimals, - prefix, - suffix, - customFormatter, - }; - - const seriesConfigMap = useMemo(() => { - const map = new Map(); - series?.forEach((s) => map.set(s.key, s)); - return map; - }, [series]); - - const barConfigs = useMemo((): BarConfig[] => { - return data.flatMap((dataPoint, dataIndex) => { - const indexValue = dataPoint[indexBy]; - return keys.map((key, keyIndex): BarConfig => { - const seriesConfig = seriesConfigMap.get(key); - const colorScheme = getColorScheme( - colorRegistry, - seriesConfig?.color, - keyIndex, - ); - const gradientId = `gradient-${id}-${instanceId}-${key}-${dataIndex}-${keyIndex}`; - - return { - key, - indexValue, - gradientId, - colorScheme, - }; - }); - }); - }, [data, indexBy, keys, colorRegistry, id, instanceId, seriesConfigMap]); - - const enrichedKeys = useMemo(() => { - return keys.map((key, index) => { - const seriesConfig = seriesConfigMap.get(key); - const colorScheme = getColorScheme( - colorRegistry, - seriesConfig?.color, - index, - ); - return { - key, - colorScheme, - label: seriesConfig?.label || seriesLabels?.[key] || key, - }; - }); - }, [keys, colorRegistry, seriesConfigMap, seriesLabels]); - - 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, - ); - }); - - const getBarColor = (datum: ComputedDatum) => { - const bar = barConfigs.find( - (b) => b.key === datum.id && b.indexValue === datum.indexValue, - ); - if (!bar) { - return theme.border.color.light; - } - return `url(#${bar.gradientId})`; - }; - - const handleBarClick = (datum: ComputedDatum) => { - const dataItem = data.find((d) => d[indexBy] === datum.indexValue); - if (isDefined(dataItem?.to)) { - window.location.href = dataItem.to; - } - }; - - const renderTooltip = (datum: ComputedDatum) => { - const hoveredKey = hoveredBar?.key; - if (!isDefined(hoveredKey)) return null; - - const enrichedKey = enrichedKeys.find((item) => item.key === hoveredKey); - if (!enrichedKey) return null; - - const dataItem = data.find((d) => d[indexBy] === datum.indexValue); - const seriesValue = Number(datum.data[hoveredKey] || 0); - const tooltipItem = { - label: enrichedKey.label, - formattedValue: formatGraphValue(seriesValue, formatOptions), - dotColor: enrichedKey.colorScheme.solid, - }; - - return ( - - ); - }; - - const axisBottomConfig = getAxisBottomConfig( - layout, - xAxisLabel, - yAxisLabel, - formatOptions, - ); - const axisLeftConfig = getAxisLeftConfig( - layout, - xAxisLabel, - yAxisLabel, - formatOptions, - ); - - const hasClickableItems = data.some((item) => isDefined(item.to)); - - const renderBarEndLines = (props: BarCustomLayerProps) => { - const { bars } = props; - - if (!bars || bars.length === 0) { - return null; - } - - return ( - - {bars.map((bar: ComputedBarDatum, index: number) => { - const enrichedKey = enrichedKeys.find((k) => k.key === bar.data.id); - if (!enrichedKey) { - return null; - } - const lineColor = enrichedKey.colorScheme.solid; - const { x1, y1, x2, y2 } = calculateBarEndLineCoordinates( - bar, - layout, - ); - - return ( - - ); - })} - - ); - }; - - return ( - - - formatGraphValue(Number(d.value), formatOptions)} - tooltip={(props) => renderTooltip(props)} - onClick={handleBarClick} - onMouseEnter={(datum) => { - if (isDefined(datum.id) && isDefined(datum.indexValue)) { - setHoveredBar({ - key: String(datum.id), - indexValue: datum.indexValue, - }); - } - }} - onMouseLeave={() => setHoveredBar(null)} - theme={{ - axis: { - domain: { - line: { - stroke: theme.border.color.light, - strokeWidth: 1, - }, - }, - ticks: { - line: { - stroke: theme.border.color.light, - strokeWidth: 1, - }, - text: { - fill: theme.font.color.secondary, - fontSize: 11, - }, - }, - legend: { - text: { - fill: theme.font.color.light, - fontSize: 12, - fontWeight: theme.font.weight.medium, - }, - }, - }, - grid: { - line: { - stroke: theme.border.color.light, - strokeWidth: 1, - strokeDasharray: '4 4', - }, - }, - labels: { - text: { - fontSize: 11, - fontWeight: theme.font.weight.medium, - }, - }, - }} - /> - - { - const total = data.reduce( - (sum, d) => sum + Number(d[item.key] || 0), - 0, - ); - return { - id: item.key, - label: item.label, - formattedValue: formatGraphValue(total, formatOptions), - color: item.colorScheme.solid, - }; - })} - /> - - ); -}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidgetRenderer.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidgetRenderer.tsx index 1b3bd37e2d..5189024f8f 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidgetRenderer.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidgetRenderer.tsx @@ -6,11 +6,11 @@ import { type GraphWidget } from '@/page-layout/widgets/graph/types/GraphWidget' import { lazy, Suspense } from 'react'; const GraphWidgetBarChart = lazy(() => - import('@/page-layout/widgets/graph/components/GraphWidgetBarChart').then( - (module) => ({ - default: module.GraphWidgetBarChart, - }), - ), + import( + '@/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChart' + ).then((module) => ({ + default: module.GraphWidgetBarChart, + })), ); const GraphWidgetLineChart = lazy(() => diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/__stories__/GraphWidgetBarChart.stories.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/__stories__/GraphWidgetBarChart.stories.tsx index 8b6fa42c90..f40c9f4e8c 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/__stories__/GraphWidgetBarChart.stories.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/__stories__/GraphWidgetBarChart.stories.tsx @@ -1,7 +1,7 @@ import { type Meta, type StoryObj } from '@storybook/react'; import { CatalogDecorator, ComponentDecorator } from 'twenty-ui/testing'; -import { GraphWidgetBarChart } from '../GraphWidgetBarChart'; +import { GraphWidgetBarChart } from '../../graphWidgetBarChart/components/GraphWidgetBarChart'; const meta: Meta = { title: 'Modules/PageLayout/Widgets/GraphWidgetBarChart', 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 new file mode 100644 index 0000000000..4369451eff --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/BarChartEndLines.tsx @@ -0,0 +1,45 @@ +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[]; + enrichedKeys: BarChartEnrichedKey[]; + layout: 'vertical' | 'horizontal'; +}; + +export const BarChartEndLines = ({ + bars, + enrichedKeys, + layout, +}: BarChartEndLinesProps) => { + return ( + + {bars.map((bar: ComputedBarDatum, index: number) => { + const enrichedKey = enrichedKeys.find((k) => k.key === 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/GraphWidgetBarChart.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChart.tsx new file mode 100644 index 0000000000..1c858d2dcd --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChart.tsx @@ -0,0 +1,227 @@ +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 { 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 { 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 { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { ResponsiveBar, type BarCustomLayerProps } from '@nivo/bar'; +import { useId } from 'react'; +import { isDefined } from 'twenty-shared/utils'; + +type GraphWidgetBarChartProps = { + data: BarChartDataItem[]; + indexBy: string; + keys: string[]; + series?: BarChartSeries[]; + showLegend?: boolean; + showGrid?: boolean; + showValues?: boolean; + xAxisLabel?: string; + yAxisLabel?: string; + id: string; + layout?: 'vertical' | 'horizontal'; + groupMode?: 'grouped' | 'stacked'; + seriesLabels?: Record; +} & GraphValueFormatOptions; + +const StyledContainer = styled.div` + align-items: center; + display: flex; + flex-direction: column; + height: 100%; + justify-content: center; + width: 100%; +`; + +const StyledChartContainer = styled.div<{ $isClickable?: boolean }>` + flex: 1; + position: relative; + width: 100%; + + ${({ $isClickable }) => + $isClickable && + ` + svg g[transform] rect[fill^="url(#gradient-"] { + cursor: pointer; + } + `} +`; + +export const GraphWidgetBarChart = ({ + data, + indexBy, + keys, + series, + showLegend = true, + showGrid = true, + showValues = false, + xAxisLabel, + yAxisLabel, + id, + layout = 'vertical', + groupMode = 'grouped', + seriesLabels, + displayType, + decimals, + prefix, + suffix, + customFormatter, +}: GraphWidgetBarChartProps) => { + const theme = useTheme(); + const instanceId = useId(); + const colorRegistry = createGraphColorRegistry(theme); + + const formatOptions: GraphValueFormatOptions = { + displayType, + decimals, + prefix, + suffix, + customFormatter, + }; + + const { hoveredBar, setHoveredBar, handleBarClick, hasClickableItems } = + useBarChartHandlers({ + data, + indexBy, + }); + + const chartTheme = useBarChartTheme(); + + const { barConfigs, enrichedKeys, defs } = useBarChartData({ + data, + indexBy, + keys, + series, + colorRegistry, + id, + instanceId, + seriesLabels, + hoveredBar, + layout, + }); + + const { renderTooltip: getTooltipData } = useBarChartTooltip({ + hoveredBar, + enrichedKeys, + data, + indexBy, + formatOptions, + }); + + const axisBottomConfig = getBarChartAxisBottomConfig( + layout, + xAxisLabel, + yAxisLabel, + formatOptions, + ); + + const axisLeftConfig = getBarChartAxisLeftConfig( + layout, + xAxisLabel, + yAxisLabel, + formatOptions, + ); + + const renderTooltip = (datum: Parameters[0]) => { + const tooltipData = getTooltipData(datum); + if (!isDefined(tooltipData)) return null; + + return ( + + ); + }; + + const barEndLinesLayer = (props: BarCustomLayerProps) => { + return ( + + ); + }; + + return ( + + + getBarChartColor(datum, barConfigs, theme)} + defs={defs} + layers={[ + 'grid', + 'axes', + 'bars', + barEndLinesLayer, + 'markers', + 'legends', + ]} + axisTop={null} + axisRight={null} + axisBottom={axisBottomConfig} + axisLeft={axisLeftConfig} + enableGridX={layout === 'horizontal' && showGrid} + enableGridY={layout === 'vertical' && showGrid} + gridXValues={layout === 'horizontal' ? 5 : undefined} + gridYValues={layout === 'vertical' ? 5 : undefined} + enableLabel={showValues} + labelSkipWidth={12} + labelSkipHeight={12} + labelTextColor={theme.font.color.primary} + label={(d) => formatGraphValue(Number(d.value), formatOptions)} + tooltip={(props) => renderTooltip(props)} + onClick={handleBarClick} + onMouseEnter={(datum) => { + if (isDefined(datum.id) && isDefined(datum.indexValue)) { + setHoveredBar({ + key: String(datum.id), + indexValue: datum.indexValue, + }); + } + }} + onMouseLeave={() => setHoveredBar(null)} + theme={chartTheme} + /> + + { + const total = data.reduce( + (sum, d) => sum + Number(d[item.key] || 0), + 0, + ); + return { + id: item.key, + label: item.label, + formattedValue: formatGraphValue(total, formatOptions), + color: item.colorScheme.solid, + }; + })} + /> + + ); +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/hooks/__tests__/useBarChartData.test.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/hooks/__tests__/useBarChartData.test.ts new file mode 100644 index 0000000000..bf92d92855 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/hooks/__tests__/useBarChartData.test.ts @@ -0,0 +1,287 @@ +import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem'; +import { type BarChartSeries } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartSeries'; +import { type GraphColorRegistry } from '@/page-layout/widgets/graph/types/GraphColorRegistry'; +import { renderHook } from '@testing-library/react'; +import { useBarChartData } from '../useBarChartData'; + +describe('useBarChartData', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + const mockColorRegistry: GraphColorRegistry = { + green: { + name: 'green', + gradient: { + normal: ['green1', 'green2'], + hover: ['green3', 'green4'], + }, + solid: 'greenSolid', + }, + purple: { + name: 'purple', + gradient: { + normal: ['purple1', 'purple2'], + hover: ['purple3', 'purple4'], + }, + solid: 'purpleSolid', + }, + }; + + const mockData: BarChartDataItem[] = [ + { month: 'Jan', sales: 100, costs: 80 }, + { month: 'Feb', sales: 120, costs: 90 }, + { month: 'Mar', sales: 150, costs: 100 }, + ]; + + const mockSeries: BarChartSeries[] = [ + { key: 'sales', label: 'Sales', color: 'green' }, + { key: 'costs', label: 'Costs', color: 'purple' }, + ]; + + it('should create series config map', () => { + const { result } = renderHook(() => + useBarChartData({ + data: mockData, + indexBy: 'month', + keys: ['sales', 'costs'], + series: mockSeries, + colorRegistry: mockColorRegistry, + id: 'test-chart', + instanceId: 'instance-1', + hoveredBar: null, + layout: 'vertical', + }), + ); + + expect(result.current.seriesConfigMap.get('sales')).toEqual(mockSeries[0]); + expect(result.current.seriesConfigMap.get('costs')).toEqual(mockSeries[1]); + }); + + it('should generate bar configs for each data point and key', () => { + const { result } = renderHook(() => + useBarChartData({ + data: mockData, + indexBy: 'month', + keys: ['sales', 'costs'], + series: mockSeries, + colorRegistry: mockColorRegistry, + id: 'test-chart', + instanceId: 'instance-1', + hoveredBar: null, + layout: 'vertical', + }), + ); + + expect(result.current.barConfigs).toHaveLength(6); + expect(result.current.barConfigs[0]).toMatchObject({ + key: 'sales', + indexValue: 'Jan', + gradientId: 'gradient-test-chart-instance-1-sales-0-0', + colorScheme: mockColorRegistry.green, + }); + expect(result.current.barConfigs[1]).toMatchObject({ + key: 'costs', + indexValue: 'Jan', + gradientId: 'gradient-test-chart-instance-1-costs-0-1', + colorScheme: mockColorRegistry.purple, + }); + }); + + it('should create enriched keys with labels', () => { + const { result } = renderHook(() => + useBarChartData({ + data: mockData, + indexBy: 'month', + 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', + }, + { + key: 'costs', + colorScheme: mockColorRegistry.purple, + label: 'Costs', + }, + ]); + }); + + it('should use series labels when series config is not provided', () => { + const { result } = renderHook(() => + useBarChartData({ + data: mockData, + indexBy: 'month', + keys: ['sales', 'costs'], + series: undefined, + colorRegistry: mockColorRegistry, + id: 'test-chart', + instanceId: 'instance-1', + seriesLabels: { sales: 'Revenue', costs: 'Expenses' }, + hoveredBar: null, + layout: 'vertical', + }), + ); + + expect(result.current.enrichedKeys[0].label).toBe('Revenue'); + 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({ + data: [], + indexBy: 'month', + 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', () => { + const { result } = renderHook(() => + useBarChartData({ + data: mockData, + indexBy: 'month', + 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', () => { + const { result } = renderHook(() => + useBarChartData({ + data: mockData, + indexBy: 'month', + 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 new file mode 100644 index 0000000000..07be9a04ab --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/hooks/useBarChartData.ts @@ -0,0 +1,93 @@ +import { type BarChartConfig } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartConfig'; +import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem'; +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'; + +type UseBarChartDataProps = { + data: BarChartDataItem[]; + indexBy: string; + keys: string[]; + series?: BarChartSeries[]; + colorRegistry: GraphColorRegistry; + id: string; + instanceId: string; + seriesLabels?: Record; + hoveredBar: { key: string; indexValue: string | number } | null; + layout: 'vertical' | 'horizontal'; +}; + +export const useBarChartData = ({ + data, + indexBy, + keys, + series, + colorRegistry, + id, + instanceId, + seriesLabels, + hoveredBar, + layout, +}: UseBarChartDataProps) => { + const seriesConfigMap = useMemo( + () => new Map(series?.map((s) => [s.key, s]) || []), + [series], + ); + const barConfigs = useMemo((): BarChartConfig[] => { + return data.flatMap((dataPoint, dataIndex) => { + const indexValue = dataPoint[indexBy]; + return keys.map((key, keyIndex): BarChartConfig => { + const seriesConfig = seriesConfigMap.get(key); + const colorScheme = getColorScheme( + colorRegistry, + seriesConfig?.color, + keyIndex, + ); + const gradientId = `gradient-${id}-${instanceId}-${key}-${dataIndex}-${keyIndex}`; + + return { + key, + indexValue, + gradientId, + colorScheme, + }; + }); + }); + }, [data, indexBy, keys, colorRegistry, id, instanceId, seriesConfigMap]); + + const enrichedKeys: BarChartEnrichedKey[] = keys.map((key, index) => { + const seriesConfig = seriesConfigMap.get(key); + const colorScheme = getColorScheme( + colorRegistry, + seriesConfig?.color, + index, + ); + return { + key, + colorScheme, + label: seriesConfig?.label || seriesLabels?.[key] || key, + }; + }); + + 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, + defs, + }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/hooks/useBarChartHandlers.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/hooks/useBarChartHandlers.ts new file mode 100644 index 0000000000..66943982f3 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/hooks/useBarChartHandlers.ts @@ -0,0 +1,35 @@ +import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem'; +import { type BarDatum, type ComputedDatum } from '@nivo/bar'; +import { useState } from 'react'; +import { isDefined } from 'twenty-shared/utils'; + +type UseBarChartHandlersProps = { + data: BarChartDataItem[]; + indexBy: string; +}; + +export const useBarChartHandlers = ({ + data, + indexBy, +}: UseBarChartHandlersProps) => { + const [hoveredBar, setHoveredBar] = useState<{ + key: string; + indexValue: string | number; + } | null>(null); + + const handleBarClick = (datum: ComputedDatum) => { + const dataItem = data.find((d) => d[indexBy] === datum.indexValue); + if (isDefined(dataItem?.to)) { + window.location.href = dataItem.to; + } + }; + + const hasClickableItems = data.some((item) => isDefined(item.to)); + + return { + hoveredBar, + setHoveredBar, + handleBarClick, + hasClickableItems, + }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/hooks/useBarChartTheme.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/hooks/useBarChartTheme.ts new file mode 100644 index 0000000000..c13178ddf7 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/hooks/useBarChartTheme.ts @@ -0,0 +1,46 @@ +import { useTheme } from '@emotion/react'; + +export const useBarChartTheme = () => { + const theme = useTheme(); + + return { + axis: { + domain: { + line: { + stroke: theme.border.color.light, + strokeWidth: 1, + }, + }, + ticks: { + line: { + stroke: theme.border.color.light, + strokeWidth: 1, + }, + text: { + fill: theme.font.color.secondary, + fontSize: 11, + }, + }, + legend: { + text: { + fill: theme.font.color.light, + fontSize: 12, + fontWeight: theme.font.weight.medium, + }, + }, + }, + grid: { + line: { + stroke: theme.border.color.light, + strokeWidth: 1, + strokeDasharray: '4 4', + }, + }, + labels: { + text: { + fontSize: 11, + fontWeight: theme.font.weight.medium, + }, + }, + }; +}; 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 new file mode 100644 index 0000000000..56d24d1b2b --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/hooks/useBarChartTooltip.ts @@ -0,0 +1,47 @@ +import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem'; +import { type BarChartEnrichedKey } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartEnrichedKey'; +import { + formatGraphValue, + type GraphValueFormatOptions, +} from '@/page-layout/widgets/graph/utils/graphFormatters'; +import { type BarDatum, type ComputedDatum } from '@nivo/bar'; +import { isDefined } from 'twenty-shared/utils'; + +type UseBarChartTooltipProps = { + hoveredBar: { key: string; indexValue: string | number } | null; + enrichedKeys: BarChartEnrichedKey[]; + data: BarChartDataItem[]; + indexBy: string; + formatOptions: GraphValueFormatOptions; +}; + +export const useBarChartTooltip = ({ + hoveredBar, + enrichedKeys, + data, + indexBy, + formatOptions, +}: UseBarChartTooltipProps) => { + const renderTooltip = (datum: ComputedDatum) => { + const hoveredKey = hoveredBar?.key; + if (!isDefined(hoveredKey)) return null; + + const enrichedKey = enrichedKeys.find((item) => item.key === hoveredKey); + if (!enrichedKey) return null; + + const dataItem = data.find((d) => d[indexBy] === datum.indexValue); + const seriesValue = Number(datum.data[hoveredKey] || 0); + const tooltipItem = { + label: enrichedKey.label, + formattedValue: formatGraphValue(seriesValue, formatOptions), + dotColor: enrichedKey.colorScheme.solid, + }; + + return { + tooltipItem, + showClickHint: isDefined(dataItem?.to), + }; + }; + + return { renderTooltip }; +}; 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 new file mode 100644 index 0000000000..784b78204c --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartConfig.ts @@ -0,0 +1,8 @@ +import { type GraphColorScheme } from '@/page-layout/widgets/graph/types/GraphColorScheme'; + +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/types/BarChartDataItem.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem.ts new file mode 100644 index 0000000000..a85ee24b04 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem.ts @@ -0,0 +1,5 @@ +import { type BarDatum } from '@nivo/bar'; + +export type BarChartDataItem = BarDatum & { + to?: string; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartEnrichedKey.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartEnrichedKey.ts new file mode 100644 index 0000000000..6d9f653a04 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartEnrichedKey.ts @@ -0,0 +1,7 @@ +import { type GraphColorScheme } from '@/page-layout/widgets/graph/types/GraphColorScheme'; + +export type BarChartEnrichedKey = { + key: string; + colorScheme: GraphColorScheme; + label: string; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartSeries.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartSeries.ts new file mode 100644 index 0000000000..50acbf19a4 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartSeries.ts @@ -0,0 +1,7 @@ +import { type GraphColor } from '@/page-layout/widgets/graph/types/GraphColor'; + +export type BarChartSeries = { + key: string; + label?: string; + color?: GraphColor; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/__tests__/calculateBarChartEndLineCoordinates.test.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/__tests__/calculateBarChartEndLineCoordinates.test.ts new file mode 100644 index 0000000000..06002cedb4 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/__tests__/calculateBarChartEndLineCoordinates.test.ts @@ -0,0 +1,96 @@ +import { type ComputedBarDatum } from '@nivo/bar'; +import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem'; +import { calculateBarChartEndLineCoordinates } from '../calculateBarChartEndLineCoordinates'; +describe('calculateBarChartEndLineCoordinates', () => { + const createMockBar = ( + overrides?: Partial>, + ): ComputedBarDatum => + ({ + x: 100, + y: 50, + width: 40, + height: 80, + color: 'url(#gradient-test)', + data: { + id: 'sales', + value: 100, + index: 0, + indexValue: 'Q1', + data: { Q1: 100 } as BarChartDataItem, + formattedValue: '100', + hidden: false, + }, + label: 'Sales', + ...overrides, + }) as ComputedBarDatum; + describe('vertical layout', () => { + it('should calculate horizontal line coordinates at the top of vertical bars', () => { + const mockBar = createMockBar(); + const result = calculateBarChartEndLineCoordinates(mockBar, 'vertical'); + expect(result).toEqual({ + x1: 100, + x2: 140, + y1: 50, + y2: 50, + }); + }); + it('should handle bars at origin position', () => { + const barAtOrigin = createMockBar({ x: 0, y: 0 }); + const result = calculateBarChartEndLineCoordinates( + barAtOrigin, + 'vertical', + ); + expect(result).toEqual({ + x1: 0, + x2: 40, + y1: 0, + y2: 0, + }); + }); + it('should handle bars with negative positions', () => { + const negativeBar = createMockBar({ x: -50, y: -20 }); + const result = calculateBarChartEndLineCoordinates( + negativeBar, + 'vertical', + ); + expect(result).toEqual({ + x1: -50, + x2: -10, + y1: -20, + y2: -20, + }); + }); + }); + describe('horizontal layout', () => { + it('should calculate vertical line coordinates at the end of horizontal bars', () => { + const mockBar = createMockBar(); + const result = calculateBarChartEndLineCoordinates(mockBar, 'horizontal'); + expect(result).toEqual({ + x1: 140, + x2: 140, + y1: 50, + y2: 130, + }); + }); + it('should handle bars with different dimensions', () => { + const wideBar = createMockBar({ width: 100, height: 20 }); + const result = calculateBarChartEndLineCoordinates(wideBar, 'horizontal'); + expect(result).toEqual({ + x1: 200, + x2: 200, + y1: 50, + y2: 70, + }); + }); + it('should handle very thin bars', () => { + const thinBar = createMockBar({ width: 1, height: 200 }); + const result = calculateBarChartEndLineCoordinates(thinBar, 'horizontal'); + expect(result).toEqual({ + x1: 101, + x2: 101, + y1: 50, + y2: 250, + }); + }); + }); +}); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/calculateBarChartEndLineCoordinates.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/calculateBarChartEndLineCoordinates.ts new file mode 100644 index 0000000000..6cc8aefa1b --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/calculateBarChartEndLineCoordinates.ts @@ -0,0 +1,22 @@ +import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem'; +import { type ComputedBarDatum } from '@nivo/bar'; + +export const calculateBarChartEndLineCoordinates = ( + bar: ComputedBarDatum, + layout: 'vertical' | 'horizontal', +) => { + if (layout === 'vertical') { + return { + x1: bar.x, + x2: bar.x + bar.width, + y1: bar.y, + y2: bar.y, + }; + } + return { + x1: bar.x + bar.width, + x2: bar.x + bar.width, + y1: bar.y, + y2: bar.y + bar.height, + }; +}; 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 new file mode 100644 index 0000000000..e41616fa83 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartAxisBottomConfig.ts @@ -0,0 +1,30 @@ +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/getBarChartAxisLeftConfig.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartAxisLeftConfig.ts new file mode 100644 index 0000000000..751bd21b5a --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartAxisLeftConfig.ts @@ -0,0 +1,30 @@ +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/getBarChartColor.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartColor.ts new file mode 100644 index 0000000000..5a1b0125a4 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartColor.ts @@ -0,0 +1,18 @@ +import { type BarChartConfig } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartConfig'; +import { type BarDatum, type ComputedDatum } from '@nivo/bar'; +import { isDefined } from 'twenty-shared/utils'; +import { type ThemeType } from 'twenty-ui/theme'; + +export const getBarChartColor = ( + datum: ComputedDatum, + barConfigs: BarChartConfig[], + theme: ThemeType, +) => { + const bar = barConfigs.find( + (b) => b.key === datum.id && b.indexValue === datum.indexValue, + ); + if (!isDefined(bar)) { + return theme.border.color.light; + } + return `url(#${bar.gradientId})`; +};