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:
@@ -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
|
||||
|
||||
+7
-1
@@ -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,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
+11
@@ -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,
|
||||
};
|
||||
+1
@@ -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`,
|
||||
|
||||
+4
@@ -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'
|
||||
|
||||
+2
@@ -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',
|
||||
|
||||
+2
@@ -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>
|
||||
|
||||
+1
@@ -75,6 +75,7 @@ export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql`
|
||||
dateGranularity
|
||||
orderBy
|
||||
displayDataLabel
|
||||
showCenterMetric
|
||||
displayLegend
|
||||
color
|
||||
description
|
||||
|
||||
-9
@@ -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',
|
||||
|
||||
+95
-164
@@ -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 },
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
+2
-6
@@ -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;
|
||||
|
||||
+2
-3
@@ -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
-9
@@ -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
-3
@@ -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;
|
||||
|
||||
|
||||
+2
-2
@@ -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 },
|
||||
|
||||
+2
-2
@@ -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[];
|
||||
|
||||
+2
-3
@@ -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',
|
||||
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
import { type BarDatum } from '@nivo/bar';
|
||||
|
||||
export type BarChartDataItem = BarDatum & {
|
||||
to?: string;
|
||||
};
|
||||
+5
-6
@@ -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();
|
||||
|
||||
+5
-5
@@ -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 },
|
||||
|
||||
+5
-5
@@ -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 },
|
||||
|
||||
+14
-15
@@ -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),
|
||||
];
|
||||
|
||||
+2
-3
@@ -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) {
|
||||
|
||||
+3
-3
@@ -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[] = [];
|
||||
|
||||
+3
-3
@@ -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[] = [];
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+2
-3
@@ -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);
|
||||
|
||||
+3
-4
@@ -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>[];
|
||||
}
|
||||
>();
|
||||
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+2
-3
@@ -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;
|
||||
|
||||
+7
-9
@@ -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);
|
||||
};
|
||||
|
||||
+2
-2
@@ -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[];
|
||||
|
||||
+4
-4
@@ -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])
|
||||
|
||||
+4
-4
@@ -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({
|
||||
|
||||
+3
-1
@@ -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,
|
||||
|
||||
+7
-5
@@ -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,
|
||||
|
||||
-1
@@ -5,6 +5,5 @@ export type GaugeChartData = {
|
||||
min: number;
|
||||
max: number;
|
||||
color?: GraphColor;
|
||||
to?: string;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
-1
@@ -1,5 +1,4 @@
|
||||
export type LineChartDataPoint = {
|
||||
x: number | string | Date;
|
||||
y: number | null;
|
||||
to?: string;
|
||||
};
|
||||
|
||||
-16
@@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
+21
-20
@@ -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,
|
||||
}))}
|
||||
/>
|
||||
|
||||
+2
@@ -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>
|
||||
);
|
||||
|
||||
+56
@@ -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>
|
||||
);
|
||||
};
|
||||
-68
@@ -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>
|
||||
);
|
||||
};
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
export const PIE_CHART_CENTER_METRIC_MIN_SIZES = {
|
||||
valueFontSize: 12,
|
||||
labelFontSize: 10,
|
||||
labelOffset: 12,
|
||||
} as const;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
export const PIE_CHART_CENTER_METRIC_RATIOS = {
|
||||
valueFontSize: 0.25,
|
||||
labelFontSize: 0.12,
|
||||
labelOffset: 0.15,
|
||||
} as const;
|
||||
+8
-56
@@ -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 } },
|
||||
);
|
||||
|
||||
+2
@@ -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
-13
@@ -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])),
|
||||
|
||||
-39
@@ -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,
|
||||
};
|
||||
};
|
||||
+1
-1
@@ -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,
|
||||
|
||||
-5
@@ -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;
|
||||
};
|
||||
|
||||
-2
@@ -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;
|
||||
};
|
||||
|
||||
-88
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
-133
@@ -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);
|
||||
});
|
||||
});
|
||||
-57
@@ -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();
|
||||
});
|
||||
});
|
||||
-14
@@ -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,
|
||||
};
|
||||
};
|
||||
-15
@@ -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 };
|
||||
};
|
||||
+3
-6
@@ -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,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -23,7 +23,15 @@
|
||||
"paths": {
|
||||
"@/*": ["./src/modules/*"],
|
||||
"~/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "@styled/typescript-styled-plugin",
|
||||
"lint": {
|
||||
"validProperties": ["container-type"],
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"files": [],
|
||||
"references": [
|
||||
|
||||
+5
@@ -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,
|
||||
|
||||
@@ -317,6 +317,7 @@ export {
|
||||
IconNoteOff,
|
||||
IconNotes,
|
||||
IconNumber,
|
||||
IconNumber123,
|
||||
IconNumber9,
|
||||
IconNumbers,
|
||||
IconPaperclip,
|
||||
|
||||
Reference in New Issue
Block a user