diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/NoDataLayer.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/NoDataLayer.tsx
new file mode 100644
index 0000000000..0148028c19
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/NoDataLayer.tsx
@@ -0,0 +1,34 @@
+import { useTheme } from '@emotion/react';
+import { useLingui } from '@lingui/react/macro';
+
+type NoDataLayerProps = {
+ innerWidth: number;
+ innerHeight: number;
+ hasNoData: boolean;
+};
+
+export const NoDataLayer = ({
+ innerWidth,
+ innerHeight,
+ hasNoData,
+}: NoDataLayerProps) => {
+ const theme = useTheme();
+ const { t } = useLingui();
+
+ if (!hasNoData) {
+ return null;
+ }
+
+ return (
+
+ {t`No data`}
+
+ );
+};
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 e2b1161532..642831098a 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
@@ -3,12 +3,14 @@ import { type Meta, type StoryObj } from '@storybook/react';
import { GraphWidgetTestWrapper } from '@/page-layout/widgets/graph/__tests__/GraphWidgetTestWrapper';
import { GraphWidgetBarChart } from '@/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChart';
import { BarChartLayout } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartLayout';
+import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { CatalogDecorator, ComponentDecorator } from 'twenty-ui/testing';
const meta: Meta = {
title: 'Modules/PageLayout/Widgets/GraphWidgetBarChart',
component: GraphWidgetBarChart,
decorators: [
+ I18nFrontDecorator,
(Story) => (
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/__stories__/GraphWidgetLineChart.stories.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/__stories__/GraphWidgetLineChart.stories.tsx
index b2dbb7ff44..6f0065346f 100644
--- a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/__stories__/GraphWidgetLineChart.stories.tsx
+++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/__stories__/GraphWidgetLineChart.stories.tsx
@@ -3,12 +3,14 @@ import { type ComponentProps } from 'react';
import { GraphWidgetTestWrapper } from '@/page-layout/widgets/graph/__tests__/GraphWidgetTestWrapper';
import { GraphWidgetLineChart } from '@/page-layout/widgets/graph/graphWidgetLineChart/components/GraphWidgetLineChart';
+import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { CatalogDecorator, ComponentDecorator } from 'twenty-ui/testing';
const meta: Meta = {
title: 'Modules/PageLayout/Widgets/GraphWidgetLineChart',
component: GraphWidgetLineChart,
decorators: [
+ I18nFrontDecorator,
(Story) => (
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/constants/NiceStepMultipliers.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/constants/NiceStepMultipliers.ts
new file mode 100644
index 0000000000..a769dd44a6
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/constants/NiceStepMultipliers.ts
@@ -0,0 +1,6 @@
+export const NICE_STEP_MULTIPLIERS = {
+ LARGE: 10,
+ MEDIUM: 5,
+ SMALL: 2,
+ DEFAULT: 1,
+} as const;
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/constants/NiceStepNormalizedValueThresholds.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/constants/NiceStepNormalizedValueThresholds.ts
new file mode 100644
index 0000000000..81ec99decf
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/constants/NiceStepNormalizedValueThresholds.ts
@@ -0,0 +1,5 @@
+export const NICE_STEP_NORMALIZED_VALUE_THRESHOLDS = {
+ LARGE: 5,
+ MEDIUM: 2,
+ SMALL: 1,
+} as const;
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 0bd753701f..94eb102a1c 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,5 +1,6 @@
import { GraphWidgetChartContainer } from '@/page-layout/widgets/graph/components/GraphWidgetChartContainer';
import { GraphWidgetLegend } from '@/page-layout/widgets/graph/components/GraphWidgetLegend';
+import { NoDataLayer } from '@/page-layout/widgets/graph/components/NoDataLayer';
import { CustomBarItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/components/CustomBarItem';
import { CustomTotalsLayer } from '@/page-layout/widgets/graph/graphWidgetBarChart/components/CustomTotalsLayer';
import { GraphBarChartTooltip } from '@/page-layout/widgets/graph/graphWidgetBarChart/components/GraphBarChartTooltip';
@@ -14,6 +15,9 @@ import { getBarChartAxisConfigs } from '@/page-layout/widgets/graph/graphWidgetB
import { getBarChartColor } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartColor';
import { getBarChartInnerPadding } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartInnerPadding';
import { getBarChartMargins } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartMargins';
+import { getBarChartTickConfig } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartTickConfig';
+import { computeEffectiveValueRange } from '@/page-layout/widgets/graph/utils/computeEffectiveValueRange';
+import { computeValueTickValues } from '@/page-layout/widgets/graph/utils/computeValueTickValues';
import { createGraphColorRegistry } from '@/page-layout/widgets/graph/utils/createGraphColorRegistry';
import {
formatGraphValue,
@@ -25,6 +29,7 @@ import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import {
ResponsiveBar,
+ type BarCustomLayerProps,
type BarDatum,
type BarItemProps,
type ComputedBarDatum,
@@ -37,6 +42,8 @@ import { useDebouncedCallback } from 'use-debounce';
import { graphWidgetBarTooltipComponentState } from '@/page-layout/widgets/graph/graphWidgetBarChart/states/graphWidgetBarTooltipComponentState';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
+type NoDataLayerWrapperProps = BarCustomLayerProps;
+
type GraphWidgetBarChartProps = {
data: BarDatum[];
indexBy: string;
@@ -120,6 +127,38 @@ export const GraphWidgetBarChart = ({
seriesLabels,
});
+ const calculatedValueRange =
+ groupMode === 'stacked'
+ ? calculateStackedBarChartValueRange(data, keys)
+ : calculateValueRangeFromBarChartKeys(data, keys);
+
+ const { effectiveMinimumValue, effectiveMaximumValue, hasNoData } =
+ computeEffectiveValueRange({
+ calculatedMinimum: calculatedValueRange.minimum,
+ calculatedMaximum: calculatedValueRange.maximum,
+ rangeMin,
+ rangeMax,
+ dataLength: data.length,
+ });
+
+ const tickConfig = getBarChartTickConfig({
+ width: chartWidth,
+ height: chartHeight,
+ data,
+ indexBy,
+ xAxisLabel,
+ yAxisLabel,
+ axisFontSize: chartTheme.axis.ticks.text.fontSize,
+ layout,
+ });
+
+ const { tickValues: valueTickValues, domain: valueDomain } =
+ computeValueTickValues({
+ minimum: effectiveMinimumValue,
+ maximum: effectiveMaximumValue,
+ tickCount: tickConfig.numberOfValueTicks,
+ });
+
const hasClickableItems = isDefined(onBarClick);
const hideTooltip = () => setActiveBarTooltip(null);
@@ -157,6 +196,8 @@ export const GraphWidgetBarChart = ({
yAxisLabel,
formatOptions,
axisFontSize: chartTheme.axis.ticks.text.fontSize,
+ valueTickValues,
+ tickConfig,
});
const BarItemWithContext = useMemo(
@@ -179,25 +220,32 @@ export const GraphWidgetBarChart = ({
bars,
}: {
bars: readonly ComputedBarDatum[];
- }) => (
- formatGraphValue(value, formatOptions)}
- offset={theme.spacingMultiplicator * 2}
- layout={layout}
- groupMode={groupMode}
- omitNullValues={omitNullValues}
- showValues={showValues}
+ }) => {
+ if (hasNoData) {
+ return null;
+ }
+
+ return (
+ formatGraphValue(value, formatOptions)}
+ offset={theme.spacingMultiplicator * 2}
+ layout={layout}
+ groupMode={groupMode}
+ omitNullValues={omitNullValues}
+ showValues={showValues}
+ />
+ );
+ };
+
+ const NoDataLayerWrapper = (layerProps: NoDataLayerWrapperProps) => (
+
);
- const calculatedValueRange =
- groupMode === 'stacked'
- ? calculateStackedBarChartValueRange(data, keys)
- : calculateValueRangeFromBarChartKeys(data, keys);
- const effectiveMinimumValue = rangeMin ?? calculatedValueRange.minimum;
- const effectiveMaximumValue = rangeMax ?? calculatedValueRange.maximum;
-
const hasNegativeValues = calculatedValueRange.minimum < 0;
const zeroMarker = hasNegativeValues
? [
@@ -239,13 +287,21 @@ export const GraphWidgetBarChart = ({
layout={layout}
valueScale={{
type: 'linear',
- min: effectiveMinimumValue,
- max: effectiveMaximumValue,
+ min: valueDomain.min,
+ max: valueDomain.max,
clamp: true,
}}
indexScale={{ type: 'band', round: true }}
colors={(datum) => getBarChartColor(datum, barConfigs, theme)}
- layers={['grid', 'markers', 'axes', 'bars', 'legends', TotalsLayer]}
+ layers={[
+ 'grid',
+ 'markers',
+ 'axes',
+ 'bars',
+ 'legends',
+ TotalsLayer,
+ NoDataLayerWrapper,
+ ]}
markers={zeroMarker}
axisTop={null}
axisRight={null}
@@ -253,8 +309,12 @@ export const GraphWidgetBarChart = ({
axisLeft={axisLeftConfig}
enableGridX={layout === BarChartLayout.HORIZONTAL && showGrid}
enableGridY={layout === BarChartLayout.VERTICAL && showGrid}
- gridXValues={layout === BarChartLayout.HORIZONTAL ? 5 : undefined}
- gridYValues={layout === BarChartLayout.VERTICAL ? 5 : undefined}
+ gridXValues={
+ layout === BarChartLayout.HORIZONTAL ? valueTickValues : undefined
+ }
+ gridYValues={
+ layout === BarChartLayout.VERTICAL ? valueTickValues : undefined
+ }
enableLabel={false}
labelSkipWidth={12}
innerPadding={getBarChartInnerPadding({
@@ -275,9 +335,9 @@ export const GraphWidgetBarChart = ({
formatGraphValue(Number(barDatumCandidate.value), formatOptions)
}
tooltip={() => null}
- onMouseEnter={handleBarEnter}
- onMouseLeave={handleBarLeave}
- onClick={onBarClick}
+ onMouseEnter={hasNoData ? undefined : handleBarEnter}
+ onMouseLeave={hasNoData ? undefined : handleBarLeave}
+ onClick={hasNoData ? undefined : onBarClick}
theme={chartTheme}
borderRadius={parseInt(theme.border.radius.sm)}
/>
@@ -294,7 +354,7 @@ export const GraphWidgetBarChart = ({
onMouseLeave={handleTooltipMouseLeave}
/>
{
return {
id: item.key,
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartMaximumValueTickCount.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartMaximumValueTickCount.ts
new file mode 100644
index 0000000000..981ea3d1d4
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartMaximumValueTickCount.ts
@@ -0,0 +1 @@
+export const BAR_CHART_MAXIMUM_VALUE_TICK_COUNT = 6;
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartMinimumValueTickCount.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartMinimumValueTickCount.ts
new file mode 100644
index 0000000000..40afea4e93
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartMinimumValueTickCount.ts
@@ -0,0 +1 @@
+export const BAR_CHART_MINIMUM_VALUE_TICK_COUNT = 2;
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
index 65c24d80ea..2ae6104a48 100644
--- 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
@@ -5,6 +5,12 @@ export const useBarChartTheme = () => {
return {
axis: {
+ domain: {
+ line: {
+ stroke: theme.border.color.light,
+ strokeWidth: 1,
+ },
+ },
ticks: {
line: {
stroke: theme.border.color.light,
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartAxisConfigs.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartAxisConfigs.ts
index 6939dd80e1..409821dfc4 100644
--- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartAxisConfigs.ts
+++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartAxisConfigs.ts
@@ -1,6 +1,9 @@
import { BarChartLayout } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartLayout';
import { getBarChartMargins } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartMargins';
-import { getBarChartTickConfig } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartTickConfig';
+import {
+ type BarChartTickConfig,
+ getBarChartTickConfig,
+} from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartTickConfig';
import { truncateTickLabel } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/truncateTickLabel';
import {
formatGraphValue,
@@ -29,6 +32,8 @@ type GetBarChartAxisConfigsProps = {
yAxisLabel?: string;
formatOptions?: GraphValueFormatOptions;
axisFontSize?: number;
+ valueTickValues?: number[];
+ tickConfig?: BarChartTickConfig;
};
export const getBarChartAxisConfigs = ({
@@ -41,24 +46,32 @@ export const getBarChartAxisConfigs = ({
yAxisLabel,
formatOptions,
axisFontSize = 11,
+ valueTickValues,
+ tickConfig,
}: GetBarChartAxisConfigsProps) => {
const {
categoryTickValues,
numberOfValueTicks,
maxBottomAxisTickLabelLength,
maxLeftAxisTickLabelLength,
- } = getBarChartTickConfig({
- width,
- height,
- data,
- indexBy,
- xAxisLabel,
- yAxisLabel,
- axisFontSize,
- layout,
- });
+ } =
+ tickConfig ??
+ getBarChartTickConfig({
+ width,
+ height,
+ data,
+ indexBy,
+ xAxisLabel,
+ yAxisLabel,
+ axisFontSize,
+ layout,
+ });
const margins = getBarChartMargins({ xAxisLabel, yAxisLabel, layout });
+ const resolvedValueTickValues =
+ valueTickValues && valueTickValues.length > 0
+ ? valueTickValues
+ : numberOfValueTicks;
if (layout === BarChartLayout.VERTICAL) {
return {
@@ -72,7 +85,7 @@ export const getBarChartAxisConfigs = ({
},
axisLeft: {
...COMMON_AXIS_CONFIG,
- tickValues: numberOfValueTicks,
+ tickValues: resolvedValueTickValues,
legend: yAxisLabel,
legendOffset: -margins.left + LEFT_AXIS_LEGEND_OFFSET_PADDING,
format: (value: number) =>
@@ -87,7 +100,7 @@ export const getBarChartAxisConfigs = ({
return {
axisBottom: {
...COMMON_AXIS_CONFIG,
- tickValues: numberOfValueTicks,
+ tickValues: resolvedValueTickValues,
legend: yAxisLabel,
legendOffset: BOTTOM_AXIS_LEGEND_OFFSET,
format: (value: number) =>
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartTickConfig.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartTickConfig.ts
index b62125c5d7..331764393a 100644
--- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartTickConfig.ts
+++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartTickConfig.ts
@@ -1,3 +1,5 @@
+import { BAR_CHART_MAXIMUM_VALUE_TICK_COUNT } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartMaximumValueTickCount';
+import { BAR_CHART_MINIMUM_VALUE_TICK_COUNT } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartMinimumValueTickCount';
import { BarChartLayout } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartLayout';
import { calculateMaxTickLabelLength } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/calculateMaxTickLabelLength';
import { calculateWidthPerTick } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/calculateWidthPerTick';
@@ -34,6 +36,12 @@ export const getBarChartTickConfig = ({
axisFontSize: number;
layout: BarChartLayout;
}): BarChartTickConfig => {
+ const clampValueTickCount = (tickCount: number) =>
+ Math.min(
+ BAR_CHART_MAXIMUM_VALUE_TICK_COUNT,
+ Math.max(BAR_CHART_MINIMUM_VALUE_TICK_COUNT, tickCount),
+ );
+
const categoryTickValues = computeBarChartCategoryTickValues({
axisSize: layout === BarChartLayout.VERTICAL ? width : height,
axisFontSize,
@@ -49,12 +57,14 @@ export const getBarChartTickConfig = ({
const availableWidth = width - (margins.left + margins.right);
const availableHeight = height - (margins.top + margins.bottom);
- const numberOfValueTicks = computeBarChartValueTickCount({
- axisSize:
- layout === BarChartLayout.VERTICAL ? availableHeight : availableWidth,
- axisFontSize,
- layout,
- });
+ const numberOfValueTicks = clampValueTickCount(
+ computeBarChartValueTickCount({
+ axisSize:
+ layout === BarChartLayout.VERTICAL ? availableHeight : availableWidth,
+ axisFontSize,
+ layout,
+ }),
+ );
const widthPerTick = calculateWidthPerTick({
layout,
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/transformGroupByDataToBarChartData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/transformGroupByDataToBarChartData.ts
index eefef243d8..ff927f36cd 100644
--- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/transformGroupByDataToBarChartData.ts
+++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/transformGroupByDataToBarChartData.ts
@@ -44,13 +44,14 @@ type TransformGroupByDataToBarChartDataResult = {
formattedToRawLookup: Map;
};
-const EMPTY_BAR_CHART_RESULT: TransformGroupByDataToBarChartDataResult = {
+const EMPTY_BAR_CHART_RESULT: Omit<
+ TransformGroupByDataToBarChartDataResult,
+ 'xAxisLabel' | 'yAxisLabel'
+> = {
data: [],
indexBy: '',
keys: [],
series: [],
- xAxisLabel: undefined,
- yAxisLabel: undefined,
showDataLabels: false,
showLegend: true,
layout: BarChartLayout.VERTICAL,
@@ -65,10 +66,6 @@ export const transformGroupByDataToBarChartData = ({
configuration,
aggregateOperation,
}: TransformGroupByDataToBarChartDataParams): TransformGroupByDataToBarChartDataResult => {
- if (!isDefined(groupByData)) {
- return EMPTY_BAR_CHART_RESULT;
- }
-
const groupByFieldX = objectMetadataItem.fields.find(
(field: FieldMetadataItem) =>
field.id === configuration.primaryAxisGroupByFieldMetadataId,
@@ -88,13 +85,53 @@ export const transformGroupByDataToBarChartData = ({
field.id === configuration.aggregateFieldMetadataId,
);
+ const queryResultGqlFieldName =
+ getGroupByQueryResultGqlFieldName(objectMetadataItem);
+ const rawResults = groupByData?.[queryResultGqlFieldName];
+ const hasNoData =
+ !isDefined(groupByData) ||
+ !isDefined(rawResults) ||
+ !Array.isArray(rawResults) ||
+ rawResults.length === 0;
+
+ const showXAxis =
+ hasNoData ||
+ configuration.axisNameDisplay === AxisNameDisplay.X ||
+ configuration.axisNameDisplay === AxisNameDisplay.BOTH;
+
+ const showYAxis =
+ hasNoData ||
+ configuration.axisNameDisplay === AxisNameDisplay.Y ||
+ configuration.axisNameDisplay === AxisNameDisplay.BOTH;
+
+ const xAxisLabel =
+ showXAxis && isDefined(groupByFieldX) ? groupByFieldX.label : undefined;
+
+ const yAxisLabel =
+ showYAxis && isDefined(aggregateField)
+ ? `${getAggregateOperationLabel(configuration.aggregateOperation)} of ${aggregateField.label}`
+ : undefined;
+
+ const layout =
+ configuration.graphType === GraphType.HORIZONTAL_BAR
+ ? BarChartLayout.HORIZONTAL
+ : BarChartLayout.VERTICAL;
+
+ if (!isDefined(groupByData)) {
+ return {
+ ...EMPTY_BAR_CHART_RESULT,
+ xAxisLabel,
+ yAxisLabel,
+ layout,
+ };
+ }
+
if (!isDefined(groupByFieldX) || !isDefined(aggregateField)) {
return {
...EMPTY_BAR_CHART_RESULT,
- layout:
- configuration.graphType === GraphType.HORIZONTAL_BAR
- ? BarChartLayout.HORIZONTAL
- : BarChartLayout.VERTICAL,
+ xAxisLabel,
+ yAxisLabel,
+ layout,
};
}
@@ -108,18 +145,13 @@ export const transformGroupByDataToBarChartData = ({
subFieldName: primaryAxisSubFieldName,
});
- const queryResultGqlFieldName =
- getGroupByQueryResultGqlFieldName(objectMetadataItem);
- const rawResults = groupByData[queryResultGqlFieldName];
-
if (!isDefined(rawResults) || !Array.isArray(rawResults)) {
return {
...EMPTY_BAR_CHART_RESULT,
indexBy: indexByKey,
- layout:
- configuration.graphType === GraphType.HORIZONTAL_BAR
- ? BarChartLayout.HORIZONTAL
- : BarChartLayout.VERTICAL,
+ xAxisLabel,
+ yAxisLabel,
+ layout,
};
}
@@ -141,20 +173,6 @@ export const transformGroupByDataToBarChartData = ({
objectMetadataItem,
});
- const showXAxis =
- configuration.axisNameDisplay === AxisNameDisplay.X ||
- configuration.axisNameDisplay === AxisNameDisplay.BOTH;
-
- const showYAxis =
- configuration.axisNameDisplay === AxisNameDisplay.Y ||
- configuration.axisNameDisplay === AxisNameDisplay.BOTH;
-
- const xAxisLabel = showXAxis ? groupByFieldX.label : undefined;
-
- const yAxisLabel = showYAxis
- ? `${getAggregateOperationLabel(configuration.aggregateOperation)} of ${aggregateField.label}`
- : undefined;
-
const showDataLabels = configuration.displayDataLabel ?? false;
const showLegend = configuration.displayLegend ?? true;
@@ -235,11 +253,6 @@ export const transformGroupByDataToBarChartData = ({
primaryAxisSubFieldName,
});
- const layout =
- configuration.graphType === GraphType.HORIZONTAL_BAR
- ? BarChartLayout.HORIZONTAL
- : BarChartLayout.VERTICAL;
-
return {
...baseResult,
xAxisLabel,
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/components/GraphWidgetLineChart.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/components/GraphWidgetLineChart.tsx
index 96471049ee..a483c1e628 100644
--- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/components/GraphWidgetLineChart.tsx
+++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/components/GraphWidgetLineChart.tsx
@@ -1,5 +1,6 @@
import { GraphWidgetChartContainer } from '@/page-layout/widgets/graph/components/GraphWidgetChartContainer';
import { GraphWidgetLegend } from '@/page-layout/widgets/graph/components/GraphWidgetLegend';
+import { NoDataLayer } from '@/page-layout/widgets/graph/components/NoDataLayer';
import {
CustomCrosshairLayer,
type SliceHoverData,
@@ -19,6 +20,8 @@ import { type LineChartSeries } from '@/page-layout/widgets/graph/graphWidgetLin
import { calculateValueRangeFromLineChartSeries } from '@/page-layout/widgets/graph/graphWidgetLineChart/utils/calculateValueRangeFromLineChartSeries';
import { getLineChartAxisBottomConfig } from '@/page-layout/widgets/graph/graphWidgetLineChart/utils/getLineChartAxisBottomConfig';
import { getLineChartAxisLeftConfig } from '@/page-layout/widgets/graph/graphWidgetLineChart/utils/getLineChartAxisLeftConfig';
+import { computeEffectiveValueRange } from '@/page-layout/widgets/graph/utils/computeEffectiveValueRange';
+import { computeValueTickValues } from '@/page-layout/widgets/graph/utils/computeValueTickValues';
import { createGraphColorRegistry } from '@/page-layout/widgets/graph/utils/createGraphColorRegistry';
import {
formatGraphValue,
@@ -42,6 +45,9 @@ import { useDebouncedCallback } from 'use-debounce';
type CrosshairLayerProps = LineCustomSvgLayerProps;
type PointLabelsLayerProps = LineCustomSvgLayerProps;
type StackedAreasLayerProps = LineCustomSvgLayerProps;
+type NoDataLayerWrapperProps = LineCustomSvgLayerProps;
+
+const LINE_CHART_DEFAULT_TICK_COUNT = 5;
type GraphWidgetLineChartProps = {
data: LineChartSeries[];
@@ -103,8 +109,15 @@ export const GraphWidgetLineChart = ({
};
const calculatedValueRange = calculateValueRangeFromLineChartSeries(data);
- const effectiveMinimumValue = rangeMin ?? calculatedValueRange.minimum;
- const effectiveMaximumValue = rangeMax ?? calculatedValueRange.maximum;
+
+ const { effectiveMinimumValue, effectiveMaximumValue, hasNoData } =
+ computeEffectiveValueRange({
+ calculatedMinimum: calculatedValueRange.minimum,
+ calculatedMaximum: calculatedValueRange.maximum,
+ rangeMin,
+ rangeMax,
+ dataLength: data.length,
+ });
const { enrichedSeries, nivoData, colors, legendItems } = useLineChartData({
data,
@@ -164,41 +177,67 @@ export const GraphWidgetLineChart = ({
});
};
- const PointLabelsLayer = (layerProps: PointLabelsLayerProps) => (
- formatGraphValue(value, formatOptions)}
- offset={theme.spacingMultiplicator * 2}
- groupMode={groupMode}
- omitNullValues={_omitNullValues}
- enablePointLabel={enablePointLabel}
- />
- );
+ const PointLabelsLayer = (layerProps: PointLabelsLayerProps) => {
+ if (hasNoData) {
+ return null;
+ }
- const CrosshairLayer = (layerProps: CrosshairLayerProps) => (
- formatGraphValue(value, formatOptions)}
+ offset={theme.spacingMultiplicator * 2}
+ groupMode={groupMode}
+ omitNullValues={_omitNullValues}
+ enablePointLabel={enablePointLabel}
+ />
+ );
+ };
+
+ const CrosshairLayer = (layerProps: CrosshairLayerProps) => {
+ if (hasNoData) {
+ return null;
+ }
+
+ return (
+ onSliceClick(sliceData.closestPoint)
+ : undefined
+ }
+ onRectLeave={handleSliceLeave}
+ />
+ );
+ };
+
+ const StackedAreasLayer = (layerProps: StackedAreasLayerProps) => {
+ if (hasNoData) {
+ return null;
+ }
+
+ return (
+
+ );
+ };
+
+ const NoDataLayerWrapper = (layerProps: NoDataLayerWrapperProps) => (
+ onSliceClick(sliceData.closestPoint)
- : undefined
- }
- onRectLeave={handleSliceLeave}
- />
- );
-
- const StackedAreasLayer = (layerProps: StackedAreasLayerProps) => (
-
);
@@ -207,7 +246,25 @@ export const GraphWidgetLineChart = ({
chartWidth,
data,
);
- const axisLeftConfig = getLineChartAxisLeftConfig(yAxisLabel, formatOptions);
+ const chartMargins = {
+ top: LINE_CHART_MARGIN_TOP,
+ right: LINE_CHART_MARGIN_RIGHT,
+ bottom: LINE_CHART_MARGIN_BOTTOM,
+ left: LINE_CHART_MARGIN_LEFT,
+ };
+ const { tickValues: valueTickValues, domain: valueDomain } =
+ computeValueTickValues({
+ minimum: effectiveMinimumValue,
+ maximum: effectiveMaximumValue,
+ tickCount: LINE_CHART_DEFAULT_TICK_COUNT,
+ });
+
+ const axisLeftConfig = getLineChartAxisLeftConfig(
+ yAxisLabel,
+ formatOptions,
+ valueTickValues,
+ chartMargins.left,
+ );
return (
@@ -225,16 +282,16 @@ export const GraphWidgetLineChart = ({
null}
tooltip={() => null}
@@ -264,6 +322,7 @@ export const GraphWidgetLineChart = ({
'points',
PointLabelsLayer,
'legends',
+ NoDataLayerWrapper,
]}
theme={chartTheme}
/>
@@ -276,7 +335,7 @@ export const GraphWidgetLineChart = ({
onMouseEnter={handleTooltipMouseEnter}
onMouseLeave={handleTooltipMouseLeave}
/>
-
+
);
};
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartMarginLeft.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartMarginLeft.ts
index 4f29117c72..95810fb255 100644
--- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartMarginLeft.ts
+++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartMarginLeft.ts
@@ -1 +1 @@
-export const LINE_CHART_MARGIN_LEFT = 70;
+export const LINE_CHART_MARGIN_LEFT = 80;
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/utils/getLineChartAxisLeftConfig.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/utils/getLineChartAxisLeftConfig.ts
index c391eb986a..57a555ddef 100644
--- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/utils/getLineChartAxisLeftConfig.ts
+++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/utils/getLineChartAxisLeftConfig.ts
@@ -1,17 +1,24 @@
+import { LINE_CHART_MARGIN_LEFT } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartMarginLeft';
import {
formatGraphValue,
type GraphValueFormatOptions,
} from '@/page-layout/widgets/graph/utils/graphFormatters';
+const LEFT_AXIS_LEGEND_OFFSET_PADDING = 5;
+const TICK_PADDING = 5;
+
export const getLineChartAxisLeftConfig = (
yAxisLabel?: string,
formatOptions?: GraphValueFormatOptions,
+ tickValues?: number[],
+ marginLeft: number = LINE_CHART_MARGIN_LEFT,
) => ({
tickSize: 0,
- tickPadding: 5,
+ tickPadding: TICK_PADDING,
tickRotation: 0,
+ tickValues,
legend: yAxisLabel,
legendPosition: 'middle' as const,
- legendOffset: -50,
+ legendOffset: -marginLeft + LEFT_AXIS_LEGEND_OFFSET_PADDING,
format: (value: number) => formatGraphValue(value, formatOptions || {}),
});
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChart.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChart.tsx
index a9eb57ba9f..f5868ef1bb 100644
--- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChart.tsx
+++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChart.tsx
@@ -174,6 +174,7 @@ export const GraphWidgetPieChart = ({
objectMetadataItemId={objectMetadataItemId}
configuration={configuration}
show={showCenterMetric && !hasNoData}
+ hasNoData={hasNoData}
/>
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/components/PieChartCenterMetricLayer.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/components/PieChartCenterMetricLayer.tsx
index 254d33adf5..1e5595641d 100644
--- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/components/PieChartCenterMetricLayer.tsx
+++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/components/PieChartCenterMetricLayer.tsx
@@ -1,6 +1,7 @@
import { usePieChartCenterMetricData } from '@/page-layout/widgets/graph/graphWidgetPieChart/hooks/usePieChartCenterMetricData';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
+import { useLingui } from '@lingui/react/macro';
import { AnimatePresence, motion } from 'framer-motion';
import { type PieChartConfiguration } from '~/generated/graphql';
@@ -8,6 +9,7 @@ type PieChartCenterMetricProps = {
objectMetadataItemId: string;
configuration: PieChartConfiguration;
show: boolean;
+ hasNoData?: boolean;
};
const StyledCenterMetricContainer = styled(motion.div)`
@@ -33,22 +35,31 @@ const StyledLabel = styled.span`
font-size: clamp(10px, 5cqmin, 24px);
`;
+const StyledNoDataText = styled.span`
+ color: ${({ theme }) => theme.font.color.tertiary};
+ font-size: ${({ theme }) => theme.font.size.md};
+`;
+
export const PieChartCenterMetric = ({
objectMetadataItemId,
configuration,
show,
+ hasNoData = false,
}: PieChartCenterMetricProps) => {
const theme = useTheme();
+ const { t } = useLingui();
const { centerMetricValue, centerMetricLabel } = usePieChartCenterMetricData({
objectMetadataItemId,
configuration,
- skip: !show,
+ skip: !show && !hasNoData,
});
+ const shouldShowContent = show || hasNoData;
+
return (
- {show && (
+ {shouldShowContent && (
- {centerMetricValue}
- {centerMetricLabel}
+ {hasNoData ? (
+ {t`No data`}
+ ) : (
+ <>
+ {centerMetricValue}
+ {centerMetricLabel}
+ >
+ )}
)}
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/computeEffectiveValueRange.test.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/computeEffectiveValueRange.test.ts
new file mode 100644
index 0000000000..01c850d5cb
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/computeEffectiveValueRange.test.ts
@@ -0,0 +1,59 @@
+import { computeEffectiveValueRange } from '../computeEffectiveValueRange';
+
+describe('computeEffectiveValueRange', () => {
+ it('should return hasNoData true when dataLength is 0', () => {
+ const result = computeEffectiveValueRange({
+ calculatedMinimum: 0,
+ calculatedMaximum: 0,
+ dataLength: 0,
+ });
+
+ expect(result.hasNoData).toBe(true);
+ });
+
+ it('should start from 0 for non-negative values', () => {
+ const result = computeEffectiveValueRange({
+ calculatedMinimum: 5,
+ calculatedMaximum: 100,
+ dataLength: 10,
+ });
+
+ expect(result.effectiveMinimumValue).toBe(0);
+ expect(result.hasNoData).toBe(false);
+ });
+
+ it('should use calculated minimum for negative values', () => {
+ const result = computeEffectiveValueRange({
+ calculatedMinimum: -50,
+ calculatedMaximum: 100,
+ dataLength: 10,
+ });
+
+ expect(result.effectiveMinimumValue).toBe(-50);
+ });
+
+ it('should respect explicit rangeMin and rangeMax', () => {
+ const result = computeEffectiveValueRange({
+ calculatedMinimum: 0,
+ calculatedMaximum: 100,
+ rangeMin: 10,
+ rangeMax: 50,
+ dataLength: 10,
+ });
+
+ expect(result.effectiveMinimumValue).toBe(10);
+ expect(result.effectiveMaximumValue).toBe(50);
+ });
+
+ it('should add padding when min equals max and no explicit range', () => {
+ const result = computeEffectiveValueRange({
+ calculatedMinimum: 50,
+ calculatedMaximum: 50,
+ dataLength: 1,
+ });
+
+ expect(result.effectiveMaximumValue).toBeGreaterThan(
+ result.effectiveMinimumValue,
+ );
+ });
+});
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/computeValueTickValues.test.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/computeValueTickValues.test.ts
new file mode 100644
index 0000000000..882f571ad7
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/computeValueTickValues.test.ts
@@ -0,0 +1,49 @@
+import { computeValueTickValues } from '../computeValueTickValues';
+
+describe('computeValueTickValues', () => {
+ it('should return empty array for non-finite inputs', () => {
+ const result = computeValueTickValues({
+ minimum: Infinity,
+ maximum: 100,
+ tickCount: 5,
+ });
+
+ expect(result.tickValues).toEqual([]);
+ expect(result.domain).toEqual({ min: 0, max: 0 });
+ });
+
+ it('should return single tick when min equals max', () => {
+ const result = computeValueTickValues({
+ minimum: 50,
+ maximum: 50,
+ tickCount: 5,
+ });
+
+ expect(result.tickValues).toEqual([50]);
+ expect(result.domain).toEqual({ min: 50, max: 50 });
+ });
+
+ it('should generate nice tick values for positive range', () => {
+ const result = computeValueTickValues({
+ minimum: 0,
+ maximum: 100,
+ tickCount: 5,
+ });
+
+ expect(result.tickValues.length).toBeGreaterThanOrEqual(2);
+ expect(result.domain.min).toBeLessThanOrEqual(0);
+ expect(result.domain.max).toBeGreaterThanOrEqual(100);
+ });
+
+ it('should handle negative ranges', () => {
+ const result = computeValueTickValues({
+ minimum: -100,
+ maximum: -10,
+ tickCount: 5,
+ });
+
+ expect(result.tickValues.length).toBeGreaterThanOrEqual(2);
+ expect(result.domain.min).toBeLessThanOrEqual(-100);
+ expect(result.domain.max).toBeGreaterThanOrEqual(-10);
+ });
+});
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/computeEffectiveValueRange.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/computeEffectiveValueRange.ts
new file mode 100644
index 0000000000..1c84b8352e
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/computeEffectiveValueRange.ts
@@ -0,0 +1,65 @@
+import { isDefined } from 'twenty-shared/utils';
+
+const POSITIVE_RANGE_PADDING_RATIO = 0.1;
+const MINIMUM_POSITIVE_RANGE_PADDING = 1;
+
+type ComputeEffectiveValueRangeParams = {
+ calculatedMinimum: number;
+ calculatedMaximum: number;
+ rangeMin?: number;
+ rangeMax?: number;
+ dataLength: number;
+};
+
+type EffectiveValueRangeResult = {
+ effectiveMinimumValue: number;
+ effectiveMaximumValue: number;
+ hasNoData: boolean;
+};
+
+export const computeEffectiveValueRange = ({
+ calculatedMinimum,
+ calculatedMaximum,
+ rangeMin,
+ rangeMax,
+ dataLength,
+}: ComputeEffectiveValueRangeParams): EffectiveValueRangeResult => {
+ const hasOnlyNonNegativeValues =
+ calculatedMinimum >= 0 && calculatedMaximum >= 0;
+ const hasNoData = dataLength === 0;
+
+ const baseMinimumValue = isDefined(rangeMin)
+ ? rangeMin
+ : hasOnlyNonNegativeValues
+ ? 0
+ : calculatedMinimum;
+
+ const positiveRangePaddingTarget = isDefined(rangeMax)
+ ? rangeMax
+ : calculatedMaximum;
+
+ const paddedMaximumForNonNegative =
+ isDefined(rangeMax) || !hasOnlyNonNegativeValues
+ ? positiveRangePaddingTarget
+ : positiveRangePaddingTarget +
+ Math.max(
+ Math.abs(positiveRangePaddingTarget) * POSITIVE_RANGE_PADDING_RATIO,
+ MINIMUM_POSITIVE_RANGE_PADDING,
+ );
+
+ let effectiveMinimumValue = baseMinimumValue;
+ let effectiveMaximumValue = paddedMaximumForNonNegative;
+
+ if (!isDefined(rangeMax) && !isDefined(rangeMin)) {
+ if (effectiveMinimumValue === effectiveMaximumValue) {
+ effectiveMaximumValue =
+ effectiveMinimumValue + MINIMUM_POSITIVE_RANGE_PADDING;
+ }
+ }
+
+ return {
+ effectiveMinimumValue,
+ effectiveMaximumValue,
+ hasNoData,
+ };
+};
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/computeValueTickValues.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/computeValueTickValues.ts
new file mode 100644
index 0000000000..0728473474
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/computeValueTickValues.ts
@@ -0,0 +1,71 @@
+import { NICE_STEP_MULTIPLIERS } from '@/page-layout/widgets/graph/constants/NiceStepMultipliers';
+import { NICE_STEP_NORMALIZED_VALUE_THRESHOLDS } from '@/page-layout/widgets/graph/constants/NiceStepNormalizedValueThresholds';
+
+const computeNiceStepInterval = (roughStepInterval: number): number => {
+ if (roughStepInterval === 0) {
+ return 0;
+ }
+
+ const stepMagnitude = Math.pow(
+ 10,
+ Math.floor(Math.log10(Math.abs(roughStepInterval))),
+ );
+ const normalizedStepValue = roughStepInterval / stepMagnitude;
+
+ if (normalizedStepValue >= NICE_STEP_NORMALIZED_VALUE_THRESHOLDS.LARGE)
+ return NICE_STEP_MULTIPLIERS.LARGE * stepMagnitude;
+ if (normalizedStepValue >= NICE_STEP_NORMALIZED_VALUE_THRESHOLDS.MEDIUM)
+ return NICE_STEP_MULTIPLIERS.MEDIUM * stepMagnitude;
+ if (normalizedStepValue >= NICE_STEP_NORMALIZED_VALUE_THRESHOLDS.SMALL)
+ return NICE_STEP_MULTIPLIERS.SMALL * stepMagnitude;
+ return NICE_STEP_MULTIPLIERS.DEFAULT * stepMagnitude;
+};
+
+export const computeValueTickValues = ({
+ minimum,
+ maximum,
+ tickCount,
+}: {
+ minimum: number;
+ maximum: number;
+ tickCount: number;
+}): {
+ tickValues: number[];
+ domain: { min: number; max: number };
+} => {
+ if (!Number.isFinite(minimum) || !Number.isFinite(maximum)) {
+ return { tickValues: [], domain: { min: 0, max: 0 } };
+ }
+
+ if (minimum === maximum) {
+ return { tickValues: [minimum], domain: { min: minimum, max: minimum } };
+ }
+
+ const safeTickCount = Math.max(2, tickCount);
+ const roughStepInterval = (maximum - minimum) / (safeTickCount - 1);
+ const niceStepInterval = computeNiceStepInterval(roughStepInterval);
+
+ if (niceStepInterval === 0) {
+ return {
+ tickValues: [minimum, maximum],
+ domain: { min: minimum, max: maximum },
+ };
+ }
+
+ const niceMinimum = Math.floor(minimum / niceStepInterval) * niceStepInterval;
+ const niceMaximum = Math.ceil(maximum / niceStepInterval) * niceStepInterval;
+ const tickValues: number[] = [];
+
+ for (
+ let tickValue = niceMinimum;
+ tickValue <= niceMaximum + niceStepInterval / 2;
+ tickValue += niceStepInterval
+ ) {
+ tickValues.push(Number(tickValue.toFixed(12)));
+ }
+
+ return {
+ tickValues,
+ domain: { min: niceMinimum, max: niceMaximum },
+ };
+};
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformGroupByDataToLineChartData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformGroupByDataToLineChartData.ts
index a279c13518..40716c98a5 100644
--- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformGroupByDataToLineChartData.ts
+++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformGroupByDataToLineChartData.ts
@@ -34,10 +34,11 @@ type TransformGroupByDataToLineChartDataResult = {
formattedToRawLookup: Map;
};
-const EMPTY_LINE_CHART_RESULT: TransformGroupByDataToLineChartDataResult = {
+const EMPTY_LINE_CHART_RESULT: Omit<
+ TransformGroupByDataToLineChartDataResult,
+ 'xAxisLabel' | 'yAxisLabel'
+> = {
series: [],
- xAxisLabel: undefined,
- yAxisLabel: undefined,
showDataLabels: false,
showLegend: true,
hasTooManyGroups: false,
@@ -51,10 +52,6 @@ export const transformGroupByDataToLineChartData = ({
configuration,
aggregateOperation,
}: TransformGroupByDataToLineChartDataParams): TransformGroupByDataToLineChartDataResult => {
- if (!isDefined(groupByData)) {
- return EMPTY_LINE_CHART_RESULT;
- }
-
const groupByFieldX = objectMetadataItem.fields.find(
(field: FieldMetadataItem) =>
field.id === configuration.primaryAxisGroupByFieldMetadataId,
@@ -74,8 +71,47 @@ export const transformGroupByDataToLineChartData = ({
field.id === configuration.aggregateFieldMetadataId,
);
+ const queryResultGqlFieldName =
+ getGroupByQueryResultGqlFieldName(objectMetadataItem);
+ const rawResults = groupByData?.[queryResultGqlFieldName];
+ const hasNoData =
+ !isDefined(groupByData) ||
+ !isDefined(rawResults) ||
+ !Array.isArray(rawResults) ||
+ rawResults.length === 0;
+
+ const showXAxis =
+ hasNoData ||
+ configuration.axisNameDisplay === AxisNameDisplay.X ||
+ configuration.axisNameDisplay === AxisNameDisplay.BOTH;
+
+ const showYAxis =
+ hasNoData ||
+ configuration.axisNameDisplay === AxisNameDisplay.Y ||
+ configuration.axisNameDisplay === AxisNameDisplay.BOTH;
+
+ const xAxisLabel =
+ showXAxis && isDefined(groupByFieldX) ? groupByFieldX.label : undefined;
+
+ const yAxisLabel =
+ showYAxis && isDefined(aggregateField)
+ ? `${getAggregateOperationLabel(configuration.aggregateOperation)} of ${aggregateField.label}`
+ : undefined;
+
+ if (!isDefined(groupByData)) {
+ return {
+ ...EMPTY_LINE_CHART_RESULT,
+ xAxisLabel,
+ yAxisLabel,
+ };
+ }
+
if (!isDefined(groupByFieldX) || !isDefined(aggregateField)) {
- return EMPTY_LINE_CHART_RESULT;
+ return {
+ ...EMPTY_LINE_CHART_RESULT,
+ xAxisLabel,
+ yAxisLabel,
+ };
}
const primaryAxisSubFieldName =
@@ -83,12 +119,12 @@ export const transformGroupByDataToLineChartData = ({
const secondaryAxisSubFieldName =
configuration.secondaryAxisGroupBySubFieldName ?? undefined;
- const queryResultGqlFieldName =
- getGroupByQueryResultGqlFieldName(objectMetadataItem);
- const rawResults = groupByData[queryResultGqlFieldName];
-
if (!isDefined(rawResults) || !Array.isArray(rawResults)) {
- return EMPTY_LINE_CHART_RESULT;
+ return {
+ ...EMPTY_LINE_CHART_RESULT,
+ xAxisLabel,
+ yAxisLabel,
+ };
}
const filteredResults = filterGroupByResults({
@@ -109,20 +145,6 @@ export const transformGroupByDataToLineChartData = ({
objectMetadataItem,
});
- const showXAxis =
- configuration.axisNameDisplay === AxisNameDisplay.X ||
- configuration.axisNameDisplay === AxisNameDisplay.BOTH;
-
- const showYAxis =
- configuration.axisNameDisplay === AxisNameDisplay.Y ||
- configuration.axisNameDisplay === AxisNameDisplay.BOTH;
-
- const xAxisLabel = showXAxis ? groupByFieldX.label : undefined;
-
- const yAxisLabel = showYAxis
- ? `${getAggregateOperationLabel(configuration.aggregateOperation)} of ${aggregateField.label}`
- : undefined;
-
const showDataLabels = configuration.displayDataLabel ?? false;
const showLegend = configuration.displayLegend ?? true;