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;