feat(dashboard): extend number format option to bar, line and pie charts (#23505)

https://discord.com/channels/1130383047699738754/1509604545381142649

Extends the Format option (Short/Full) added for the Number widget in
#21521 to bar, line and pie charts.

Format controls the numbers printed on the chart face: data labels and
the pie center metric. Axis ticks stay abbreviated and tooltips always
show the full value. Defaults to Short, so existing charts render
unchanged.

Server: nullable `numberFormat` on the bar/line/pie configuration DTOs,
exposed in the dashboard AI tool schema. No migration, configuration is
jsonb.

Deferred:
- The Format row has no visible effect while data labels are off, since
tooltips are always full.
- Number widget format defaults differ by field type (CURRENCY defaults
to Short, NUMBER to Full). Pre-existing, untouched here.


https://github.com/user-attachments/assets/0778f08a-6681-4e7a-8716-fb3026d1e01f



<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23505?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
nitin
2026-07-30 15:12:21 +05:30
committed by GitHub
parent 72322a4d72
commit 079e9b8e56
39 changed files with 530 additions and 60 deletions
@@ -63,6 +63,21 @@ describe('transformAggregateRawValueIntoAggregateDisplayValue', () => {
).toBe('153,909');
});
it('should format large COUNT values with the SHORT chart number format', () => {
expect(
transformAggregateRawValueIntoAggregateDisplayValue({
aggregateFieldMetadataItem: undefined,
aggregateOperation: AggregateOperations.COUNT,
aggregateRawValue: 153909,
dateFormat: DateFormat.DAY_FIRST,
timeFormat: TimeFormat.HOUR_24,
localeCatalog: enUS,
timeZone: 'UTC',
chartNumberFormat: ChartNumberFormat.SHORT,
}),
).toBe('153.9k');
});
it('should format large COUNT values respecting the SPACES_AND_COMMA number format', () => {
expect(
transformAggregateRawValueIntoAggregateDisplayValue({
@@ -46,7 +46,11 @@ export const transformAggregateRawValueIntoAggregateDisplayValue = ({
aggregateOperation as AggregateOperations,
)
) {
return formatNumber(Number(aggregateRawValue), { format: numberFormat });
const countValue = Number(aggregateRawValue);
return chartNumberFormat === ChartNumberFormat.SHORT
? formatToShortNumber(countValue)
: formatNumber(countValue, { format: numberFormat });
} else if (!isDefined(aggregateFieldMetadataItem)) {
return '-';
} else if (
@@ -54,6 +54,7 @@ export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql`
axisNameDisplay
displayDataLabel
displayLegend
numberFormat
rangeMin
rangeMax
color
@@ -84,6 +85,7 @@ export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql`
axisNameDisplay
displayDataLabel
displayLegend
numberFormat
rangeMin
rangeMax
color
@@ -107,6 +109,7 @@ export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql`
displayDataLabel
showCenterMetric
displayLegend
numberFormat
hideEmptyCategory
splitMultiValueFields
color
@@ -0,0 +1,3 @@
import { ChartNumberFormat } from '~/generated-metadata/graphql';
export const CHART_NUMBER_FORMAT_DEFAULT = ChartNumberFormat.SHORT;
@@ -32,6 +32,7 @@ type BarChartProps = {
effectiveValueRange: { minimum: number; maximum: number };
hasExplicitRangeBounds: boolean;
formatOptions: GraphValueFormatOptions;
axisFormatOptions?: GraphValueFormatOptions;
axisConfig?: {
xAxisLabel?: string;
yAxisLabel?: string;
@@ -69,6 +70,7 @@ export const BarChart = ({
effectiveValueRange,
hasExplicitRangeBounds,
formatOptions,
axisFormatOptions,
axisConfig,
rightTickLabels,
dataLabelsConfig,
@@ -107,7 +109,7 @@ export const BarChart = ({
data,
effectiveValueRange,
hasExplicitRangeBounds,
formatOptions,
formatOptions: axisFormatOptions ?? formatOptions,
groupMode,
indexBy,
keys,
@@ -1,3 +1,4 @@
import { useNumberFormat } from '@/localization/hooks/useNumberFormat';
import { pageLayoutDraggingWidgetIdComponentState } from '@/page-layout/states/pageLayoutDraggingWidgetIdComponentState';
import { pageLayoutResizingWidgetIdComponentState } from '@/page-layout/states/pageLayoutResizingWidgetIdComponentState';
import { GraphWidgetChartContainer } from '@/page-layout/widgets/graph/components/GraphWidgetChartContainer';
@@ -34,6 +35,7 @@ import { useDebouncedCallback } from 'use-debounce';
import { BarChartLayout } from '~/generated-metadata/graphql';
type GraphWidgetBarChartProps = {
axisDisplayType?: GraphValueFormatOptions['displayType'];
colorMode: GraphColorMode;
data: BarChartDatum[];
groupMode?: 'grouped' | 'stacked';
@@ -50,6 +52,7 @@ type GraphWidgetBarChartProps = {
showGrid?: boolean;
showLegend?: boolean;
showValues?: boolean;
tooltipDisplayType?: GraphValueFormatOptions['displayType'];
xAxisLabel?: string;
yAxisLabel?: string;
} & GraphValueFormatOptions;
@@ -82,6 +85,8 @@ export const GraphWidgetBarChart = ({
rangeMax,
omitNullValues = false,
displayType,
axisDisplayType,
tooltipDisplayType,
decimals,
prefix,
suffix,
@@ -124,15 +129,34 @@ export const GraphWidgetBarChart = ({
const allowDataTransitions = !isLayoutAnimating;
const { formatNumber } = useNumberFormat();
const formatOptions = useMemo<GraphValueFormatOptions>(
() => ({
customFormatter,
decimals,
displayType,
formatNumberFn: formatNumber,
prefix,
suffix,
}),
[customFormatter, decimals, displayType, prefix, suffix],
[customFormatter, decimals, displayType, formatNumber, prefix, suffix],
);
const axisFormatOptions = useMemo<GraphValueFormatOptions>(
() => ({
...formatOptions,
displayType: axisDisplayType ?? displayType,
}),
[formatOptions, axisDisplayType, displayType],
);
const tooltipFormatOptions = useMemo<GraphValueFormatOptions>(
() => ({
...formatOptions,
displayType: tooltipDisplayType ?? displayType,
}),
[formatOptions, tooltipDisplayType, displayType],
);
const { enrichedKeysMap, enrichedKeys, legendItems, visibleKeys } =
@@ -262,6 +286,7 @@ export const GraphWidgetBarChart = ({
hasExplicitRangeBounds={hasExplicitRangeBounds}
enrichedKeysMap={enrichedKeysMap}
formatOptions={formatOptions}
axisFormatOptions={axisFormatOptions}
rightTickLabels={rightTickLabels}
groupMode={groupMode}
hasNoData={hasNoData}
@@ -280,7 +305,7 @@ export const GraphWidgetBarChart = ({
containerRef={containerRef}
dataByIndexValue={dataByIndexValue}
enrichedKeys={enrichedKeys}
formatOptions={formatOptions}
formatOptions={tooltipFormatOptions}
onMouseEnter={handleTooltipMouseEnter}
onMouseLeave={handleTooltipMouseLeave}
onSliceClick={onSliceClick}
@@ -7,6 +7,7 @@ import { type BarChartSlice } from '@/page-layout/widgets/graph/graph-widget-bar
import { assertBarChartWidgetOrThrow } from '@/page-layout/widgets/graph/utils/assertBarChartWidget';
import { buildChartDrilldownQueryParams } from '@/page-layout/widgets/graph/utils/buildChartDrilldownQueryParams';
import { generateChartAggregateFilterKey } from '@/page-layout/widgets/graph/utils/generateChartAggregateFilterKey';
import { getChartValueFormatOptions } from '@/page-layout/widgets/graph/utils/getChartValueFormatOptions';
import { isFilteredViewRedirectionSupported } from '@/page-layout/widgets/graph/utils/isFilteredViewRedirectionSupported';
import { useCurrentWidget } from '@/page-layout/widgets/hooks/useCurrentWidget';
import { useUserFirstDayOfTheWeek } from '@/ui/input/components/internal/date/hooks/useUserFirstDayOfTheWeek';
@@ -61,6 +62,13 @@ export const GraphWidgetBarChartRenderer = () => {
const configuration = widget.configuration;
const isPageLayoutInEditMode = useIsPageLayoutInEditMode();
const chartValueFormatOptions = getChartValueFormatOptions({
aggregateOperation: configuration.aggregateOperation,
aggregateFieldMetadataId: configuration.aggregateFieldMetadataId,
fieldMetadataItems: objectMetadataItem.fields,
numberFormat: configuration.numberFormat,
});
const axisNameDisplay = configuration.axisNameDisplay;
const showXLabel =
@@ -143,7 +151,10 @@ export const GraphWidgetBarChartRenderer = () => {
groupMode={groupMode}
colorMode={colorMode}
id={widget.id}
displayType="shortNumber"
decimals={chartValueFormatOptions.decimals}
displayType={chartValueFormatOptions.displayType}
axisDisplayType="shortNumber"
tooltipDisplayType="number"
rangeMin={configuration.rangeMin ?? undefined}
rangeMax={configuration.rangeMax ?? undefined}
omitNullValues={configuration.omitNullValues ?? false}
@@ -1,3 +1,4 @@
import { useNumberFormat } from '@/localization/hooks/useNumberFormat';
import { CHART_MOTION_CONFIG } from '@/page-layout/widgets/graph/constants/ChartMotionConfig';
import { GraphWidgetChartContainer } from '@/page-layout/widgets/graph/components/GraphWidgetChartContainer';
import { GraphWidgetLegend } from '@/page-layout/widgets/graph/components/GraphWidgetLegend';
@@ -46,6 +47,7 @@ type LinesLayerProps = LineCustomSvgLayerProps<LineSeries>;
type NoDataLayerWrapperProps = LineCustomSvgLayerProps<LineSeries>;
type GraphWidgetLineChartProps = {
axisDisplayType?: GraphValueFormatOptions['displayType'];
data: LineChartSeriesWithColor[];
showLegend?: boolean;
showGrid?: boolean;
@@ -60,6 +62,7 @@ type GraphWidgetLineChartProps = {
groupMode?: 'stacked';
colorMode: GraphColorMode;
onSliceClick?: (point: Point<LineSeries>) => void;
tooltipDisplayType?: GraphValueFormatOptions['displayType'];
} & GraphValueFormatOptions;
const StyledContainer = styled.div`
@@ -84,6 +87,8 @@ export const GraphWidgetLineChart = ({
rangeMax,
omitNullValues = false,
displayType,
axisDisplayType,
tooltipDisplayType,
groupMode,
colorMode,
decimals,
@@ -98,12 +103,25 @@ export const GraphWidgetLineChart = ({
const containerRef = useRef<HTMLDivElement>(null);
const [chartWidth, setChartWidth] = useState(0);
const { formatNumber } = useNumberFormat();
const formatOptions: GraphValueFormatOptions = {
displayType,
decimals,
prefix,
suffix,
customFormatter,
formatNumberFn: formatNumber,
};
const axisFormatOptions: GraphValueFormatOptions = {
...formatOptions,
displayType: axisDisplayType ?? displayType,
};
const tooltipFormatOptions: GraphValueFormatOptions = {
...formatOptions,
displayType: tooltipDisplayType ?? displayType,
};
const { enrichedSeries, nivoData, colors, legendItems, visibleData } =
@@ -168,7 +186,7 @@ export const GraphWidgetLineChart = ({
data,
xAxisLabel,
yAxisLabel,
formatOptions,
formatOptions: axisFormatOptions,
effectiveMinimumValue,
effectiveMaximumValue,
});
@@ -342,7 +360,7 @@ export const GraphWidgetLineChart = ({
<GraphLineChartTooltip
containerRef={containerRef}
enrichedSeries={enrichedSeries}
formatOptions={formatOptions}
formatOptions={tooltipFormatOptions}
isStacked={groupMode === 'stacked'}
onSliceClick={onSliceClick}
onMouseEnter={handleTooltipMouseEnter}
@@ -7,6 +7,7 @@ import { useGraphLineChartWidgetData } from '@/page-layout/widgets/graph/graph-w
import { assertLineChartWidgetOrThrow } from '@/page-layout/widgets/graph/utils/assertLineChartWidget';
import { buildChartDrilldownQueryParams } from '@/page-layout/widgets/graph/utils/buildChartDrilldownQueryParams';
import { generateChartAggregateFilterKey } from '@/page-layout/widgets/graph/utils/generateChartAggregateFilterKey';
import { getChartValueFormatOptions } from '@/page-layout/widgets/graph/utils/getChartValueFormatOptions';
import { isFilteredViewRedirectionSupported } from '@/page-layout/widgets/graph/utils/isFilteredViewRedirectionSupported';
import { useCurrentWidget } from '@/page-layout/widgets/hooks/useCurrentWidget';
import { useUserFirstDayOfTheWeek } from '@/ui/input/components/internal/date/hooks/useUserFirstDayOfTheWeek';
@@ -59,6 +60,13 @@ export const GraphWidgetLineChartRenderer = () => {
const configuration = widget.configuration;
const isPageLayoutInEditMode = useIsPageLayoutInEditMode();
const chartValueFormatOptions = getChartValueFormatOptions({
aggregateOperation: configuration.aggregateOperation,
aggregateFieldMetadataId: configuration.aggregateFieldMetadataId,
fieldMetadataItems: objectMetadataItem.fields,
numberFormat: configuration.numberFormat,
});
const hasGroupByOnSecondaryAxis = isDefined(
configuration.secondaryAxisGroupByFieldMetadataId,
);
@@ -151,7 +159,10 @@ export const GraphWidgetLineChartRenderer = () => {
omitNullValues={configuration.omitNullValues ?? false}
groupMode={groupMode}
colorMode={colorMode}
displayType="shortNumber"
decimals={chartValueFormatOptions.decimals}
displayType={chartValueFormatOptions.displayType}
axisDisplayType="shortNumber"
tooltipDisplayType="number"
onSliceClick={
isPageLayoutInEditMode || !canRedirectToFilteredView
? undefined
@@ -1,3 +1,4 @@
import { useNumberFormat } from '@/localization/hooks/useNumberFormat';
import { GraphWidgetChartContainer } from '@/page-layout/widgets/graph/components/GraphWidgetChartContainer';
import { GraphWidgetLegend } from '@/page-layout/widgets/graph/components/GraphWidgetLegend';
import { CHART_MOTION_CONFIG } from '@/page-layout/widgets/graph/constants/ChartMotionConfig';
@@ -41,6 +42,7 @@ type GraphWidgetPieChartProps = {
onSliceClick?: (datum: PieChartDataItemWithColor) => void;
showDataLabels?: boolean;
showCenterMetric?: boolean;
tooltipDisplayType?: GraphValueFormatOptions['displayType'];
} & GraphValueFormatOptions;
const emptyStateData: PieChartDataItemWithColor[] = [
@@ -80,6 +82,7 @@ export const GraphWidgetPieChart = ({
configuration,
colorMode,
displayType,
tooltipDisplayType,
decimals,
prefix,
suffix,
@@ -95,12 +98,20 @@ export const GraphWidgetPieChart = ({
graphWidgetPieTooltipComponentState,
);
const { formatNumber } = useNumberFormat();
const formatOptions: GraphValueFormatOptions = {
displayType,
decimals,
prefix,
suffix,
customFormatter,
formatNumberFn: formatNumber,
};
const tooltipFormatOptions: GraphValueFormatOptions = {
...formatOptions,
displayType: tooltipDisplayType ?? displayType,
};
const { enrichedData, legendItems } = usePieChartData({
@@ -229,8 +240,8 @@ export const GraphWidgetPieChart = ({
<GraphPieChartTooltip
containerRef={containerRef}
enrichedData={enrichedData}
formatOptions={formatOptions}
displayType={displayType}
formatOptions={tooltipFormatOptions}
displayType={tooltipDisplayType ?? displayType}
onSliceClick={onSliceClick}
/>
{showLegend && data.length > 0 && (
@@ -6,6 +6,7 @@ import { useGraphPieChartWidgetData } from '@/page-layout/widgets/graph/graph-wi
import { type PieChartDataItemWithColor } from '@/page-layout/widgets/graph/graph-widget-pie-chart/types/PieChartDataItem';
import { assertPieChartWidgetOrThrow } from '@/page-layout/widgets/graph/utils/assertPieChartWidget';
import { buildChartDrilldownQueryParams } from '@/page-layout/widgets/graph/utils/buildChartDrilldownQueryParams';
import { getChartValueFormatOptions } from '@/page-layout/widgets/graph/utils/getChartValueFormatOptions';
import { isFilteredViewRedirectionSupported } from '@/page-layout/widgets/graph/utils/isFilteredViewRedirectionSupported';
import { useCurrentWidget } from '@/page-layout/widgets/hooks/useCurrentWidget';
import { useUserFirstDayOfTheWeek } from '@/ui/input/components/internal/date/hooks/useUserFirstDayOfTheWeek';
@@ -58,6 +59,13 @@ export const GraphWidgetPieChartRenderer = () => {
const { userFirstDayOfTheWeek } = useUserFirstDayOfTheWeek();
const chartValueFormatOptions = getChartValueFormatOptions({
aggregateOperation: widget.configuration.aggregateOperation,
aggregateFieldMetadataId: widget.configuration.aggregateFieldMetadataId,
fieldMetadataItems: objectMetadataItem.fields,
numberFormat: widget.configuration.numberFormat,
});
const groupByField = objectMetadataItem.fields.find(
(field) => field.id === widget.configuration.groupByFieldMetadataId,
);
@@ -109,7 +117,9 @@ export const GraphWidgetPieChartRenderer = () => {
configuration={widget.configuration}
showLegend={showLegend}
colorMode={colorMode}
displayType="shortNumber"
decimals={chartValueFormatOptions.decimals}
displayType={chartValueFormatOptions.displayType}
tooltipDisplayType="number"
onSliceClick={
isPageLayoutInEditMode || !canRedirectToFilteredView
? undefined
@@ -5,6 +5,7 @@ import { transformAggregateRawValueIntoAggregateDisplayValue } from '@/object-re
import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel';
import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
import { convertAggregateOperationToExtendedAggregateOperation } from '@/object-record/utils/convertAggregateOperationToExtendedAggregateOperation';
import { CHART_NUMBER_FORMAT_DEFAULT } from '@/page-layout/widgets/graph/constants/ChartNumberFormatDefault';
import { useGraphWidgetQueryCommon } from '@/page-layout/widgets/graph/hooks/useGraphWidgetQueryCommon';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { UserContext } from '@/users/contexts/UserContext';
@@ -82,7 +83,9 @@ export const usePieChartCenterMetricData = ({
const { dateFormat, timeFormat, timeZone } = useContext(UserContext);
const dateLocale = useAtomStateValue(dateLocaleState);
const { numberFormat, formatNumber } = useNumberFormat();
const { numberFormat } = useNumberFormat();
const chartNumberFormat =
configuration.numberFormat ?? CHART_NUMBER_FORMAT_DEFAULT;
const aggregateFieldMetadataItem = objectMetadataItem.readableFields.find(
findById(configuration.aggregateFieldMetadataId),
@@ -122,9 +125,21 @@ export const usePieChartCenterMetricData = ({
AggregateOperations.COUNT
];
return isDefined(totalCountValue)
? formatNumber(Number(totalCountValue))
: totalCountValue;
if (!isDefined(totalCountValue)) {
return totalCountValue;
}
return transformAggregateRawValueIntoAggregateDisplayValue({
aggregateFieldMetadataItem,
aggregateOperation: extendedAggregateOperation,
aggregateRawValue: totalCountValue,
dateFormat,
localeCatalog: dateLocale.localeCatalog,
timeFormat,
timeZone,
numberFormat,
chartNumberFormat,
});
}
const aggregateRawValue =
@@ -141,6 +156,7 @@ export const usePieChartCenterMetricData = ({
timeFormat,
timeZone,
numberFormat,
chartNumberFormat,
});
}, [
aggregateFieldMetadataItem,
@@ -152,7 +168,7 @@ export const usePieChartCenterMetricData = ({
timeFormat,
timeZone,
numberFormat,
formatNumber,
chartNumberFormat,
]);
return {
@@ -0,0 +1,19 @@
import { getChartValueDisplayType } from '@/page-layout/widgets/graph/utils/getChartValueDisplayType';
import { ChartNumberFormat } from '~/generated-metadata/graphql';
describe('getChartValueDisplayType', () => {
it('should return number when the format is FULL', () => {
expect(getChartValueDisplayType(ChartNumberFormat.FULL)).toBe('number');
});
it('should return shortNumber when the format is SHORT', () => {
expect(getChartValueDisplayType(ChartNumberFormat.SHORT)).toBe(
'shortNumber',
);
});
it('should fall back to the default format when the format is unset', () => {
expect(getChartValueDisplayType(null)).toBe('shortNumber');
expect(getChartValueDisplayType(undefined)).toBe('shortNumber');
});
});
@@ -0,0 +1,96 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { getChartValueFormatOptions } from '@/page-layout/widgets/graph/utils/getChartValueFormatOptions';
import { formatGraphValue } from '@/page-layout/widgets/graph/utils/graphFormatters';
import { FieldMetadataType } from 'twenty-shared/types';
import {
AggregateOperations,
ChartNumberFormat,
} from '~/generated-metadata/graphql';
describe('getChartValueFormatOptions', () => {
const aggregateFieldMetadataItem = {
id: 'score-field-id',
type: FieldMetadataType.NUMBER,
settings: {
decimals: 2,
type: 'number',
},
} as FieldMetadataItem;
const aggregateCurrencyFieldMetadataItem = {
id: 'revenue-field-id',
type: FieldMetadataType.CURRENCY,
} as FieldMetadataItem;
it.each([
AggregateOperations.AVG,
AggregateOperations.MAX,
AggregateOperations.MIN,
AggregateOperations.SUM,
])(
'should honor the aggregate number field decimal setting for %s',
(aggregateOperation) => {
const formatOptions = getChartValueFormatOptions({
aggregateOperation,
aggregateFieldMetadataId: aggregateFieldMetadataItem.id,
fieldMetadataItems: [aggregateFieldMetadataItem],
numberFormat: ChartNumberFormat.FULL,
});
expect(formatGraphValue(123.4567, formatOptions)).toBe('123.46');
},
);
it.each([
AggregateOperations.AVG,
AggregateOperations.MAX,
AggregateOperations.MIN,
AggregateOperations.SUM,
])(
'should format aggregate currency fields with two decimals for %s',
(aggregateOperation) => {
const formatOptions = getChartValueFormatOptions({
aggregateOperation,
aggregateFieldMetadataId: aggregateCurrencyFieldMetadataItem.id,
fieldMetadataItems: [aggregateCurrencyFieldMetadataItem],
numberFormat: ChartNumberFormat.FULL,
});
expect(formatGraphValue(123.4567, formatOptions)).toBe('123.46');
},
);
it('should keep the short format compact', () => {
const formatOptions = getChartValueFormatOptions({
aggregateOperation: AggregateOperations.SUM,
aggregateFieldMetadataId: aggregateFieldMetadataItem.id,
fieldMetadataItems: [aggregateFieldMetadataItem],
numberFormat: ChartNumberFormat.SHORT,
});
expect(formatGraphValue(123.4567, formatOptions)).toBe('123.5');
});
it.each([
AggregateOperations.COUNT,
AggregateOperations.COUNT_EMPTY,
AggregateOperations.COUNT_FALSE,
AggregateOperations.COUNT_NOT_EMPTY,
AggregateOperations.COUNT_TRUE,
AggregateOperations.COUNT_UNIQUE_VALUES,
AggregateOperations.PERCENTAGE_EMPTY,
AggregateOperations.PERCENTAGE_NOT_EMPTY,
])(
'should ignore the aggregate number field decimal setting for %s',
(aggregateOperation) => {
const formatOptions = getChartValueFormatOptions({
aggregateOperation,
aggregateFieldMetadataId: aggregateFieldMetadataItem.id,
fieldMetadataItems: [aggregateFieldMetadataItem],
numberFormat: ChartNumberFormat.FULL,
});
expect(formatOptions.decimals).toBeUndefined();
},
);
});
@@ -191,4 +191,36 @@ describe('formatGraphValue', () => {
expect(result).toBeDefined();
});
});
describe('formatNumberFn option', () => {
const germanFormatNumberFn = (value: number) =>
value.toLocaleString('de-DE');
it('should use the provided formatNumberFn for number display type', () => {
const result = formatGraphValue(1234567, {
displayType: 'number',
formatNumberFn: germanFormatNumberFn,
});
expect(result).toBe('1.234.567');
});
it('should use the provided formatNumberFn for currency display type', () => {
const result = formatGraphValue(1234567, {
displayType: 'currency',
formatNumberFn: germanFormatNumberFn,
});
expect(result).toBe('$1.234.567');
});
it('should not apply formatNumberFn to abbreviated short numbers', () => {
const result = formatGraphValue(1234567, {
displayType: 'shortNumber',
formatNumberFn: germanFormatNumberFn,
});
expect(result).toBe('1.2m');
});
});
});
@@ -0,0 +1,10 @@
import { CHART_NUMBER_FORMAT_DEFAULT } from '@/page-layout/widgets/graph/constants/ChartNumberFormatDefault';
import { type GraphValueFormatOptions } from '@/page-layout/widgets/graph/utils/graphFormatters';
import { ChartNumberFormat } from '~/generated-metadata/graphql';
export const getChartValueDisplayType = (
numberFormat: ChartNumberFormat | null | undefined,
): GraphValueFormatOptions['displayType'] =>
(numberFormat ?? CHART_NUMBER_FORMAT_DEFAULT) === ChartNumberFormat.FULL
? 'number'
: 'shortNumber';
@@ -0,0 +1,58 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { type AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
import { NON_STANDARD_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/nonStandardAggregateOperationsOptions';
import { getChartValueDisplayType } from '@/page-layout/widgets/graph/utils/getChartValueDisplayType';
import { type GraphValueFormatOptions } from '@/page-layout/widgets/graph/utils/graphFormatters';
import { FieldMetadataType } from 'twenty-shared/types';
import { findById, isDefined } from 'twenty-shared/utils';
import {
type AggregateOperations as GeneratedAggregateOperations,
type ChartNumberFormat,
} from '~/generated-metadata/graphql';
type GetChartValueFormatOptionsParams = {
aggregateOperation: GeneratedAggregateOperations;
aggregateFieldMetadataId: string;
fieldMetadataItems: FieldMetadataItem[];
numberFormat: ChartNumberFormat | null | undefined;
};
export const getChartValueFormatOptions = ({
aggregateOperation,
aggregateFieldMetadataId,
fieldMetadataItems,
numberFormat,
}: GetChartValueFormatOptionsParams): GraphValueFormatOptions => {
const aggregateFieldMetadataItem = fieldMetadataItems.find(
findById(aggregateFieldMetadataId),
);
const aggregateFieldSettings =
aggregateFieldMetadataItem?.type === FieldMetadataType.NUMBER
? aggregateFieldMetadataItem.settings
: undefined;
const aggregateNumberFieldDecimals =
isDefined(aggregateFieldSettings) && 'decimals' in aggregateFieldSettings
? aggregateFieldSettings.decimals
: undefined;
const aggregateFieldDecimals =
aggregateFieldMetadataItem?.type === FieldMetadataType.CURRENCY
? 2
: aggregateNumberFieldDecimals;
const shouldUseAggregateFieldDecimals =
NON_STANDARD_AGGREGATE_OPERATION_OPTIONS.includes(
aggregateOperation as AggregateOperations,
);
const decimals = shouldUseAggregateFieldDecimals
? aggregateFieldDecimals
: undefined;
return {
decimals,
displayType: getChartValueDisplayType(numberFormat),
};
};
@@ -1,3 +1,4 @@
import { CHART_NUMBER_FORMAT_DEFAULT } from '@/page-layout/widgets/graph/constants/ChartNumberFormatDefault';
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
import { useUpdateCurrentWidgetConfig } from '@/side-panel/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode';
@@ -20,13 +21,21 @@ export const ChartNumberFormatSelectionDropdownContent = () => {
const configuration = widgetInEditMode?.configuration;
if (
!isWidgetConfigurationOfType(configuration, 'AggregateChartConfiguration')
) {
const isChartWithNumberFormat =
isWidgetConfigurationOfType(configuration, 'AggregateChartConfiguration') ||
isWidgetConfigurationOfType(configuration, 'BarChartConfiguration') ||
isWidgetConfigurationOfType(configuration, 'LineChartConfiguration') ||
isWidgetConfigurationOfType(configuration, 'PieChartConfiguration');
if (!isChartWithNumberFormat) {
throw new Error('Invalid configuration type');
}
const currentNumberFormat = configuration.numberFormat;
const currentNumberFormat =
configuration.numberFormat ??
(configuration.__typename === 'AggregateChartConfiguration'
? undefined
: CHART_NUMBER_FORMAT_DEFAULT);
const dropdownId = useAvailableComponentInstanceIdOrThrow(
DropdownComponentInstanceContext,
@@ -9,6 +9,7 @@ import { DATA_LABELS_SETTING } from '@/side-panel/pages/page-layout/constants/se
import { DATE_GRANULARITY_X_SETTING } from '@/side-panel/pages/page-layout/constants/settings/DateGranularityXSetting';
import { DATE_GRANULARITY_Y_SETTING } from '@/side-panel/pages/page-layout/constants/settings/DateGranularityYSetting';
import { FILTER_SETTING } from '@/side-panel/pages/page-layout/constants/settings/FilterSetting';
import { FORMAT_SETTING } from '@/side-panel/pages/page-layout/constants/settings/FormatSetting';
import { GROUP_BY_SETTING } from '@/side-panel/pages/page-layout/constants/settings/GroupBySetting';
import { OMIT_NULL_VALUES_SETTING } from '@/side-panel/pages/page-layout/constants/settings/OmitNullValuesSetting';
import { PRIMARY_SORT_BY_SETTING } from '@/side-panel/pages/page-layout/constants/settings/PrimarySortBySetting';
@@ -56,6 +57,7 @@ export const LINE_CHART_SETTINGS: ChartSettingsGroup[] = [
AXIS_NAME_SETTING,
STACKED_LINES_SETTING,
DATA_LABELS_SETTING,
FORMAT_SETTING,
SHOW_LEGEND_SETTING,
],
},
@@ -7,6 +7,7 @@ import { DATA_LABELS_SETTING } from '@/side-panel/pages/page-layout/constants/se
import { DATE_GRANULARITY_SETTING } from '@/side-panel/pages/page-layout/constants/settings/DateGranularitySetting';
import { EACH_SLICE_REPRESENTS_SETTING } from '@/side-panel/pages/page-layout/constants/settings/EachSliceRepresentsSetting';
import { FILTER_SETTING } from '@/side-panel/pages/page-layout/constants/settings/FilterSetting';
import { FORMAT_SETTING } from '@/side-panel/pages/page-layout/constants/settings/FormatSetting';
import { HIDE_EMPTY_CATEGORY_SETTING } from '@/side-panel/pages/page-layout/constants/settings/HideEmptyCategorySetting';
import { PRIMARY_SORT_BY_SETTING } from '@/side-panel/pages/page-layout/constants/settings/PrimarySortBySetting';
import { SHOW_LEGEND_SETTING } from '@/side-panel/pages/page-layout/constants/settings/ShowLegendSetting';
@@ -32,6 +33,7 @@ export const PIE_CHART_SETTINGS: ChartSettingsGroup[] = [
items: [
COLORS_SETTING,
DATA_LABELS_SETTING,
FORMAT_SETTING,
SHOW_LEGEND_SETTING,
CENTER_METRIC_SETTING,
],
@@ -11,6 +11,7 @@ import {
AxisNameDisplay,
BarChartGroupMode,
BarChartLayout,
ChartNumberFormat,
FieldMetadataType,
GraphOrderBy,
RelationType,
@@ -676,4 +677,32 @@ describe('useChartSettingsValues', () => {
});
});
});
describe('Number format setting', () => {
it('should return the Full label for FORMAT when numberFormat is set on a bar chart', () => {
const config = buildBarChartConfiguration({
numberFormat: ChartNumberFormat.FULL,
});
const { result } = renderUseChartSettingsValues(config);
const value = result.current.getChartSettingsValues(
CHART_CONFIGURATION_SETTING_IDS.FORMAT,
);
expect(value).toBe('Full');
});
it('should return the default format label for FORMAT when numberFormat is not set', () => {
const { result } = renderUseChartSettingsValues(
buildBarChartConfiguration({}),
);
const value = result.current.getChartSettingsValues(
CHART_CONFIGURATION_SETTING_IDS.FORMAT,
);
expect(value).toBe('Short');
});
});
});
@@ -1,3 +1,4 @@
import { CHART_NUMBER_FORMAT_DEFAULT } from '@/page-layout/widgets/graph/constants/ChartNumberFormatDefault';
import { useGraphGroupBySortOptionLabels } from '@/side-panel/pages/page-layout/hooks/useGraphGroupBySortOptionLabels';
import { useGraphXSortOptionLabels } from '@/side-panel/pages/page-layout/hooks/useGraphXSortOptionLabels';
import { type ChartConfiguration } from '@/side-panel/pages/page-layout/types/ChartConfiguration';
@@ -300,6 +301,12 @@ export const useChartSettingsValues = ({
? (configuration.displayLegend ?? true)
: true;
case CHART_CONFIGURATION_SETTING_IDS.FORMAT:
if (isBarOrLineChart || isPieChart) {
return getChartNumberFormatLabel(
configuration.numberFormat ?? CHART_NUMBER_FORMAT_DEFAULT,
);
}
return configuration.__typename === 'AggregateChartConfiguration' &&
isDefined(configuration.numberFormat)
? getChartNumberFormatLabel(configuration.numberFormat)
@@ -9,6 +9,7 @@ import { DATA_LABELS_SETTING } from '@/side-panel/pages/page-layout/constants/se
import { DATE_GRANULARITY_X_SETTING } from '@/side-panel/pages/page-layout/constants/settings/DateGranularityXSetting';
import { DATE_GRANULARITY_Y_SETTING } from '@/side-panel/pages/page-layout/constants/settings/DateGranularityYSetting';
import { FILTER_SETTING } from '@/side-panel/pages/page-layout/constants/settings/FilterSetting';
import { FORMAT_SETTING } from '@/side-panel/pages/page-layout/constants/settings/FormatSetting';
import { GROUP_BY_SETTING } from '@/side-panel/pages/page-layout/constants/settings/GroupBySetting';
import { OMIT_NULL_VALUES_SETTING } from '@/side-panel/pages/page-layout/constants/settings/OmitNullValuesSetting';
import { PRIMARY_SORT_BY_SETTING } from '@/side-panel/pages/page-layout/constants/settings/PrimarySortBySetting';
@@ -156,6 +157,7 @@ describe('getBarChartSettings', () => {
AXIS_NAME_SETTING,
STACKED_BARS_SETTING,
DATA_LABELS_SETTING,
FORMAT_SETTING,
SHOW_LEGEND_SETTING,
]);
});
@@ -9,6 +9,7 @@ import { DATA_LABELS_SETTING } from '@/side-panel/pages/page-layout/constants/se
import { DATE_GRANULARITY_X_SETTING } from '@/side-panel/pages/page-layout/constants/settings/DateGranularityXSetting';
import { DATE_GRANULARITY_Y_SETTING } from '@/side-panel/pages/page-layout/constants/settings/DateGranularityYSetting';
import { FILTER_SETTING } from '@/side-panel/pages/page-layout/constants/settings/FilterSetting';
import { FORMAT_SETTING } from '@/side-panel/pages/page-layout/constants/settings/FormatSetting';
import { GROUP_BY_SETTING } from '@/side-panel/pages/page-layout/constants/settings/GroupBySetting';
import { OMIT_NULL_VALUES_SETTING } from '@/side-panel/pages/page-layout/constants/settings/OmitNullValuesSetting';
import { PRIMARY_SORT_BY_SETTING } from '@/side-panel/pages/page-layout/constants/settings/PrimarySortBySetting';
@@ -73,6 +74,7 @@ export const getBarChartSettings = (
AXIS_NAME_SETTING,
STACKED_BARS_SETTING,
DATA_LABELS_SETTING,
FORMAT_SETTING,
SHOW_LEGEND_SETTING,
],
},