Center metric layer on pie chart + cleanup (#16105)

https://github.com/user-attachments/assets/bb48bebe-d0be-4006-8f63-e686c70c0011

---------

Co-authored-by: bosiraphael <raphael.bosi@gmail.com>
This commit is contained in:
nitin
2025-11-27 16:45:48 +05:30
committed by GitHub
parent 1a45576990
commit d7f8c2d338
64 changed files with 343 additions and 826 deletions
@@ -3152,6 +3152,7 @@ export type PieChartConfiguration = {
groupByFieldMetadataId: Scalars['UUID'];
groupBySubFieldName?: Maybe<Scalars['String']>;
orderBy?: Maybe<GraphOrderBy>;
showCenterMetric?: Maybe<Scalars['Boolean']>;
timezone?: Maybe<Scalars['String']>;
};
@@ -3073,6 +3073,7 @@ export type PieChartConfiguration = {
groupByFieldMetadataId: Scalars['UUID'];
groupBySubFieldName?: Maybe<Scalars['String']>;
orderBy?: Maybe<GraphOrderBy>;
showCenterMetric?: Maybe<Scalars['Boolean']>;
timezone?: Maybe<Scalars['String']>;
};
@@ -4781,7 +4782,7 @@ export type SearchQueryVariables = Exact<{
export type SearchQuery = { __typename?: 'Query', search: { __typename?: 'SearchResultConnection', edges: Array<{ __typename?: 'SearchResultEdge', cursor: string, node: { __typename?: 'SearchRecord', recordId: any, objectNameSingular: string, label: string, imageUrl?: string | null, tsRankCD: number, tsRank: number } }>, pageInfo: { __typename?: 'SearchResultPageInfo', hasNextPage: boolean, endCursor?: string | null } } };
export type PageLayoutWidgetFragmentFragment = { __typename?: 'PageLayoutWidget', id: any, title: string, type: WidgetType, objectMetadataId?: any | null, createdAt: string, updatedAt: string, deletedAt?: string | null, pageLayoutTabId: any, gridPosition: { __typename?: 'GridPosition', column: number, columnSpan: number, row: number, rowSpan: number }, configuration?: { __typename?: 'AggregateChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, label?: string | null, displayDataLabel?: boolean | null, format?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'BarChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, groupMode?: BarChartGroupMode | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'GaugeChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'IframeConfiguration', url?: string | null } | { __typename?: 'LineChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, isStacked?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'PieChartConfiguration', graphType: GraphType, groupByFieldMetadataId: any, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, groupBySubFieldName?: string | null, dateGranularity?: ObjectRecordGroupByDateGranularity | null, orderBy?: GraphOrderBy | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | null };
export type PageLayoutWidgetFragmentFragment = { __typename?: 'PageLayoutWidget', id: any, title: string, type: WidgetType, objectMetadataId?: any | null, createdAt: string, updatedAt: string, deletedAt?: string | null, pageLayoutTabId: any, gridPosition: { __typename?: 'GridPosition', column: number, columnSpan: number, row: number, rowSpan: number }, configuration?: { __typename?: 'AggregateChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, label?: string | null, displayDataLabel?: boolean | null, format?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'BarChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, groupMode?: BarChartGroupMode | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'GaugeChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'IframeConfiguration', url?: string | null } | { __typename?: 'LineChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, isStacked?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'PieChartConfiguration', graphType: GraphType, groupByFieldMetadataId: any, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, groupBySubFieldName?: string | null, dateGranularity?: ObjectRecordGroupByDateGranularity | null, orderBy?: GraphOrderBy | null, displayDataLabel?: boolean | null, showCenterMetric?: boolean | null, displayLegend?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | null };
export type UpdatePageLayoutWithTabsAndWidgetsMutationVariables = Exact<{
id: Scalars['String'];
@@ -4789,7 +4790,7 @@ export type UpdatePageLayoutWithTabsAndWidgetsMutationVariables = Exact<{
}>;
export type UpdatePageLayoutWithTabsAndWidgetsMutation = { __typename?: 'Mutation', updatePageLayoutWithTabsAndWidgets: { __typename?: 'PageLayout', id: any, name: string, type: PageLayoutType, objectMetadataId?: any | null, createdAt: string, updatedAt: string, deletedAt?: string | null, tabs?: Array<{ __typename?: 'PageLayoutTab', id: any, title: string, position: number, pageLayoutId: any, createdAt: string, updatedAt: string, widgets?: Array<{ __typename?: 'PageLayoutWidget', id: any, title: string, type: WidgetType, objectMetadataId?: any | null, createdAt: string, updatedAt: string, deletedAt?: string | null, pageLayoutTabId: any, gridPosition: { __typename?: 'GridPosition', column: number, columnSpan: number, row: number, rowSpan: number }, configuration?: { __typename?: 'AggregateChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, label?: string | null, displayDataLabel?: boolean | null, format?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'BarChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, groupMode?: BarChartGroupMode | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'GaugeChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'IframeConfiguration', url?: string | null } | { __typename?: 'LineChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, isStacked?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'PieChartConfiguration', graphType: GraphType, groupByFieldMetadataId: any, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, groupBySubFieldName?: string | null, dateGranularity?: ObjectRecordGroupByDateGranularity | null, orderBy?: GraphOrderBy | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | null }> | null }> | null } };
export type UpdatePageLayoutWithTabsAndWidgetsMutation = { __typename?: 'Mutation', updatePageLayoutWithTabsAndWidgets: { __typename?: 'PageLayout', id: any, name: string, type: PageLayoutType, objectMetadataId?: any | null, createdAt: string, updatedAt: string, deletedAt?: string | null, tabs?: Array<{ __typename?: 'PageLayoutTab', id: any, title: string, position: number, pageLayoutId: any, createdAt: string, updatedAt: string, widgets?: Array<{ __typename?: 'PageLayoutWidget', id: any, title: string, type: WidgetType, objectMetadataId?: any | null, createdAt: string, updatedAt: string, deletedAt?: string | null, pageLayoutTabId: any, gridPosition: { __typename?: 'GridPosition', column: number, columnSpan: number, row: number, rowSpan: number }, configuration?: { __typename?: 'AggregateChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, label?: string | null, displayDataLabel?: boolean | null, format?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'BarChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, groupMode?: BarChartGroupMode | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'GaugeChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'IframeConfiguration', url?: string | null } | { __typename?: 'LineChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, isStacked?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'PieChartConfiguration', graphType: GraphType, groupByFieldMetadataId: any, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, groupBySubFieldName?: string | null, dateGranularity?: ObjectRecordGroupByDateGranularity | null, orderBy?: GraphOrderBy | null, displayDataLabel?: boolean | null, showCenterMetric?: boolean | null, displayLegend?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | null }> | null }> | null } };
export type OnDbEventSubscriptionVariables = Exact<{
input: OnDbEventInput;
@@ -5159,6 +5160,7 @@ export const PageLayoutWidgetFragmentFragmentDoc = gql`
dateGranularity
orderBy
displayDataLabel
showCenterMetric
displayLegend
color
description
@@ -1,3 +1,4 @@
import { CENTER_METRIC_SETTING } from '@/command-menu/pages/page-layout/constants/settings/CenterMetricSetting';
import { CHART_DATA_SOURCE_SETTING } from '@/command-menu/pages/page-layout/constants/settings/ChartDataSourceSetting';
import { COLORS_SETTING } from '@/command-menu/pages/page-layout/constants/settings/ColorsSetting';
import { DATA_DISPLAY_PIE_CHART_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DataDisplayPieChartSetting';
@@ -23,6 +24,11 @@ export const PIE_CHART_SETTINGS: ChartSettingsGroup[] = [
},
{
heading: 'Style',
items: [COLORS_SETTING, DATA_LABELS_SETTING, SHOW_LEGEND_SETTING],
items: [
COLORS_SETTING,
DATA_LABELS_SETTING,
SHOW_LEGEND_SETTING,
CENTER_METRIC_SETTING,
],
},
];
@@ -0,0 +1,11 @@
import { CHART_CONFIGURATION_SETTING_LABELS } from '@/command-menu/pages/page-layout/constants/settings/ChartConfigurationSettingLabels';
import { CHART_CONFIGURATION_SETTING_IDS } from '@/command-menu/pages/page-layout/types/ChartConfigurationSettingIds';
import { type ChartSettingsItem } from '@/command-menu/pages/page-layout/types/ChartSettingsGroup';
import { IconNumber123 } from 'twenty-ui/display';
export const CENTER_METRIC_SETTING: ChartSettingsItem = {
isBoolean: true,
Icon: IconNumber123,
label: CHART_CONFIGURATION_SETTING_LABELS.CENTER_METRIC,
id: CHART_CONFIGURATION_SETTING_IDS.CENTER_METRIC,
};
@@ -3,6 +3,7 @@ import { msg } from '@lingui/core/macro';
export const CHART_CONFIGURATION_SETTING_LABELS = {
SOURCE: msg`Source`,
DATA_LABELS: msg`Data labels`,
CENTER_METRIC: msg`Show value in center`,
COLORS: msg`Colors`,
FILTER: msg`Filter`,
GROUP_BY: msg`Group by`,
@@ -199,6 +199,10 @@ export const useChartSettingsValues = ({
return groupByOrderByLabel;
case CHART_CONFIGURATION_SETTING_IDS.DATA_LABELS:
return configuration.displayDataLabel ?? undefined;
case CHART_CONFIGURATION_SETTING_IDS.CENTER_METRIC:
return isPieChart
? (configuration.showCenterMetric ?? undefined)
: undefined;
case CHART_CONFIGURATION_SETTING_IDS.STACKED_BARS:
return configuration.__typename === 'BarChartConfiguration'
? configuration.groupMode !== 'GROUPED'
@@ -1,6 +1,7 @@
export enum CHART_CONFIGURATION_SETTING_IDS {
SOURCE = 'SOURCE',
DATA_LABELS = 'DATA_LABELS',
CENTER_METRIC = 'CENTER_METRIC',
COLORS = 'COLORS',
FILTER = 'FILTER',
GROUP_BY = 'GROUP_BY',
@@ -26,6 +27,7 @@ export enum CHART_CONFIGURATION_SETTING_IDS {
export const CHART_CONFIGURATION_SETTING_TO_CONFIG_KEY_MAP = {
[CHART_CONFIGURATION_SETTING_IDS.DATA_LABELS]: 'displayDataLabel',
[CHART_CONFIGURATION_SETTING_IDS.CENTER_METRIC]: 'showCenterMetric',
[CHART_CONFIGURATION_SETTING_IDS.STACKED_BARS]: 'groupMode',
[CHART_CONFIGURATION_SETTING_IDS.STACKED_LINES]: 'isStacked',
[CHART_CONFIGURATION_SETTING_IDS.OMIT_NULL_VALUES]: 'omitNullValues',
@@ -26,6 +26,7 @@ import {
PageLayoutType,
type PageLayoutWidget,
} from '~/generated/graphql';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
import { getMockFieldMetadataItemOrThrow } from '~/testing/utils/getMockFieldMetadataItemOrThrow';
import { getMockObjectMetadataItemOrThrow } from '~/testing/utils/getMockObjectMetadataItemOrThrow';
@@ -268,6 +269,7 @@ const meta: Meta<typeof PageLayoutRenderer> = {
title: 'Modules/PageLayout/PageLayoutRenderer',
component: PageLayoutRenderer,
decorators: [
I18nFrontDecorator,
(Story) => (
<MemoryRouter>
<JestMetadataAndApolloMocksWrapper>
@@ -75,6 +75,7 @@ export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql`
dateGranularity
orderBy
displayDataLabel
showCenterMetric
displayLegend
color
description
@@ -53,7 +53,6 @@ export const WithCustomColors: Story = {
min: 0,
max: 100,
color: 'purple',
to: '/metrics/progress',
label: 'Progress',
},
displayType: 'number',
@@ -83,7 +82,6 @@ export const Default: Story = {
value: 0.5,
min: 0,
max: 1,
to: '/metrics/conversion',
label: 'Conversion rate',
},
displayType: 'percentage',
@@ -120,7 +118,6 @@ export const Catalog: Story = {
min: 0,
max: 1,
label: 'Percentage',
to: '/metrics/catalog',
},
displayType: 'percentage' as const,
id: `gauge-chart-catalog-${value}`,
@@ -157,7 +154,6 @@ export const WithoutValue: Story = {
value: 65,
min: 0,
max: 100,
to: '/metrics/conversion-without-value',
label: 'Conversion rate',
},
displayType: 'percentage',
@@ -186,7 +182,6 @@ export const Revenue: Story = {
value: 750,
min: 0,
max: 1000,
to: '/financials/revenue',
label: 'Revenue',
},
displayType: 'shortNumber',
@@ -215,7 +210,6 @@ export const Temperature: Story = {
value: 22,
min: -10,
max: 40,
to: '/sensors/temperature',
label: 'Temperature',
},
suffix: '°C',
@@ -244,7 +238,6 @@ export const Storage: Story = {
value: 384,
min: 0,
max: 512,
to: '/system/storage',
label: 'Storage Used',
},
suffix: ' GB',
@@ -273,7 +266,6 @@ export const Rating: Story = {
value: 4.2,
min: 0,
max: 5,
to: '/reviews/rating',
label: 'Average Rating',
},
suffix: ' ⭐',
@@ -303,7 +295,6 @@ export const WithoutLegend: Story = {
value: 65,
min: 0,
max: 100,
to: '/metrics/conversion-no-legend',
label: 'Conversion rate',
},
displayType: 'percentage',
@@ -1,12 +1,13 @@
import { type Meta, type StoryObj } from '@storybook/react';
import { CatalogDecorator, ComponentDecorator } from 'twenty-ui/testing';
import { GraphWidgetPieChart } from '@/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChart';
import { CatalogDecorator, ComponentDecorator } from 'twenty-ui/testing';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
const meta: Meta<typeof GraphWidgetPieChart> = {
title: 'Modules/PageLayout/Widgets/GraphWidgetPieChart',
component: GraphWidgetPieChart,
decorators: [ComponentDecorator],
decorators: [ComponentDecorator, I18nFrontDecorator],
parameters: {
layout: 'centered',
},
@@ -33,6 +34,12 @@ const meta: Meta<typeof GraphWidgetPieChart> = {
id: {
control: 'text',
},
showCenterMetric: {
control: 'boolean',
},
showDataLabels: {
control: 'boolean',
},
},
};
@@ -43,91 +50,14 @@ const Container = ({ children }: { children: React.ReactNode }) => (
<div style={{ width: '300px', height: '300px' }}>{children}</div>
);
export const WithCustomColors: Story = {
args: {
data: [
{
id: 'segment1',
value: 30,
label: 'Segment A',
color: 'blue',
to: '/segments/a',
},
{
id: 'segment2',
value: 25,
label: 'Segment B',
color: 'purple',
to: '/segments/b',
},
{
id: 'segment3',
value: 20,
label: 'Segment C',
color: 'turquoise',
to: '/segments/c',
},
{
id: 'segment4',
value: 15,
label: 'Segment D',
color: 'orange',
to: '/segments/d',
},
{
id: 'segment5',
value: 10,
label: 'Segment E',
color: 'pink',
to: '/segments/e',
},
],
showLegend: true,
id: 'pie-chart-custom-colors',
},
render: (args) => (
<Container>
<GraphWidgetPieChart
data={args.data}
displayType={args.displayType}
prefix={args.prefix}
suffix={args.suffix}
decimals={args.decimals}
showLegend={args.showLegend}
id={args.id}
/>
</Container>
),
};
export const Default: Story = {
args: {
data: [
{
id: 'qualified',
value: 35,
label: 'Qualified',
to: '/leads/qualified',
},
{
id: 'contacted',
value: 25,
label: 'Contacted',
to: '/leads/contacted',
},
{
id: 'unqualified',
value: 20,
label: 'Unqualified',
to: '/leads/unqualified',
},
{ id: 'proposal', value: 15, label: 'Proposal', to: '/leads/proposal' },
{
id: 'negotiation',
value: 5,
label: 'Negotiation',
to: '/leads/negotiation',
},
{ id: 'Qualified', value: 35 },
{ id: 'Contacted', value: 25 },
{ id: 'Unqualified', value: 20 },
{ id: 'Proposal', value: 15 },
{ id: 'Negotiation', value: 5 },
],
showLegend: true,
id: 'pie-chart-default',
@@ -147,33 +77,54 @@ export const Default: Story = {
),
};
export const WithCenterMetric: Story = {
args: {
data: [
{ id: 'Qualified', value: 35 },
{ id: 'Contacted', value: 25 },
{ id: 'Unqualified', value: 20 },
],
showCenterMetric: true,
id: 'pie-chart-with-center-metric',
},
render: (args) => (
<Container>
<GraphWidgetPieChart
data={args.data}
showCenterMetric={args.showCenterMetric}
id={args.id}
/>
</Container>
),
};
export const WithDataLabels: Story = {
args: {
data: [
{ id: 'Qualified', value: 35 },
{ id: 'Contacted', value: 25 },
{ id: 'Unqualified', value: 20 },
],
showDataLabels: true,
id: 'pie-chart-with-data-labels',
},
render: (args) => (
<Container>
<GraphWidgetPieChart
data={args.data}
showDataLabels={args.showDataLabels}
id={args.id}
/>
</Container>
),
};
export const Revenue: Story = {
args: {
data: [
{
id: 'product-a',
value: 420000,
label: 'Product A',
to: '/products/a/revenue',
},
{
id: 'product-b',
value: 380000,
label: 'Product B',
to: '/products/b/revenue',
},
{
id: 'product-c',
value: 250000,
label: 'Product C',
to: '/products/c/revenue',
},
{
id: 'product-d',
value: 180000,
label: 'Product D',
to: '/products/d/revenue',
},
{ id: 'Product A', value: 420000 },
{ id: 'Product B', value: 380000 },
{ id: 'Product C', value: 250000 },
{ id: 'Product D', value: 180000 },
],
prefix: '$',
displayType: 'shortNumber',
@@ -198,19 +149,9 @@ export const Revenue: Story = {
export const TaskStatus: Story = {
args: {
data: [
{
id: 'completed',
value: 45,
label: 'Completed',
to: '/tasks/completed',
},
{
id: 'in-progress',
value: 30,
label: 'In Progress',
to: '/tasks/in-progress',
},
{ id: 'todo', value: 25, label: 'To Do', to: '/tasks/todo' },
{ id: 'Completed', value: 45 },
{ id: 'In Progress', value: 30 },
{ id: 'To Do', value: 25 },
],
displayType: 'percentage',
showLegend: true,
@@ -234,8 +175,8 @@ export const TaskStatus: Story = {
export const TwoSlices: Story = {
args: {
data: [
{ id: 'active', value: 75, label: 'Active', to: '/users/active' },
{ id: 'inactive', value: 25, label: 'Inactive', to: '/users/inactive' },
{ id: 'Active', value: 75 },
{ id: 'Inactive', value: 25 },
],
displayType: 'percentage',
showLegend: true,
@@ -259,14 +200,14 @@ export const TwoSlices: Story = {
export const ManySlices: Story = {
args: {
data: [
{ id: 'category-1', value: 20, label: 'Category 1', to: '/categories/1' },
{ id: 'category-2', value: 18, label: 'Category 2', to: '/categories/2' },
{ id: 'category-3', value: 16, label: 'Category 3', to: '/categories/3' },
{ id: 'category-4', value: 14, label: 'Category 4', to: '/categories/4' },
{ id: 'category-5', value: 12, label: 'Category 5', to: '/categories/5' },
{ id: 'category-6', value: 10, label: 'Category 6', to: '/categories/6' },
{ id: 'category-7', value: 6, label: 'Category 7' },
{ id: 'category-8', value: 4, label: 'Category 8' },
{ id: 'Category 1', value: 20 },
{ id: 'Category 2', value: 18 },
{ id: 'Category 3', value: 16 },
{ id: 'Category 4', value: 14 },
{ id: 'Category 5', value: 12 },
{ id: 'Category 6', value: 10 },
{ id: 'Category 7', value: 6 },
{ id: 'Category 8', value: 4 },
],
showLegend: true,
id: 'pie-chart-many-slices',
@@ -289,9 +230,9 @@ export const ManySlices: Story = {
export const WithoutLegend: Story = {
args: {
data: [
{ id: 'web', value: 45, label: 'Web', to: '/platforms/web' },
{ id: 'mobile', value: 35, label: 'Mobile', to: '/platforms/mobile' },
{ id: 'desktop', value: 20, label: 'Desktop', to: '/platforms/desktop' },
{ id: 'Web', value: 45 },
{ id: 'Mobile', value: 35 },
{ id: 'Desktop', value: 20 },
],
displayType: 'percentage',
showLegend: false,
@@ -315,10 +256,10 @@ export const WithoutLegend: Story = {
export const MarketShare: Story = {
args: {
data: [
{ id: 'brand-a', value: 35.5, label: 'Brand A', to: '/market/brand-a' },
{ id: 'brand-b', value: 28.2, label: 'Brand B', to: '/market/brand-b' },
{ id: 'brand-c', value: 18.7, label: 'Brand C', to: '/market/brand-c' },
{ id: 'others', value: 17.6, label: 'Others', to: '/market/others' },
{ id: 'Brand A', value: 35.5 },
{ id: 'Brand B', value: 28.2 },
{ id: 'Brand C', value: 18.7 },
{ id: 'Others', value: 17.6 },
],
displayType: 'percentage',
showLegend: true,
@@ -342,20 +283,10 @@ export const MarketShare: Story = {
export const Storage: Story = {
args: {
data: [
{
id: 'documents',
value: 125,
label: 'Documents',
to: '/storage/documents',
},
{ id: 'media', value: 280, label: 'Media', to: '/storage/media' },
{
id: 'applications',
value: 95,
label: 'Applications',
to: '/storage/applications',
},
{ id: 'system', value: 50, label: 'System', to: '/storage/system' },
{ id: 'Documents', value: 125 },
{ id: 'Media', value: 280 },
{ id: 'Applications', value: 95 },
{ id: 'System', value: 50 },
],
suffix: ' GB',
showLegend: true,
@@ -377,7 +308,7 @@ export const Storage: Story = {
};
export const Catalog: Story = {
decorators: [CatalogDecorator],
decorators: [CatalogDecorator, I18nFrontDecorator],
parameters: {
catalog: {
dimensions: [
@@ -387,23 +318,23 @@ export const Catalog: Story = {
props: (sliceCount: number) => {
const dataMap: Record<
number,
Array<{ id: string; value: number; label?: string }>
Array<{ id: string; value: number }>
> = {
2: [
{ id: 'yes', value: 65, label: 'Yes' },
{ id: 'no', value: 35, label: 'No' },
{ id: 'Yes', value: 65 },
{ id: 'No', value: 35 },
],
3: [
{ id: 'gold', value: 45, label: 'Gold' },
{ id: 'silver', value: 35, label: 'Silver' },
{ id: 'bronze', value: 20, label: 'Bronze' },
{ id: 'Gold', value: 45 },
{ id: 'Silver', value: 35 },
{ id: 'Bronze', value: 20 },
],
5: [
{ id: 'item-1', value: 30, label: 'Item 1' },
{ id: 'item-2', value: 25, label: 'Item 2' },
{ id: 'item-3', value: 20, label: 'Item 3' },
{ id: 'item-4', value: 15, label: 'Item 4' },
{ id: 'item-5', value: 10, label: 'Item 5' },
{ id: 'Item 1', value: 30 },
{ id: 'Item 2', value: 25 },
{ id: 'Item 3', value: 20 },
{ id: 'Item 4', value: 15 },
{ id: 'Item 5', value: 10 },
],
};
@@ -1,16 +1,12 @@
import { GraphDataLabel } from '@/page-layout/widgets/graph/components/GraphDataLabel';
import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem';
import { type BarChartLabelData } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartLabelData';
import { BarChartLayout } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartLayout';
import { computeBarChartGroupedLabels } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/computeBarChartGroupedLabels';
import { computeBarChartStackedLabels } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/computeBarChartStackedLabels';
import { type GraphLabelData } from '@/page-layout/widgets/graph/types/GraphLabelData';
import { type BarCustomLayerProps } from '@nivo/bar';
import { type BarCustomLayerProps, type BarDatum } from '@nivo/bar';
type CustomTotalsLayerProps = Pick<
BarCustomLayerProps<BarChartDataItem>,
'bars'
> & {
type CustomTotalsLayerProps = Pick<BarCustomLayerProps<BarDatum>, 'bars'> & {
formatValue?: (value: number) => string;
offset?: number;
layout?: BarChartLayout;
@@ -1,12 +1,11 @@
import { GraphWidgetFloatingTooltip } from '@/page-layout/widgets/graph/components/GraphWidgetFloatingTooltip';
import { graphWidgetBarTooltipComponentState } from '@/page-layout/widgets/graph/graphWidgetBarChart/states/graphWidgetBarTooltipComponentState';
import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem';
import { type BarChartEnrichedKey } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartEnrichedKey';
import { getBarChartTooltipData } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartTooltipData';
import { getTooltipReferenceFromBarChartElementAnchor } from '@/page-layout/widgets/graph/utils/getTooltipReferenceFromBarChartElementAnchor';
import { type GraphValueFormatOptions } from '@/page-layout/widgets/graph/utils/graphFormatters';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { type ComputedDatum } from '@nivo/bar';
import { type BarDatum, type ComputedDatum } from '@nivo/bar';
import { isDefined } from 'twenty-shared/utils';
type GraphBarChartTooltipProps = {
@@ -15,7 +14,7 @@ type GraphBarChartTooltipProps = {
formatOptions: GraphValueFormatOptions;
enableGroupTooltip?: boolean;
layout?: 'vertical' | 'horizontal';
onBarClick?: (datum: ComputedDatum<BarChartDataItem>) => void;
onBarClick?: (datum: ComputedDatum<BarDatum>) => void;
onMouseEnter?: () => void;
onMouseLeave?: () => void;
};
@@ -6,7 +6,6 @@ import { GraphBarChartTooltip } from '@/page-layout/widgets/graph/graphWidgetBar
import { BAR_CHART_MINIMUM_INNER_PADDING } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartMinimumInnerPadding';
import { useBarChartData } from '@/page-layout/widgets/graph/graphWidgetBarChart/hooks/useBarChartData';
import { useBarChartTheme } from '@/page-layout/widgets/graph/graphWidgetBarChart/hooks/useBarChartTheme';
import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem';
import { BarChartLayout } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartLayout';
import { type BarChartSeries } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartSeries';
import { calculateStackedBarChartValueRange } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/calculateStackedBarChartValueRange';
@@ -25,6 +24,7 @@ import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import {
ResponsiveBar,
type BarDatum,
type BarItemProps,
type ComputedBarDatum,
type ComputedDatum,
@@ -37,7 +37,7 @@ import { graphWidgetBarTooltipComponentState } from '@/page-layout/widgets/graph
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
type GraphWidgetBarChartProps = {
data: BarChartDataItem[];
data: BarDatum[];
indexBy: string;
keys: string[];
series?: BarChartSeries[];
@@ -53,7 +53,7 @@ type GraphWidgetBarChartProps = {
rangeMin?: number;
rangeMax?: number;
omitNullValues?: boolean;
onBarClick?: (datum: ComputedDatum<BarChartDataItem>) => void;
onBarClick?: (datum: ComputedDatum<BarDatum>) => void;
} & GraphValueFormatOptions;
const StyledContainer = styled.div`
@@ -131,10 +131,7 @@ export const GraphWidgetBarChart = ({
const handleTooltipMouseLeave = debouncedHideTooltip;
const handleBarEnter = useCallback(
(
datum: ComputedDatum<BarChartDataItem>,
event: MouseEvent<SVGRectElement>,
) => {
(datum: ComputedDatum<BarDatum>, event: MouseEvent<SVGRectElement>) => {
debouncedHideTooltip.cancel();
setActiveBarTooltip({
datum,
@@ -162,7 +159,7 @@ export const GraphWidgetBarChart = ({
});
const BarItemWithContext = useMemo(
() => (props: BarItemProps<BarChartDataItem>) => (
() => (props: BarItemProps<BarDatum>) => (
<CustomBarItem
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
@@ -180,7 +177,7 @@ export const GraphWidgetBarChart = ({
const TotalsLayer = ({
bars,
}: {
bars: readonly ComputedBarDatum<BarChartDataItem>[];
bars: readonly ComputedBarDatum<BarDatum>[];
}) => (
<CustomTotalsLayer
bars={bars}
@@ -2,13 +2,12 @@ import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPag
import { ChartSkeletonLoader } from '@/page-layout/widgets/graph/components/ChartSkeletonLoader';
import { GraphWidgetChartHasTooManyGroupsEffect } from '@/page-layout/widgets/graph/components/GraphWidgetChartHasTooManyGroupsEffect';
import { useGraphBarChartWidgetData } from '@/page-layout/widgets/graph/graphWidgetBarChart/hooks/useGraphBarChartWidgetData';
import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem';
import { getEffectiveGroupMode } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getEffectiveGroupMode';
import { buildChartDrilldownQueryParams } from '@/page-layout/widgets/graph/utils/buildChartDrilldownQueryParams';
import { generateChartAggregateFilterKey } from '@/page-layout/widgets/graph/utils/generateChartAggregateFilterKey';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { coreIndexViewIdFromObjectMetadataItemFamilySelector } from '@/views/states/selectors/coreIndexViewIdFromObjectMetadataItemFamilySelector';
import { type ComputedDatum } from '@nivo/bar';
import { type BarDatum, type ComputedDatum } from '@nivo/bar';
import { lazy, Suspense } from 'react';
import { useNavigate } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
@@ -76,7 +75,7 @@ export const GraphWidgetBarChartRenderer = ({
}),
);
const handleBarClick = (datum: ComputedDatum<BarChartDataItem>) => {
const handleBarClick = (datum: ComputedDatum<BarDatum>) => {
const displayValue = datum.data[indexBy];
const rawValue = formattedToRawLookup.get(displayValue as string) ?? null;
@@ -1,6 +1,6 @@
import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem';
import { type BarChartSeries } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartSeries';
import { type GraphColorRegistry } from '@/page-layout/widgets/graph/types/GraphColorRegistry';
import { type BarDatum } from '@nivo/bar';
import { renderHook } from '@testing-library/react';
import { useBarChartData } from '../useBarChartData';
@@ -56,7 +56,7 @@ describe('useBarChartData', () => {
},
};
const mockData: BarChartDataItem[] = [
const mockData: BarDatum[] = [
{ month: 'Jan', sales: 100, costs: 80 },
{ month: 'Feb', sales: 120, costs: 90 },
{ month: 'Mar', sales: 150, costs: 100 },
@@ -1,13 +1,13 @@
import { type BarChartConfig } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartConfig';
import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem';
import { type BarChartEnrichedKey } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartEnrichedKey';
import { type BarChartSeries } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartSeries';
import { type GraphColorRegistry } from '@/page-layout/widgets/graph/types/GraphColorRegistry';
import { getColorScheme } from '@/page-layout/widgets/graph/utils/getColorScheme';
import { type BarDatum } from '@nivo/bar';
import { useMemo } from 'react';
type UseBarChartDataProps = {
data: BarChartDataItem[];
data: BarDatum[];
indexBy: string;
keys: string[];
series?: BarChartSeries[];
@@ -1,10 +1,9 @@
import { GraphWidgetComponentInstanceContext } from '@/page-layout/widgets/graph/states/contexts/GraphWidgetComponentInstanceContext';
import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem';
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
import { type ComputedDatum } from '@nivo/bar';
import { type BarDatum, type ComputedDatum } from '@nivo/bar';
export const graphWidgetBarTooltipComponentState = createComponentState<{
datum: ComputedDatum<BarChartDataItem>;
datum: ComputedDatum<BarDatum>;
anchorElement: Element;
} | null>({
key: 'graphWidgetBarTooltipComponentState',
@@ -1,5 +0,0 @@
import { type BarDatum } from '@nivo/bar';
export type BarChartDataItem = BarDatum & {
to?: string;
};
@@ -1,11 +1,10 @@
import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem';
import { BarChartLayout } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartLayout';
import { type ComputedBarDatum } from '@nivo/bar';
import { type BarDatum, type ComputedBarDatum } from '@nivo/bar';
import { calculateBarChartEndLineCoordinates } from '../calculateBarChartEndLineCoordinates';
describe('calculateBarChartEndLineCoordinates', () => {
const createMockBar = (
overrides?: Partial<ComputedBarDatum<BarChartDataItem>>,
): ComputedBarDatum<BarChartDataItem> =>
overrides?: Partial<ComputedBarDatum<BarDatum>>,
): ComputedBarDatum<BarDatum> =>
({
x: 100,
y: 50,
@@ -17,13 +16,13 @@ describe('calculateBarChartEndLineCoordinates', () => {
value: 100,
index: 0,
indexValue: 'Q1',
data: { Q1: 100 } as BarChartDataItem,
data: { Q1: 100 } as BarDatum,
formattedValue: '100',
hidden: false,
},
label: 'Sales',
...overrides,
}) as ComputedBarDatum<BarChartDataItem>;
}) as ComputedBarDatum<BarDatum>;
describe('vertical layout', () => {
it('should calculate horizontal line coordinates at the top of vertical bars', () => {
const mockBar = createMockBar();
@@ -1,9 +1,9 @@
import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem';
import { type BarDatum } from '@nivo/bar';
import { calculateValueRangeFromBarChartKeys } from '../calculateValueRangeFromBarChartKeys';
describe('calculateValueRangeFromBarChartKeys (essential cases)', () => {
it('returns minimum=0 and maximum=highest value for all positive values', () => {
const data: BarChartDataItem[] = [
const data: BarDatum[] = [
{ category: 'A', v1: 10, v2: 20 },
{ category: 'B', v1: 30, v2: 15 },
{ category: 'C', v1: 25, v2: 40 },
@@ -17,7 +17,7 @@ describe('calculateValueRangeFromBarChartKeys (essential cases)', () => {
});
it('returns minimum=lowest and maximum=0 for all negative values', () => {
const data: BarChartDataItem[] = [
const data: BarDatum[] = [
{ category: 'A', v1: -10, v2: -20 },
{ category: 'B', v1: -30, v2: -15 },
{ category: 'C', v1: -25, v2: -40 },
@@ -31,7 +31,7 @@ describe('calculateValueRangeFromBarChartKeys (essential cases)', () => {
});
it('includes zero and spans minimum/maximum when values cross zero', () => {
const data: BarChartDataItem[] = [
const data: BarDatum[] = [
{ category: 'A', v1: -20, v2: 30 },
{ category: 'B', v1: 15, v2: -10 },
{ category: 'C', v1: -5, v2: 25 },
@@ -58,7 +58,7 @@ describe('calculateValueRangeFromBarChartKeys (essential cases)', () => {
});
it('ignores NaN/missing values', () => {
const data: BarChartDataItem[] = [
const data: BarDatum[] = [
{ category: 'A', v1: 10, v2: NaN },
{ category: 'B', v1: 20, v2: 30 },
{ category: 'C', v1: undefined as unknown as number },
@@ -1,9 +1,9 @@
import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem';
import { type BarDatum } from '@nivo/bar';
import { calculateStackedBarChartValueRange } from '../calculateStackedBarChartValueRange';
describe('calculateStackedBarChartValueRange (essential cases)', () => {
it('returns minimum=0 and maximum=largest positive stack', () => {
const data: BarChartDataItem[] = [
const data: BarDatum[] = [
{ cat: 'A', v1: 100, v2: 200, v3: 50 },
{ cat: 'B', v1: 150, v2: 25, v3: 75 },
{ cat: 'C', v1: 300, v2: 10, v3: 0 },
@@ -17,7 +17,7 @@ describe('calculateStackedBarChartValueRange (essential cases)', () => {
});
it('returns minimum=most negative stack and maximum=0 for all negative values', () => {
const data: BarChartDataItem[] = [
const data: BarDatum[] = [
{ cat: 'A', v1: -100, v2: -200, v3: 0 },
{ cat: 'B', v1: -50, v2: -25, v3: -75 },
];
@@ -30,7 +30,7 @@ describe('calculateStackedBarChartValueRange (essential cases)', () => {
});
it('sums positives and negatives per index to compute range when values mix', () => {
const data: BarChartDataItem[] = [
const data: BarDatum[] = [
{ cat: 'A', v1: 100, v2: -60, v3: 20 },
{ cat: 'B', v1: 50, v2: -80, v3: -30 },
{ cat: 'C', v1: 10, v2: 0, v3: 0 },
@@ -54,7 +54,7 @@ describe('calculateStackedBarChartValueRange (essential cases)', () => {
});
it('ignores missing keys and NaN values', () => {
const data: BarChartDataItem[] = [
const data: BarDatum[] = [
{ cat: 'A', v1: 10 },
{ cat: 'B', v2: 30 },
{ cat: 'C', v1: NaN as number },
@@ -1,5 +1,4 @@
import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem';
import { type ComputedBarDatum } from '@nivo/bar';
import { type BarDatum, type ComputedBarDatum } from '@nivo/bar';
import { computeBarChartStackedLabels } from '../computeBarChartStackedLabels';
const createMockBar = (
@@ -10,7 +9,7 @@ const createMockBar = (
y: number,
width: number,
height: number,
): ComputedBarDatum<BarChartDataItem> =>
): ComputedBarDatum<BarDatum> =>
({
id,
key: id,
@@ -25,11 +24,11 @@ const createMockBar = (
color: 'blue',
label: String(indexValue),
formattedValue: String(value),
}) as unknown as ComputedBarDatum<BarChartDataItem>;
}) as unknown as ComputedBarDatum<BarDatum>;
describe('computeBarChartStackedLabels (essential cases)', () => {
it('returns total for single index with all positive values', () => {
const bars: ComputedBarDatum<BarChartDataItem>[] = [
const bars: ComputedBarDatum<BarDatum>[] = [
createMockBar('series1', 'Jan', 100, 50, 200, 30, 100),
createMockBar('series2', 'Jan', 50, 50, 100, 30, 100),
createMockBar('series3', 'Jan', 25, 50, 50, 30, 50),
@@ -48,7 +47,7 @@ describe('computeBarChartStackedLabels (essential cases)', () => {
});
it('returns total for single index with all negative values', () => {
const bars: ComputedBarDatum<BarChartDataItem>[] = [
const bars: ComputedBarDatum<BarDatum>[] = [
createMockBar('series1', 'Jan', -100, 50, 400, 30, 100),
createMockBar('series2', 'Jan', -50, 50, 500, 30, 100),
createMockBar('series3', 'Jan', -25, 50, 575, 30, 50),
@@ -67,7 +66,7 @@ describe('computeBarChartStackedLabels (essential cases)', () => {
});
it('returns total for single index with mixed positive/negative (net positive)', () => {
const bars: ComputedBarDatum<BarChartDataItem>[] = [
const bars: ComputedBarDatum<BarDatum>[] = [
createMockBar('series1', 'Jan', 100, 50, 200, 30, 100),
createMockBar('series2', 'Jan', -30, 50, 370, 30, 70),
createMockBar('series3', 'Jan', 20, 50, 250, 30, 50),
@@ -86,7 +85,7 @@ describe('computeBarChartStackedLabels (essential cases)', () => {
});
it('returns total for single index with mixed positive/negative (net negative)', () => {
const bars: ComputedBarDatum<BarChartDataItem>[] = [
const bars: ComputedBarDatum<BarDatum>[] = [
createMockBar('series1', 'Jan', -100, 50, 400, 30, 100),
createMockBar('series2', 'Jan', 30, 50, 270, 30, 70),
createMockBar('series3', 'Jan', 20, 50, 280, 30, 50),
@@ -105,7 +104,7 @@ describe('computeBarChartStackedLabels (essential cases)', () => {
});
it('returns multiple totals for multiple indices', () => {
const bars: ComputedBarDatum<BarChartDataItem>[] = [
const bars: ComputedBarDatum<BarDatum>[] = [
createMockBar('series1', 'Jan', 100, 50, 200, 30, 100),
createMockBar('series2', 'Jan', 50, 50, 100, 30, 100),
createMockBar('series1', 'Feb', 75, 150, 225, 30, 75),
@@ -139,7 +138,7 @@ describe('computeBarChartStackedLabels (essential cases)', () => {
});
it('handles single bar per index (no stacking)', () => {
const bars: ComputedBarDatum<BarChartDataItem>[] = [
const bars: ComputedBarDatum<BarDatum>[] = [
createMockBar('series1', 'Jan', 100, 50, 200, 30, 100),
createMockBar('series1', 'Feb', 150, 150, 150, 30, 150),
createMockBar('series1', 'Mar', -50, 250, 400, 30, 50),
@@ -179,7 +178,7 @@ describe('computeBarChartStackedLabels (essential cases)', () => {
});
it('handles zero values correctly', () => {
const bars: ComputedBarDatum<BarChartDataItem>[] = [
const bars: ComputedBarDatum<BarDatum>[] = [
createMockBar('series1', 'Jan', 100, 50, 200, 30, 100),
createMockBar('series2', 'Jan', 0, 50, 300, 30, 0),
createMockBar('series3', 'Jan', -100, 50, 400, 30, 100),
@@ -203,7 +202,7 @@ describe('computeBarChartStackedLabels (essential cases)', () => {
});
it('handles numeric index values (not just strings)', () => {
const bars: ComputedBarDatum<BarChartDataItem>[] = [
const bars: ComputedBarDatum<BarDatum>[] = [
createMockBar('series1', 2024, 100, 50, 200, 30, 100),
createMockBar('series2', 2024, 50, 50, 100, 30, 100),
createMockBar('series1', 2025, 75, 150, 225, 30, 75),
@@ -221,7 +220,7 @@ describe('computeBarChartStackedLabels (essential cases)', () => {
});
it('tracks minimum Y position correctly for positive totals', () => {
const bars: ComputedBarDatum<BarChartDataItem>[] = [
const bars: ComputedBarDatum<BarDatum>[] = [
createMockBar('series1', 'Jan', 50, 100, 250, 30, 50),
createMockBar('series2', 'Jan', 100, 100, 150, 30, 100),
createMockBar('series3', 'Jan', 25, 100, 325, 30, 25),
@@ -233,7 +232,7 @@ describe('computeBarChartStackedLabels (essential cases)', () => {
});
it('tracks maximum bottom Y position correctly for negative totals', () => {
const bars: ComputedBarDatum<BarChartDataItem>[] = [
const bars: ComputedBarDatum<BarDatum>[] = [
createMockBar('series1', 'Jan', -50, 100, 350, 30, 50),
createMockBar('series2', 'Jan', -100, 100, 400, 30, 100),
createMockBar('series3', 'Jan', -25, 100, 325, 30, 25),
@@ -245,7 +244,7 @@ describe('computeBarChartStackedLabels (essential cases)', () => {
});
it('calculates center positions correctly', () => {
const bars: ComputedBarDatum<BarChartDataItem>[] = [
const bars: ComputedBarDatum<BarDatum>[] = [
createMockBar('series1', 'Jan', 100, 50, 200, 40, 100),
createMockBar('series2', 'Jan', 50, 60, 150, 20, 50),
];
@@ -1,9 +1,8 @@
import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem';
import { BarChartLayout } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartLayout';
import { type ComputedBarDatum } from '@nivo/bar';
import { type BarDatum, type ComputedBarDatum } from '@nivo/bar';
export const calculateBarChartEndLineCoordinates = (
bar: ComputedBarDatum<BarChartDataItem>,
bar: ComputedBarDatum<BarDatum>,
layout: BarChartLayout,
) => {
if (layout === BarChartLayout.VERTICAL) {
@@ -1,9 +1,9 @@
import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem';
import { calculateValueRangeFromValues } from '@/page-layout/widgets/graph/utils/calculateValueRangeFromValues';
import { type ChartValueRange } from '@/page-layout/widgets/graph/types/ChartValueRange';
import { calculateValueRangeFromValues } from '@/page-layout/widgets/graph/utils/calculateValueRangeFromValues';
import { type BarDatum } from '@nivo/bar';
export const calculateStackedBarChartValueRange = (
data: BarChartDataItem[],
data: BarDatum[],
keys: string[],
): ChartValueRange => {
const stackedValues: number[] = [];
@@ -1,9 +1,9 @@
import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem';
import { calculateValueRangeFromValues } from '@/page-layout/widgets/graph/utils/calculateValueRangeFromValues';
import { type ChartValueRange } from '@/page-layout/widgets/graph/types/ChartValueRange';
import { calculateValueRangeFromValues } from '@/page-layout/widgets/graph/utils/calculateValueRangeFromValues';
import { type BarDatum } from '@nivo/bar';
export const calculateValueRangeFromBarChartKeys = (
data: BarChartDataItem[],
data: BarDatum[],
keys: string[],
): ChartValueRange => {
const values: number[] = [];
@@ -1,9 +1,9 @@
import { BAR_CHART_MINIMUM_WIDTH_PER_TICK } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartMinimumWidthPerTick';
import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem';
import { BarChartLayout } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartLayout';
import { computeMinHeightPerTick } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/computeMinHeightPerTick';
import { getBarChartMargins } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartMargins';
import { computeChartCategoryTickValues } from '@/page-layout/widgets/graph/utils/computeChartCategoryTickValues';
import { type BarDatum } from '@nivo/bar';
export const computeBarChartCategoryTickValues = ({
axisSize,
@@ -16,7 +16,7 @@ export const computeBarChartCategoryTickValues = ({
}: {
axisSize: number;
axisFontSize: number;
data: BarChartDataItem[];
data: BarDatum[];
indexBy: string;
layout: BarChartLayout;
xAxisLabel?: string;
@@ -1,9 +1,8 @@
import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem';
import { type BarChartLabelData } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartLabelData';
import { type ComputedBarDatum } from '@nivo/bar';
import { type BarDatum, type ComputedBarDatum } from '@nivo/bar';
export const computeBarChartGroupedLabels = (
bars: readonly ComputedBarDatum<BarChartDataItem>[],
bars: readonly ComputedBarDatum<BarDatum>[],
): BarChartLabelData[] => {
return bars.map((bar) => {
const value = Number(bar.data.value);
@@ -1,10 +1,9 @@
import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem';
import { type BarChartLabelData } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartLabelData';
import { type ComputedBarDatum } from '@nivo/bar';
import { type BarDatum, type ComputedBarDatum } from '@nivo/bar';
import { isDefined } from 'twenty-shared/utils';
export const computeBarChartStackedLabels = (
bars: readonly ComputedBarDatum<BarChartDataItem>[],
bars: readonly ComputedBarDatum<BarDatum>[],
): BarChartLabelData[] => {
const stackData = new Map<
string,
@@ -13,7 +12,7 @@ export const computeBarChartStackedLabels = (
minimumYPosition: number;
maximumBottomYPosition: number;
maximumXPosition: number;
bars: ComputedBarDatum<BarChartDataItem>[];
bars: ComputedBarDatum<BarDatum>[];
}
>();
@@ -1,4 +1,3 @@
import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem';
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';
@@ -7,6 +6,7 @@ import {
formatGraphValue,
type GraphValueFormatOptions,
} from '@/page-layout/widgets/graph/utils/graphFormatters';
import { type BarDatum } from '@nivo/bar';
const LEFT_AXIS_LEGEND_OFFSET_PADDING = 5;
const TICK_PADDING = 5;
@@ -22,7 +22,7 @@ const COMMON_AXIS_CONFIG = {
type GetBarChartAxisConfigsProps = {
width: number;
height: number;
data: BarChartDataItem[];
data: BarDatum[];
layout: BarChartLayout;
indexBy: string;
xAxisLabel?: string;
@@ -1,10 +1,10 @@
import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem';
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';
import { computeBarChartCategoryTickValues } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/computeBarChartCategoryTickValues';
import { computeBarChartValueTickCount } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/computeBarChartValueTickCount';
import { getBarChartMargins } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartMargins';
import { type BarDatum } from '@nivo/bar';
const MAX_LEFT_AXIS_LABEL_LENGTH = 10;
@@ -27,7 +27,7 @@ export const getBarChartTickConfig = ({
}: {
width: number;
height: number;
data: BarChartDataItem[];
data: BarDatum[];
indexBy: string;
xAxisLabel?: string;
yAxisLabel?: string;
@@ -1,15 +1,14 @@
import { type GraphWidgetTooltipItem } from '@/page-layout/widgets/graph/components/GraphWidgetTooltip';
import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem';
import { type BarChartEnrichedKey } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartEnrichedKey';
import {
formatGraphValue,
type GraphValueFormatOptions,
} from '@/page-layout/widgets/graph/utils/graphFormatters';
import { type ComputedDatum } from '@nivo/bar';
import { type BarDatum, type ComputedDatum } from '@nivo/bar';
import { isDefined } from 'twenty-shared/utils';
type GetBarChartTooltipDataParameters = {
datum: ComputedDatum<BarChartDataItem>;
datum: ComputedDatum<BarDatum>;
enrichedKeys: BarChartEnrichedKey[];
formatOptions: GraphValueFormatOptions;
enableGroupTooltip?: boolean;
@@ -1,9 +1,9 @@
import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem';
import { type BarDatum } from '@nivo/bar';
import { isDefined } from 'twenty-shared/utils';
import { GraphOrderBy } from '~/generated/graphql';
type SortBarChartDataBySecondaryDimensionSumParams = {
data: BarChartDataItem[];
data: BarDatum[];
keys: string[];
orderBy: GraphOrderBy;
};
@@ -12,7 +12,7 @@ export const sortBarChartDataBySecondaryDimensionSum = ({
data,
keys,
orderBy,
}: SortBarChartDataBySecondaryDimensionSumParams): BarChartDataItem[] => {
}: SortBarChartDataBySecondaryDimensionSumParams): BarDatum[] => {
if (
orderBy !== GraphOrderBy.VALUE_ASC &&
orderBy !== GraphOrderBy.VALUE_DESC
@@ -20,16 +20,16 @@ export const sortBarChartDataBySecondaryDimensionSum = ({
return data;
}
const dataWithSecondaryDimensionSums = data.map((barChartDataItem) => {
const dataWithSecondaryDimensionSums = data.map((datum) => {
const secondaryDimensionSum = keys.reduce((sumAccumulator, segmentKey) => {
const segmentValue = barChartDataItem[segmentKey];
const segmentValue = datum[segmentKey];
if (isDefined(segmentValue) && typeof segmentValue === 'number') {
return sumAccumulator + segmentValue;
}
return sumAccumulator;
}, 0);
return { barChartDataItem, secondaryDimensionSum };
return { datum, secondaryDimensionSum };
});
dataWithSecondaryDimensionSums.sort((a, b) => {
@@ -40,7 +40,5 @@ export const sortBarChartDataBySecondaryDimensionSum = ({
}
});
return dataWithSecondaryDimensionSums.map(
({ barChartDataItem }) => barChartDataItem,
);
return dataWithSecondaryDimensionSums.map(({ datum }) => datum);
};
@@ -4,7 +4,6 @@ import { getAggregateOperationLabel } from '@/object-record/record-board/record-
import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
import { getGroupByQueryResultGqlFieldName } from '@/page-layout/utils/getGroupByQueryResultGqlFieldName';
import { GRAPH_DEFAULT_DATE_GRANULARITY } from '@/page-layout/widgets/graph/constants/GraphDefaultDateGranularity.constant';
import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem';
import { BarChartLayout } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartLayout';
import { type BarChartSeries } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartSeries';
import { fillDateGapsInBarChartData } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/fillDateGapsInBarChartData';
@@ -14,6 +13,7 @@ import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupBy
import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue';
import { filterGroupByResults } from '@/page-layout/widgets/graph/utils/filterGroupByResults';
import { getFieldKey } from '@/page-layout/widgets/graph/utils/getFieldKey';
import { type BarDatum } from '@nivo/bar';
import { isDefined } from 'twenty-shared/utils';
import { GraphType } from '~/generated-metadata/graphql';
import {
@@ -30,7 +30,7 @@ type TransformGroupByDataToBarChartDataParams = {
};
type TransformGroupByDataToBarChartDataResult = {
data: BarChartDataItem[];
data: BarDatum[];
indexBy: string;
keys: string[];
series: BarChartSeries[];
@@ -3,16 +3,16 @@ import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataI
import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
import { GRAPH_DEFAULT_COLOR } from '@/page-layout/widgets/graph/constants/GraphDefaultColor.constant';
import { BAR_CHART_MAXIMUM_NUMBER_OF_BARS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartMaximumNumberOfBars.constant';
import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem';
import { type BarChartSeries } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartSeries';
import { type GraphColor } from '@/page-layout/widgets/graph/types/GraphColor';
import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult';
import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue';
import { buildFormattedToRawLookup } from '@/page-layout/widgets/graph/utils/buildFormattedToRawLookup';
import { formatPrimaryDimensionValues } from '@/page-layout/widgets/graph/utils/formatPrimaryDimensionValues';
import { computeAggregateValueFromGroupByResult } from '@/page-layout/widgets/graph/utils/computeAggregateValueFromGroupByResult';
import { formatDimensionValue } from '@/page-layout/widgets/graph/utils/formatDimensionValue';
import { formatPrimaryDimensionValues } from '@/page-layout/widgets/graph/utils/formatPrimaryDimensionValues';
import { getFieldKey } from '@/page-layout/widgets/graph/utils/getFieldKey';
import { type BarDatum } from '@nivo/bar';
import { isDefined } from 'twenty-shared/utils';
import { type BarChartConfiguration } from '~/generated/graphql';
@@ -27,7 +27,7 @@ type TransformOneDimensionalGroupByToBarChartDataParams = {
};
type TransformOneDimensionalGroupByToBarChartDataResult = {
data: BarChartDataItem[];
data: BarDatum[];
indexBy: string;
keys: string[];
series: BarChartSeries[];
@@ -67,7 +67,7 @@ export const transformOneDimensionalGroupByToBarChartData = ({
const formattedToRawLookup = buildFormattedToRawLookup(formattedValues);
const data: BarChartDataItem[] = limitedResults.map((result) => {
const data: BarDatum[] = limitedResults.map((result) => {
const dimensionValues = result.groupByDimensionValues;
const xValue = isDefined(dimensionValues?.[0])
@@ -2,18 +2,18 @@ import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataIte
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
import { BAR_CHART_MAXIMUM_NUMBER_OF_BARS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartMaximumNumberOfBars.constant';
import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem';
import { type BarChartSeries } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartSeries';
import { sortBarChartDataBySecondaryDimensionSum } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/sortBarChartDataBySecondaryDimensionSum';
import { type GraphColor } from '@/page-layout/widgets/graph/types/GraphColor';
import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult';
import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue';
import { buildFormattedToRawLookup } from '@/page-layout/widgets/graph/utils/buildFormattedToRawLookup';
import { formatPrimaryDimensionValues } from '@/page-layout/widgets/graph/utils/formatPrimaryDimensionValues';
import { computeAggregateValueFromGroupByResult } from '@/page-layout/widgets/graph/utils/computeAggregateValueFromGroupByResult';
import { formatDimensionValue } from '@/page-layout/widgets/graph/utils/formatDimensionValue';
import { formatPrimaryDimensionValues } from '@/page-layout/widgets/graph/utils/formatPrimaryDimensionValues';
import { getFieldKey } from '@/page-layout/widgets/graph/utils/getFieldKey';
import { getSortedKeys } from '@/page-layout/widgets/graph/utils/getSortedKeys';
import { type BarDatum } from '@nivo/bar';
import { isDefined } from 'twenty-shared/utils';
import {
BarChartGroupMode,
@@ -32,7 +32,7 @@ type TransformTwoDimensionalGroupByToBarChartDataParams = {
};
type TransformTwoDimensionalGroupByToBarChartDataResult = {
data: BarChartDataItem[];
data: BarDatum[];
indexBy: string;
keys: string[];
series: BarChartSeries[];
@@ -55,7 +55,7 @@ export const transformTwoDimensionalGroupByToBarChartData = ({
subFieldName: primaryAxisSubFieldName ?? undefined,
});
const dataMap = new Map<string, BarChartDataItem>();
const dataMap = new Map<string, BarDatum>();
const xValues = new Set<string>();
const yValues = new Set<string>();
const formattedValues = formatPrimaryDimensionValues({
@@ -25,6 +25,7 @@ type GraphWidgetGaugeChartProps = {
showValue?: boolean;
showLegend?: boolean;
id: string;
onGaugeClick?: (data: GaugeChartData) => void;
} & GraphValueFormatOptions;
const StyledContainer = styled.div`
@@ -67,6 +68,7 @@ export const GraphWidgetGaugeChart = ({
prefix,
suffix,
customFormatter,
onGaugeClick,
}: GraphWidgetGaugeChartProps) => {
const theme = useTheme();
const instanceId = useId();
@@ -81,7 +83,7 @@ export const GraphWidgetGaugeChart = ({
};
const { isHovered, setIsHovered, handleClick, hasClickableItems } =
useGaugeChartHandlers({ data });
useGaugeChartHandlers({ data, onGaugeClick });
const {
colorScheme,
@@ -4,18 +4,20 @@ import { isDefined } from 'twenty-shared/utils';
type UseGaugeChartHandlersProps = {
data: GaugeChartData;
onGaugeClick?: (data: GaugeChartData) => void;
};
export const useGaugeChartHandlers = ({ data }: UseGaugeChartHandlersProps) => {
export const useGaugeChartHandlers = ({
data,
onGaugeClick,
}: UseGaugeChartHandlersProps) => {
const [isHovered, setIsHovered] = useState(false);
const handleClick = () => {
if (isDefined(data.to)) {
window.location.href = data.to;
}
onGaugeClick?.(data);
};
const hasClickableItems = isDefined(data.to);
const hasClickableItems = isDefined(onGaugeClick);
return {
isHovered,
@@ -5,6 +5,5 @@ export type GaugeChartData = {
min: number;
max: number;
color?: GraphColor;
to?: string;
label?: string;
};
@@ -1,5 +1,4 @@
export type LineChartDataPoint = {
x: number | string | Date;
y: number | null;
to?: string;
};
@@ -1,16 +0,0 @@
import { type LineChartSeries } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartSeries';
import { type LineSeries, type Point } from '@nivo/line';
import { isDefined } from 'twenty-shared/utils';
export const handleLineChartPointClick = (
point: Point<LineSeries>,
dataMap: Record<string, LineChartSeries>,
) => {
const series = dataMap[point.seriesId];
if (isDefined(series)) {
const dataPoint = series.data[point.indexInSeries];
if (isDefined(dataPoint?.to)) {
window.location.href = dataPoint.to;
}
}
};
@@ -1,10 +1,10 @@
import { GraphWidgetChartContainer } from '@/page-layout/widgets/graph/components/GraphWidgetChartContainer';
import { GraphWidgetLegend } from '@/page-layout/widgets/graph/components/GraphWidgetLegend';
import { GraphWidgetTooltip } from '@/page-layout/widgets/graph/components/GraphWidgetTooltip';
import { PieChartCenterMetric } from '@/page-layout/widgets/graph/graphWidgetPieChart/components/PieChartCenterMetricLayer';
import { PIE_CHART_HOVER_BRIGHTNESS } from '@/page-layout/widgets/graph/graphWidgetPieChart/constants/PieChartHoverBrightness';
import { PIE_CHART_MARGINS } from '@/page-layout/widgets/graph/graphWidgetPieChart/constants/PieChartMargins';
import { usePieChartData } from '@/page-layout/widgets/graph/graphWidgetPieChart/hooks/usePieChartData';
import { usePieChartHandlers } from '@/page-layout/widgets/graph/graphWidgetPieChart/hooks/usePieChartHandlers';
import { usePieChartTooltip } from '@/page-layout/widgets/graph/graphWidgetPieChart/hooks/usePieChartTooltip';
import { type PieChartDataItem } from '@/page-layout/widgets/graph/graphWidgetPieChart/types/PieChartDataItem';
import { createGraphColorRegistry } from '@/page-layout/widgets/graph/utils/createGraphColorRegistry';
@@ -25,11 +25,10 @@ type GraphWidgetPieChartProps = {
id: string;
onSliceClick?: (datum: PieChartDataItem) => void;
showDataLabels?: boolean;
showCenterMetric?: boolean;
} & GraphValueFormatOptions;
const emptyStateData: PieChartDataItem[] = [
{ id: 'empty', label: '', value: 1 },
];
const emptyStateData: PieChartDataItem[] = [{ id: 'empty', value: 1 }];
const StyledContainer = styled.div`
align-items: center;
@@ -41,8 +40,10 @@ const StyledContainer = styled.div`
`;
const StyledPieChartWrapper = styled.div<{ preventHover: boolean }>`
width: 100%;
container-type: size;
height: 100%;
position: relative;
width: 100%;
svg g path {
transition: filter 0.15s ease-in-out;
@@ -65,6 +66,7 @@ export const GraphWidgetPieChart = ({
customFormatter,
onSliceClick,
showDataLabels = false,
showCenterMetric = false,
}: GraphWidgetPieChartProps) => {
const theme = useTheme();
const colorRegistry = createGraphColorRegistry(theme);
@@ -77,17 +79,9 @@ export const GraphWidgetPieChart = ({
customFormatter,
};
const {
hoveredSliceId,
setHoveredSliceId,
handleSliceClick,
hasClickableItems,
} = usePieChartHandlers({ data, onSliceClick });
const { enrichedData } = usePieChartData({
data,
colorRegistry,
hoveredSliceId,
});
const { createTooltipData } = usePieChartTooltip({
@@ -96,6 +90,12 @@ export const GraphWidgetPieChart = ({
displayType,
});
const handleSliceClick = (datum: ComputedDatum<PieChartDataItem>) => {
if (isDefined(onSliceClick)) {
onSliceClick(datum.data);
}
};
const renderTooltip = ({ datum }: PieTooltipProps<PieChartDataItem>) => {
const tooltipData = createTooltipData(datum);
if (!isDefined(tooltipData)) return null;
@@ -121,7 +121,7 @@ export const GraphWidgetPieChart = ({
return (
<StyledContainer id={id}>
<GraphWidgetChartContainer
$isClickable={!hasNoData && hasClickableItems}
$isClickable={!hasNoData && isDefined(onSliceClick)}
$cursorSelector="svg g path"
>
<StyledPieChartWrapper preventHover={hasNoData}>
@@ -135,11 +135,7 @@ export const GraphWidgetPieChart = ({
enableArcLinkLabels={showDataLabels && !hasNoData}
enableArcLabels={false}
tooltip={hasNoData ? () => null : renderTooltip}
onClick={hasNoData ? undefined : handleSliceClick}
onMouseEnter={
hasNoData ? undefined : (datum) => setHoveredSliceId(datum.id)
}
onMouseLeave={hasNoData ? undefined : () => setHoveredSliceId(null)}
onClick={hasNoData ? undefined : (datum) => handleSliceClick(datum)}
layers={['arcs', 'arcLinkLabels']}
arcLinkLabel={(datum: ComputedDatum<PieChartDataItem>) => {
const tooltipData = createTooltipData(datum);
@@ -161,13 +157,18 @@ export const GraphWidgetPieChart = ({
},
}}
/>
<PieChartCenterMetric
data={data}
formatOptions={formatOptions}
show={showCenterMetric && !hasNoData}
/>
</StyledPieChartWrapper>
</GraphWidgetChartContainer>
<GraphWidgetLegend
show={showLegend && !hasNoData}
items={enrichedData.map((item) => ({
id: item.id,
label: item.label || item.id,
label: item.id,
color: item.colorScheme.solid,
}))}
/>
@@ -34,6 +34,7 @@ export const GraphWidgetPieChartRenderer = ({
objectMetadataItem,
showLegend,
showDataLabels,
showCenterMetric,
formattedToRawLookup,
} = useGraphPieChartWidgetData({
objectMetadataItemId: widget.objectMetadataId,
@@ -89,6 +90,7 @@ export const GraphWidgetPieChartRenderer = ({
displayType="shortNumber"
onSliceClick={handleSliceClick}
showDataLabels={showDataLabels}
showCenterMetric={showCenterMetric}
/>
</Suspense>
);
@@ -0,0 +1,56 @@
import { type PieChartDataItem } from '@/page-layout/widgets/graph/graphWidgetPieChart/types/PieChartDataItem';
import {
formatGraphValue,
type GraphValueFormatOptions,
} from '@/page-layout/widgets/graph/utils/graphFormatters';
import styled from '@emotion/styled';
import { Trans } from '@lingui/react/macro';
type PieChartCenterMetricProps = {
data: PieChartDataItem[];
formatOptions: GraphValueFormatOptions;
show: boolean;
};
const StyledCenterMetricContainer = styled.div<{ show: boolean }>`
align-items: center;
display: flex;
flex-direction: column;
justify-content: center;
left: 50%;
opacity: ${({ show }) => (show ? 1 : 0)};
pointer-events: none;
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
transition: opacity ${({ theme }) => theme.animation.duration.fast}s
ease-in-out;
`;
const StyledValue = styled.span`
color: ${({ theme }) => theme.font.color.primary};
font-size: clamp(12px, 10cqmin, 48px);
font-weight: ${({ theme }) => theme.font.weight.semiBold};
`;
const StyledLabel = styled.span`
color: ${({ theme }) => theme.font.color.tertiary};
font-size: clamp(10px, 5cqmin, 24px);
`;
export const PieChartCenterMetric = ({
data,
formatOptions,
show,
}: PieChartCenterMetricProps) => {
const total = data.reduce((sum, datum) => sum + datum.value, 0);
return (
<StyledCenterMetricContainer show={show}>
<StyledValue>{formatGraphValue(total, formatOptions)}</StyledValue>
<StyledLabel>
<Trans>Total</Trans>
</StyledLabel>
</StyledCenterMetricContainer>
);
};
@@ -1,68 +0,0 @@
import { type PieChartEnrichedData } from '@/page-layout/widgets/graph/graphWidgetPieChart/types/PieChartEnrichedData';
import { calculatePieChartEndLineCoordinates } from '@/page-layout/widgets/graph/graphWidgetPieChart/utils/calculatePieChartEndLineCoordinates';
import { useTheme } from '@emotion/react';
import { type ComputedDatum } from '@nivo/pie';
import { isDefined } from 'twenty-shared/utils';
type PieChartEndLinesProps = {
dataWithArc: readonly ComputedDatum<{
id: string;
value: number;
label?: string;
}>[];
centerX: number;
centerY: number;
innerRadius: number;
radius: number;
enrichedDataMap: Map<string, PieChartEnrichedData>;
};
export const PieChartEndLines = ({
dataWithArc,
centerX,
centerY,
innerRadius,
radius,
enrichedDataMap,
}: PieChartEndLinesProps) => {
const theme = useTheme();
if (
!isDefined(dataWithArc) ||
!Array.isArray(dataWithArc) ||
dataWithArc.length < 2
) {
return null;
}
return (
<g>
{dataWithArc.map((datum) => {
const enrichedItem = enrichedDataMap.get(datum.id);
const lineColor = enrichedItem
? enrichedItem.colorScheme.solid
: theme.border.color.strong;
const { x1, y1, x2, y2 } = calculatePieChartEndLineCoordinates(
datum.arc.endAngle,
centerX,
centerY,
innerRadius,
radius,
);
return (
<line
key={`${datum.id}-separator`}
x1={x1}
y1={y1}
x2={x2}
y2={y2}
stroke={lineColor}
strokeWidth={1}
/>
);
})}
</g>
);
};
@@ -0,0 +1,5 @@
export const PIE_CHART_CENTER_METRIC_MIN_SIZES = {
valueFontSize: 12,
labelFontSize: 10,
labelOffset: 12,
} as const;
@@ -0,0 +1,5 @@
export const PIE_CHART_CENTER_METRIC_RATIOS = {
valueFontSize: 0.25,
labelFontSize: 0.12,
labelOffset: 0.15,
} as const;
@@ -57,9 +57,9 @@ describe('usePieChartData', () => {
};
const mockData: PieChartDataItem[] = [
{ id: 'item1', value: 30, label: 'Item 1' },
{ id: 'item2', value: 50, label: 'Item 2' },
{ id: 'item3', value: 20, label: 'Item 3' },
{ id: 'item1', value: 30 },
{ id: 'item2', value: 50 },
{ id: 'item3', value: 20 },
];
it('should enrich data with color schemes and percentages', () => {
@@ -67,7 +67,6 @@ describe('usePieChartData', () => {
usePieChartData({
data: mockData,
colorRegistry: mockColorRegistry,
hoveredSliceId: null,
}),
);
@@ -75,54 +74,18 @@ describe('usePieChartData', () => {
expect(result.current.enrichedData[0]).toMatchObject({
id: 'item1',
value: 30,
label: 'Item 1',
percentage: 30,
colorScheme: mockColorRegistry.red,
isHovered: false,
});
expect(result.current.enrichedData[1].percentage).toBe(50);
expect(result.current.enrichedData[2].percentage).toBe(20);
});
it('should calculate middle angles for each slice', () => {
const { result } = renderHook(() =>
usePieChartData({
data: mockData,
colorRegistry: mockColorRegistry,
hoveredSliceId: null,
}),
);
expect(result.current.enrichedData[0].middleAngle).toBe(54);
expect(result.current.enrichedData[1].middleAngle).toBe(198);
expect(result.current.enrichedData[2].middleAngle).toBe(324);
});
it('should handle hover state', () => {
const { result, rerender } = renderHook(
({ hoveredSliceId }: { hoveredSliceId: DatumId | null }) =>
usePieChartData({
data: mockData,
colorRegistry: mockColorRegistry,
hoveredSliceId,
}),
{ initialProps: { hoveredSliceId: null as DatumId | null } },
);
expect(result.current.enrichedData[1].isHovered).toBe(false);
rerender({ hoveredSliceId: 'item2' as DatumId });
expect(result.current.enrichedData[1].isHovered).toBe(true);
expect(result.current.enrichedData[0].isHovered).toBe(false);
expect(result.current.enrichedData[2].isHovered).toBe(false);
});
it('should handle empty data', () => {
const { result } = renderHook(() =>
usePieChartData({
data: [],
colorRegistry: mockColorRegistry,
hoveredSliceId: null,
}),
);
@@ -130,47 +93,36 @@ describe('usePieChartData', () => {
});
it('should handle single data item', () => {
const singleData: PieChartDataItem[] = [
{ id: 'single', value: 100, label: 'Single Item' },
];
const singleData: PieChartDataItem[] = [{ id: 'single', value: 100 }];
const { result } = renderHook(() =>
usePieChartData({
data: singleData,
colorRegistry: mockColorRegistry,
hoveredSliceId: null,
}),
);
expect(result.current.enrichedData[0].percentage).toBe(100);
expect(result.current.enrichedData[0].middleAngle).toBe(180);
});
it('should handle custom colors in data items', () => {
const dataWithColors: PieChartDataItem[] = [
{ id: 'item1', value: 50, label: 'Item 1', color: 'blue' },
{ id: 'item2', value: 50, label: 'Item 2' },
];
it('should assign colors based on index', () => {
const { result } = renderHook(() =>
usePieChartData({
data: dataWithColors,
data: mockData,
colorRegistry: mockColorRegistry,
hoveredSliceId: null,
}),
);
expect(result.current.enrichedData[0].colorScheme.name).toBe('blue');
expect(result.current.enrichedData[0].colorScheme.name).toBe('red');
expect(result.current.enrichedData[1].colorScheme.name).toBe('blue');
});
it('should memoize calculations', () => {
const { result, rerender } = renderHook(
({ hoveredSliceId }) =>
() =>
usePieChartData({
data: mockData,
colorRegistry: mockColorRegistry,
hoveredSliceId,
}),
{ initialProps: { hoveredSliceId: null as DatumId | null } },
);
@@ -23,6 +23,7 @@ type UseGraphPieChartWidgetDataResult = {
objectMetadataItem: ObjectMetadataItem;
formattedToRawLookup: Map<string, RawDimensionValue>;
showDataLabels: boolean;
showCenterMetric: boolean;
};
export const useGraphPieChartWidgetData = ({
@@ -60,6 +61,7 @@ export const useGraphPieChartWidgetData = ({
...transformedData,
objectMetadataItem,
showDataLabels: configuration.displayDataLabel ?? false,
showCenterMetric: configuration.showCenterMetric ?? false,
loading,
error,
};
@@ -1,50 +1,38 @@
import { type PieChartDataItem } from '@/page-layout/widgets/graph/graphWidgetPieChart/types/PieChartDataItem';
import { type PieChartEnrichedData } from '@/page-layout/widgets/graph/graphWidgetPieChart/types/PieChartEnrichedData';
import { calculatePieChartAngles } from '@/page-layout/widgets/graph/graphWidgetPieChart/utils/calculatePieChartAngles';
import { calculatePieChartPercentage } from '@/page-layout/widgets/graph/graphWidgetPieChart/utils/calculatePieChartPercentage';
import { type GraphColorRegistry } from '@/page-layout/widgets/graph/types/GraphColorRegistry';
import { getColorScheme } from '@/page-layout/widgets/graph/utils/getColorScheme';
import { type DatumId } from '@nivo/pie';
import { useMemo } from 'react';
type UsePieChartDataProps = {
data: PieChartDataItem[];
colorRegistry: GraphColorRegistry;
hoveredSliceId: DatumId | null;
};
export const usePieChartData = ({
data,
colorRegistry,
hoveredSliceId,
}: UsePieChartDataProps) => {
const enrichedData = useMemo((): PieChartEnrichedData[] => {
const totalValue = data.reduce((sum, item) => sum + item.value, 0);
let cumulativeAngle = 0;
return data.map((item, index) => {
const colorScheme = getColorScheme({
registry: colorRegistry,
colorName: item.color,
fallbackIndex: index,
totalGroups: data.length,
});
const isHovered = hoveredSliceId === item.id;
const percentage = calculatePieChartPercentage(item.value, totalValue);
const angles = calculatePieChartAngles(percentage, cumulativeAngle);
cumulativeAngle = angles.newCumulativeAngle;
return {
...item,
colorScheme,
isHovered,
percentage,
middleAngle: angles.middleAngle,
};
});
}, [data, colorRegistry, hoveredSliceId]);
}, [data, colorRegistry]);
const enrichedDataMap = useMemo(
() => new Map(enrichedData.map((item) => [item.id, item])),
@@ -1,39 +0,0 @@
import { type PieChartDataItem } from '@/page-layout/widgets/graph/graphWidgetPieChart/types/PieChartDataItem';
import { type ComputedDatum, type DatumId } from '@nivo/pie';
import { useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
type UsePieChartHandlersProps = {
data: PieChartDataItem[];
onSliceClick?: (datum: PieChartDataItem) => void;
};
export const usePieChartHandlers = ({
data,
onSliceClick,
}: UsePieChartHandlersProps) => {
const [hoveredSliceId, setHoveredSliceId] = useState<DatumId | null>(null);
const handleSliceClick = (
datum: ComputedDatum<{ id: string; value: number; label?: string }>,
) => {
const clickedItem = data.find((d) => d.id === datum.id);
if (isDefined(clickedItem)) {
if (isDefined(onSliceClick)) {
onSliceClick(clickedItem);
} else if (isDefined(clickedItem.to)) {
window.location.href = clickedItem.to;
}
}
};
const hasClickableItems =
isDefined(onSliceClick) || data.some((item) => isDefined(item.to));
return {
hoveredSliceId,
setHoveredSliceId,
handleSliceClick,
hasClickableItems,
};
};
@@ -33,7 +33,7 @@ export const usePieChartTooltip = ({
return {
tooltipItem: {
key: item.id,
label: item.label || item.id,
label: item.id,
formattedValue,
value: item.value,
dotColor: item.colorScheme.solid,
@@ -1,9 +1,4 @@
import { type GraphColor } from '@/page-layout/widgets/graph/types/GraphColor';
export type PieChartDataItem = {
id: string;
value: number;
label?: string;
color?: GraphColor;
to?: string;
};
@@ -3,7 +3,5 @@ import { type GraphColorScheme } from '@/page-layout/widgets/graph/types/GraphCo
export type PieChartEnrichedData = PieChartDataItem & {
colorScheme: GraphColorScheme;
isHovered: boolean;
percentage: number;
middleAngle: number;
};
@@ -1,88 +0,0 @@
import { calculatePieChartAngles } from '../calculatePieChartAngles';
describe('calculatePieChartAngles', () => {
it('should calculate angles for a quarter slice (25%)', () => {
const result = calculatePieChartAngles(25, 0);
expect(result).toEqual({
sliceAngle: 90,
middleAngle: 45,
newCumulativeAngle: 90,
});
});
it('should calculate angles for a half slice (50%)', () => {
const result = calculatePieChartAngles(50, 0);
expect(result).toEqual({
sliceAngle: 180,
middleAngle: 90,
newCumulativeAngle: 180,
});
});
it('should calculate angles for a full circle (100%)', () => {
const result = calculatePieChartAngles(100, 0);
expect(result).toEqual({
sliceAngle: 360,
middleAngle: 180,
newCumulativeAngle: 360,
});
});
it('should handle cumulative angles correctly', () => {
const result = calculatePieChartAngles(25, 90);
expect(result).toEqual({
sliceAngle: 90,
middleAngle: 135,
newCumulativeAngle: 180,
});
});
it('should handle very small percentages', () => {
const result = calculatePieChartAngles(1, 0);
expect(result).toEqual({
sliceAngle: 3.6,
middleAngle: 1.8,
newCumulativeAngle: 3.6,
});
});
it('should handle decimal percentages', () => {
const result = calculatePieChartAngles(33.33, 0);
expect(result.sliceAngle).toBeCloseTo(119.988, 2);
expect(result.middleAngle).toBeCloseTo(59.994, 2);
expect(result.newCumulativeAngle).toBeCloseTo(119.988, 2);
});
it('should handle zero percentage', () => {
const result = calculatePieChartAngles(0, 45);
expect(result).toEqual({
sliceAngle: 0,
middleAngle: 45,
newCumulativeAngle: 45,
});
});
it('should calculate sequential slices correctly', () => {
let cumulative = 0;
const slice1 = calculatePieChartAngles(30, cumulative);
expect(slice1.sliceAngle).toBe(108);
expect(slice1.middleAngle).toBe(54);
cumulative = slice1.newCumulativeAngle;
const slice2 = calculatePieChartAngles(50, cumulative);
expect(slice2.sliceAngle).toBe(180);
expect(slice2.middleAngle).toBe(198);
cumulative = slice2.newCumulativeAngle;
const slice3 = calculatePieChartAngles(20, cumulative);
expect(slice3.sliceAngle).toBe(72);
expect(slice3.middleAngle).toBe(324);
expect(slice3.newCumulativeAngle).toBe(360);
});
it('should handle negative percentages', () => {
const result = calculatePieChartAngles(-10, 0);
expect(result).toEqual({
sliceAngle: -36,
middleAngle: -18,
newCumulativeAngle: -36,
});
});
it('should handle percentages over 100', () => {
const result = calculatePieChartAngles(150, 0);
expect(result).toEqual({
sliceAngle: 540,
middleAngle: 270,
newCumulativeAngle: 540,
});
});
});
@@ -1,133 +0,0 @@
import { calculatePieChartEndLineCoordinates } from '../calculatePieChartEndLineCoordinates';
describe('calculatePieChartEndLineCoordinates', () => {
it('should calculate coordinates for angle 0 (top)', () => {
const result = calculatePieChartEndLineCoordinates(0, 100, 100, 50, 80);
expect(result).toEqual({
x1: 100,
y1: 50,
x2: 100,
y2: 20,
});
});
it('should calculate coordinates for angle π/2 (right)', () => {
const result = calculatePieChartEndLineCoordinates(
Math.PI / 2,
100,
100,
50,
80,
);
expect(result).toEqual({
x1: 150,
y1: 100,
x2: 180,
y2: 100,
});
});
it('should calculate coordinates for angle π (bottom)', () => {
const result = calculatePieChartEndLineCoordinates(
Math.PI,
100,
100,
50,
80,
);
expect(result).toEqual({
x1: 100,
y1: 150,
x2: 100,
y2: 180,
});
});
it('should calculate coordinates for angle 3π/2 (left)', () => {
const result = calculatePieChartEndLineCoordinates(
(3 * Math.PI) / 2,
100,
100,
50,
80,
);
expect(result.x1).toBeCloseTo(50, 5);
expect(result.y1).toBeCloseTo(100, 5);
expect(result.x2).toBeCloseTo(20, 5);
expect(result.y2).toBeCloseTo(100, 5);
});
it('should calculate coordinates for 45-degree angle', () => {
const angle = Math.PI / 4;
const result = calculatePieChartEndLineCoordinates(angle, 100, 100, 50, 80);
const expectedCos = Math.sqrt(2) / 2;
const expectedSin = -Math.sqrt(2) / 2;
expect(result.x1).toBeCloseTo(100 + expectedCos * 50, 5);
expect(result.y1).toBeCloseTo(100 + expectedSin * 50, 5);
expect(result.x2).toBeCloseTo(100 + expectedCos * 80, 5);
expect(result.y2).toBeCloseTo(100 + expectedSin * 80, 5);
});
it('should handle different center positions', () => {
const result = calculatePieChartEndLineCoordinates(
Math.PI / 2,
200,
150,
30,
60,
);
expect(result).toEqual({
x1: 230,
y1: 150,
x2: 260,
y2: 150,
});
});
it('should handle zero radius', () => {
const result = calculatePieChartEndLineCoordinates(
Math.PI / 4,
100,
100,
0,
0,
);
expect(result).toEqual({
x1: 100,
y1: 100,
x2: 100,
y2: 100,
});
});
it('should handle negative angles', () => {
const result = calculatePieChartEndLineCoordinates(
-Math.PI / 2,
100,
100,
50,
80,
);
expect(result.x1).toBeCloseTo(50, 5);
expect(result.y1).toBeCloseTo(100, 5);
expect(result.x2).toBeCloseTo(20, 5);
expect(result.y2).toBeCloseTo(100, 5);
});
it('should create a line from inner to outer radius', () => {
const angle = Math.PI / 6;
const centerX = 100;
const centerY = 100;
const innerRadius = 40;
const outerRadius = 70;
const result = calculatePieChartEndLineCoordinates(
angle,
centerX,
centerY,
innerRadius,
outerRadius,
);
const dist1 = Math.sqrt(
(result.x1 - centerX) ** 2 + (result.y1 - centerY) ** 2,
);
const dist2 = Math.sqrt(
(result.x2 - centerX) ** 2 + (result.y2 - centerY) ** 2,
);
expect(dist1).toBeCloseTo(innerRadius, 5);
expect(dist2).toBeCloseTo(outerRadius, 5);
const angle1 = Math.atan2(result.y1 - centerY, result.x1 - centerX);
const angle2 = Math.atan2(result.y2 - centerY, result.x2 - centerX);
expect(angle1).toBeCloseTo(angle2, 5);
});
});
@@ -1,57 +0,0 @@
import { calculatePieChartPercentage } from '../calculatePieChartPercentage';
describe('calculatePieChartPercentage', () => {
it('should calculate percentage for normal values', () => {
expect(calculatePieChartPercentage(25, 100)).toBe(25);
expect(calculatePieChartPercentage(50, 100)).toBe(50);
expect(calculatePieChartPercentage(75, 100)).toBe(75);
});
it('should calculate percentage with decimal values', () => {
expect(calculatePieChartPercentage(33, 100)).toBe(33);
expect(calculatePieChartPercentage(1, 3)).toBeCloseTo(33.333, 2);
expect(calculatePieChartPercentage(2, 3)).toBeCloseTo(66.667, 2);
});
it('should handle zero value', () => {
expect(calculatePieChartPercentage(0, 100)).toBe(0);
expect(calculatePieChartPercentage(0, 1)).toBe(0);
});
it('should handle zero total (divide by zero)', () => {
expect(calculatePieChartPercentage(10, 0)).toBe(0);
expect(calculatePieChartPercentage(0, 0)).toBe(0);
expect(calculatePieChartPercentage(-5, 0)).toBe(0);
});
it('should handle negative total', () => {
expect(calculatePieChartPercentage(10, -100)).toBe(0);
expect(calculatePieChartPercentage(-10, -100)).toBe(0);
});
it('should handle value greater than total', () => {
expect(calculatePieChartPercentage(150, 100)).toBe(150);
expect(calculatePieChartPercentage(200, 50)).toBe(400);
});
it('should handle very small values', () => {
expect(calculatePieChartPercentage(0.01, 100)).toBe(0.01);
expect(calculatePieChartPercentage(0.001, 1)).toBe(0.1);
});
it('should handle very large values', () => {
expect(calculatePieChartPercentage(1000000, 10000000)).toBe(10);
expect(calculatePieChartPercentage(1e10, 1e12)).toBe(1);
});
it('should maintain precision for financial calculations', () => {
const value1 = 33.33;
const value2 = 33.33;
const value3 = 33.34;
const total = value1 + value2 + value3;
expect(calculatePieChartPercentage(value1, total)).toBeCloseTo(33.33, 2);
expect(calculatePieChartPercentage(value2, total)).toBeCloseTo(33.33, 2);
expect(calculatePieChartPercentage(value3, total)).toBeCloseTo(33.34, 2);
});
it('should handle edge case with Infinity', () => {
expect(calculatePieChartPercentage(Infinity, 100)).toBe(Infinity);
expect(calculatePieChartPercentage(100, Infinity)).toBe(0);
expect(calculatePieChartPercentage(Infinity, Infinity)).toBeNaN();
});
it('should handle NaN inputs', () => {
expect(calculatePieChartPercentage(NaN, 100)).toBeNaN();
expect(calculatePieChartPercentage(100, NaN)).toBeNaN();
expect(calculatePieChartPercentage(NaN, NaN)).toBeNaN();
});
});
@@ -1,14 +0,0 @@
export const calculatePieChartAngles = (
percentage: number,
cumulativeAngle: number,
) => {
const sliceAngle = (percentage / 100) * 360;
const middleAngle = cumulativeAngle + sliceAngle / 2;
const newCumulativeAngle = cumulativeAngle + sliceAngle;
return {
sliceAngle,
middleAngle,
newCumulativeAngle,
};
};
@@ -1,15 +0,0 @@
export const calculatePieChartEndLineCoordinates = (
angle: number,
centerX: number,
centerY: number,
innerRadius: number,
outerRadius: number,
) => {
const adjustedAngle = angle - Math.PI / 2;
const x1 = centerX + Math.cos(adjustedAngle) * innerRadius;
const y1 = centerY + Math.sin(adjustedAngle) * innerRadius;
const x2 = centerX + Math.cos(adjustedAngle) * outerRadius;
const y2 = centerY + Math.sin(adjustedAngle) * outerRadius;
return { x1, y1, x2, y2 };
};
@@ -4,7 +4,6 @@ import { type ExtendedAggregateOperations } from '@/object-record/record-table/t
import { getGroupByQueryResultGqlFieldName } from '@/page-layout/utils/getGroupByQueryResultGqlFieldName';
import { PIE_CHART_MAXIMUM_NUMBER_OF_SLICES } from '@/page-layout/widgets/graph/graphWidgetPieChart/constants/PieChartMaximumNumberOfSlices.constant';
import { type PieChartDataItem } from '@/page-layout/widgets/graph/graphWidgetPieChart/types/PieChartDataItem';
import { type GraphColor } from '@/page-layout/widgets/graph/types/GraphColor';
import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult';
import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue';
import { buildFormattedToRawLookup } from '@/page-layout/widgets/graph/utils/buildFormattedToRawLookup';
@@ -82,7 +81,7 @@ export const transformGroupByDataToPieChartData = ({
const formattedToRawLookup = buildFormattedToRawLookup(formattedValues);
const data: PieChartDataItem[] = limitedResults.map((result, index) => {
const label = formattedValues[index]?.formattedPrimaryDimensionValue ?? '';
const id = formattedValues[index]?.formattedPrimaryDimensionValue ?? '';
const value = computeAggregateValueFromGroupByResult({
rawResult: result,
@@ -94,10 +93,8 @@ export const transformGroupByDataToPieChartData = ({
});
return {
id: label,
value: value,
label: label,
color: (configuration.color as GraphColor | undefined) ?? undefined,
id,
value,
};
});
+9 -1
View File
@@ -23,7 +23,15 @@
"paths": {
"@/*": ["./src/modules/*"],
"~/*": ["./src/*"]
}
},
"plugins": [
{
"name": "@styled/typescript-styled-plugin",
"lint": {
"validProperties": ["container-type"],
}
}
]
},
"files": [],
"references": [
@@ -71,6 +71,11 @@ export class PieChartConfigurationDTO {
@IsOptional()
displayDataLabel?: boolean;
@Field(() => Boolean, { nullable: true, defaultValue: false })
@IsBoolean()
@IsOptional()
showCenterMetric?: boolean;
@Field(() => Boolean, { nullable: true, defaultValue: true })
@IsBoolean()
@IsOptional()
@@ -252,6 +252,7 @@ export {
IconNoteOff,
IconNotes,
IconNumber,
IconNumber123,
IconNumber9,
IconNumbers,
IconPaperclip,
+1
View File
@@ -317,6 +317,7 @@ export {
IconNoteOff,
IconNotes,
IconNumber,
IconNumber123,
IconNumber9,
IconNumbers,
IconPaperclip,