[DASHBOARDS] Pie Chart (#16048)
## Description - Connect Pie Chart to the backend - Update from the old design to the new design - Switch seamlessly from/to other types of chart from the Pie Chart by converting the configuration Note: There are a couple things to implement before the pie chart is ready to go live: - Add a new setting on Bar, Line and Pie chart to hide and display legends (toggle) - Add data labels next to each slice ## Video QA https://github.com/user-attachments/assets/b1561e32-0434-43ad-8855-d617b209ce27
This commit is contained in:
+35
-38
@@ -1,7 +1,7 @@
|
||||
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 { PieChartEndLines } from '@/page-layout/widgets/graph/graphWidgetPieChart/components/PieChartEndLines';
|
||||
import { PIE_CHART_HOVER_BRIGHTNESS } from '@/page-layout/widgets/graph/graphWidgetPieChart/constants/PieChartHoverBrightness';
|
||||
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';
|
||||
@@ -10,17 +10,14 @@ import { createGraphColorRegistry } from '@/page-layout/widgets/graph/utils/crea
|
||||
import { type GraphValueFormatOptions } from '@/page-layout/widgets/graph/utils/graphFormatters';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
ResponsivePie,
|
||||
type PieCustomLayerProps,
|
||||
type PieTooltipProps,
|
||||
} from '@nivo/pie';
|
||||
import { ResponsivePie, type PieTooltipProps } from '@nivo/pie';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type GraphWidgetPieChartProps = {
|
||||
data: PieChartDataItem[];
|
||||
showLegend?: boolean;
|
||||
id: string;
|
||||
onSliceClick?: (datum: PieChartDataItem) => void;
|
||||
} & GraphValueFormatOptions;
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
@@ -32,6 +29,19 @@ const StyledContainer = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledPieChartWrapper = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
svg g path {
|
||||
transition: filter 0.15s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
filter: brightness(${PIE_CHART_HOVER_BRIGHTNESS});
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GraphWidgetPieChart = ({
|
||||
data,
|
||||
showLegend = true,
|
||||
@@ -41,6 +51,7 @@ export const GraphWidgetPieChart = ({
|
||||
prefix,
|
||||
suffix,
|
||||
customFormatter,
|
||||
onSliceClick,
|
||||
}: GraphWidgetPieChartProps) => {
|
||||
const theme = useTheme();
|
||||
const colorRegistry = createGraphColorRegistry(theme);
|
||||
@@ -58,12 +69,11 @@ export const GraphWidgetPieChart = ({
|
||||
setHoveredSliceId,
|
||||
handleSliceClick,
|
||||
hasClickableItems,
|
||||
} = usePieChartHandlers({ data });
|
||||
} = usePieChartHandlers({ data, onSliceClick });
|
||||
|
||||
const { enrichedData, enrichedDataMap, defs, fill } = usePieChartData({
|
||||
const { enrichedData } = usePieChartData({
|
||||
data,
|
||||
colorRegistry,
|
||||
id,
|
||||
hoveredSliceId,
|
||||
});
|
||||
|
||||
@@ -73,19 +83,6 @@ export const GraphWidgetPieChart = ({
|
||||
displayType,
|
||||
});
|
||||
|
||||
const renderSliceEndLines = (
|
||||
layerProps: PieCustomLayerProps<PieChartDataItem>,
|
||||
) => (
|
||||
<PieChartEndLines
|
||||
dataWithArc={layerProps.dataWithArc}
|
||||
centerX={layerProps.centerX}
|
||||
centerY={layerProps.centerY}
|
||||
innerRadius={layerProps.innerRadius}
|
||||
radius={layerProps.radius}
|
||||
enrichedDataMap={enrichedDataMap}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderTooltip = ({ datum }: PieTooltipProps<PieChartDataItem>) => {
|
||||
const tooltipData = createTooltipData(datum);
|
||||
if (!isDefined(tooltipData)) return null;
|
||||
@@ -97,23 +94,23 @@ export const GraphWidgetPieChart = ({
|
||||
<StyledContainer id={id}>
|
||||
<GraphWidgetChartContainer
|
||||
$isClickable={hasClickableItems}
|
||||
$cursorSelector='svg g path[fill^="url(#"]'
|
||||
$cursorSelector="svg g path"
|
||||
>
|
||||
<ResponsivePie
|
||||
data={data}
|
||||
innerRadius={0.8}
|
||||
colors={enrichedData.map((item) => `url(#${item.gradientId})`)}
|
||||
borderWidth={0}
|
||||
enableArcLinkLabels={false}
|
||||
enableArcLabels={false}
|
||||
tooltip={renderTooltip}
|
||||
onClick={handleSliceClick}
|
||||
onMouseEnter={(datum) => setHoveredSliceId(datum.id)}
|
||||
onMouseLeave={() => setHoveredSliceId(null)}
|
||||
defs={defs}
|
||||
fill={fill}
|
||||
layers={['arcs', renderSliceEndLines]}
|
||||
/>
|
||||
<StyledPieChartWrapper>
|
||||
<ResponsivePie
|
||||
data={data}
|
||||
innerRadius={0.8}
|
||||
colors={enrichedData.map((item) => item.colorScheme.solid)}
|
||||
borderWidth={0}
|
||||
enableArcLinkLabels={false}
|
||||
enableArcLabels={false}
|
||||
tooltip={renderTooltip}
|
||||
onClick={handleSliceClick}
|
||||
onMouseEnter={(datum) => setHoveredSliceId(datum.id)}
|
||||
onMouseLeave={() => setHoveredSliceId(null)}
|
||||
layers={['arcs']}
|
||||
/>
|
||||
</StyledPieChartWrapper>
|
||||
</GraphWidgetChartContainer>
|
||||
<GraphWidgetLegend
|
||||
show={showLegend}
|
||||
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
import { ChartSkeletonLoader } from '@/page-layout/widgets/graph/components/ChartSkeletonLoader';
|
||||
import { GraphWidgetChartHasTooManyGroupsEffect } from '@/page-layout/widgets/graph/components/GraphWidgetChartHasTooManyGroupsEffect';
|
||||
import { useGraphPieChartWidgetData } from '@/page-layout/widgets/graph/graphWidgetPieChart/hooks/useGraphPieChartWidgetData';
|
||||
import { type PieChartDataItem } from '@/page-layout/widgets/graph/graphWidgetPieChart/types/PieChartDataItem';
|
||||
import { coreIndexViewIdFromObjectMetadataItemFamilySelector } from '@/views/states/selectors/coreIndexViewIdFromObjectMetadataItemFamilySelector';
|
||||
import { lazy, Suspense } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { AppPath } from 'twenty-shared/types';
|
||||
import { getAppPath, isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
type PageLayoutWidget,
|
||||
type PieChartConfiguration,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
const GraphWidgetPieChart = lazy(() =>
|
||||
import(
|
||||
'@/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChart'
|
||||
).then((module) => ({
|
||||
default: module.GraphWidgetPieChart,
|
||||
})),
|
||||
);
|
||||
|
||||
export const GraphWidgetPieChartRenderer = ({
|
||||
widget,
|
||||
}: {
|
||||
widget: PageLayoutWidget;
|
||||
}) => {
|
||||
const { data, loading, hasTooManyGroups, objectMetadataItem } =
|
||||
useGraphPieChartWidgetData({
|
||||
objectMetadataItemId: widget.objectMetadataId,
|
||||
configuration: widget.configuration as PieChartConfiguration,
|
||||
});
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const indexViewId = useRecoilValue(
|
||||
coreIndexViewIdFromObjectMetadataItemFamilySelector({
|
||||
objectMetadataItemId: objectMetadataItem.id,
|
||||
}),
|
||||
);
|
||||
|
||||
const handleSliceClick = (_datum: PieChartDataItem) => {
|
||||
return navigate(
|
||||
getAppPath(
|
||||
AppPath.RecordIndexPage,
|
||||
{
|
||||
objectNamePlural: objectMetadataItem.namePlural,
|
||||
},
|
||||
isDefined(indexViewId) ? { viewId: indexViewId } : undefined,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <ChartSkeletonLoader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={<ChartSkeletonLoader />}>
|
||||
<GraphWidgetChartHasTooManyGroupsEffect
|
||||
hasTooManyGroups={hasTooManyGroups}
|
||||
/>
|
||||
<GraphWidgetPieChart
|
||||
data={data}
|
||||
id={widget.id}
|
||||
displayType="shortNumber"
|
||||
onSliceClick={handleSliceClick}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export const PIE_CHART_HOVER_BRIGHTNESS = 0.85;
|
||||
+1
@@ -0,0 +1 @@
|
||||
export const PIE_CHART_MAXIMUM_NUMBER_OF_SLICES = 50;
|
||||
+4
-56
@@ -67,7 +67,6 @@ describe('usePieChartData', () => {
|
||||
usePieChartData({
|
||||
data: mockData,
|
||||
colorRegistry: mockColorRegistry,
|
||||
id: 'test-chart',
|
||||
hoveredSliceId: null,
|
||||
}),
|
||||
);
|
||||
@@ -80,7 +79,6 @@ describe('usePieChartData', () => {
|
||||
percentage: 30,
|
||||
colorScheme: mockColorRegistry.red,
|
||||
isHovered: false,
|
||||
gradientId: 'redGradient-test-chart-0',
|
||||
});
|
||||
expect(result.current.enrichedData[1].percentage).toBe(50);
|
||||
expect(result.current.enrichedData[2].percentage).toBe(20);
|
||||
@@ -91,7 +89,6 @@ describe('usePieChartData', () => {
|
||||
usePieChartData({
|
||||
data: mockData,
|
||||
colorRegistry: mockColorRegistry,
|
||||
id: 'test-chart',
|
||||
hoveredSliceId: null,
|
||||
}),
|
||||
);
|
||||
@@ -107,7 +104,6 @@ describe('usePieChartData', () => {
|
||||
usePieChartData({
|
||||
data: mockData,
|
||||
colorRegistry: mockColorRegistry,
|
||||
id: 'test-chart',
|
||||
hoveredSliceId,
|
||||
}),
|
||||
{ initialProps: { hoveredSliceId: null as DatumId | null } },
|
||||
@@ -121,57 +117,16 @@ describe('usePieChartData', () => {
|
||||
expect(result.current.enrichedData[2].isHovered).toBe(false);
|
||||
});
|
||||
|
||||
it('should generate gradient definitions', () => {
|
||||
const { result } = renderHook(() =>
|
||||
usePieChartData({
|
||||
data: mockData,
|
||||
colorRegistry: mockColorRegistry,
|
||||
id: 'test-chart',
|
||||
hoveredSliceId: 'item1',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current.defs).toHaveLength(3);
|
||||
expect(result.current.defs[0]).toMatchObject({
|
||||
id: 'redGradient-test-chart-0',
|
||||
type: 'linearGradient',
|
||||
colors: [
|
||||
{ offset: 0, color: 'red3' },
|
||||
{ offset: 100, color: 'red4' },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate fill configuration', () => {
|
||||
const { result } = renderHook(() =>
|
||||
usePieChartData({
|
||||
data: mockData,
|
||||
colorRegistry: mockColorRegistry,
|
||||
id: 'test-chart',
|
||||
hoveredSliceId: null,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current.fill).toEqual([
|
||||
{ match: { id: 'item1' }, id: 'redGradient-test-chart-0' },
|
||||
{ match: { id: 'item2' }, id: 'blueGradient-test-chart-1' },
|
||||
{ match: { id: 'item3' }, id: 'redGradient-test-chart-2' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle empty data', () => {
|
||||
const { result } = renderHook(() =>
|
||||
usePieChartData({
|
||||
data: [],
|
||||
colorRegistry: mockColorRegistry,
|
||||
id: 'test-chart',
|
||||
hoveredSliceId: null,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current.enrichedData).toEqual([]);
|
||||
expect(result.current.defs).toEqual([]);
|
||||
expect(result.current.fill).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle single data item', () => {
|
||||
@@ -183,7 +138,6 @@ describe('usePieChartData', () => {
|
||||
usePieChartData({
|
||||
data: singleData,
|
||||
colorRegistry: mockColorRegistry,
|
||||
id: 'test-chart',
|
||||
hoveredSliceId: null,
|
||||
}),
|
||||
);
|
||||
@@ -202,7 +156,6 @@ describe('usePieChartData', () => {
|
||||
usePieChartData({
|
||||
data: dataWithColors,
|
||||
colorRegistry: mockColorRegistry,
|
||||
id: 'test-chart',
|
||||
hoveredSliceId: null,
|
||||
}),
|
||||
);
|
||||
@@ -213,24 +166,19 @@ describe('usePieChartData', () => {
|
||||
|
||||
it('should memoize calculations', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ id }) =>
|
||||
({ hoveredSliceId }) =>
|
||||
usePieChartData({
|
||||
data: mockData,
|
||||
colorRegistry: mockColorRegistry,
|
||||
id,
|
||||
hoveredSliceId: null,
|
||||
hoveredSliceId,
|
||||
}),
|
||||
{ initialProps: { id: 'test-chart' } },
|
||||
{ initialProps: { hoveredSliceId: null as DatumId | null } },
|
||||
);
|
||||
|
||||
const firstEnrichedData = result.current.enrichedData;
|
||||
const firstDefs = result.current.defs;
|
||||
const firstFill = result.current.fill;
|
||||
|
||||
rerender({ id: 'test-chart' });
|
||||
rerender({ hoveredSliceId: null as DatumId | null });
|
||||
|
||||
expect(result.current.enrichedData).toBe(firstEnrichedData);
|
||||
expect(result.current.defs).toBe(firstDefs);
|
||||
expect(result.current.fill).toBe(firstFill);
|
||||
});
|
||||
});
|
||||
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
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 { transformGroupByDataToPieChartData } from '@/page-layout/widgets/graph/graphWidgetPieChart/utils/transformGroupByDataToPieChartData';
|
||||
import { useGraphWidgetGroupByQuery } from '@/page-layout/widgets/graph/hooks/useGraphWidgetGroupByQuery';
|
||||
import { useMemo } from 'react';
|
||||
import { type PieChartConfiguration } from '~/generated/graphql';
|
||||
|
||||
type UseGraphPieChartWidgetDataProps = {
|
||||
objectMetadataItemId: string;
|
||||
configuration: PieChartConfiguration;
|
||||
};
|
||||
|
||||
type UseGraphPieChartWidgetDataResult = {
|
||||
data: PieChartDataItem[];
|
||||
loading: boolean;
|
||||
error?: Error;
|
||||
hasTooManyGroups: boolean;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
};
|
||||
|
||||
// TODO: Remove this once backend returns total group count
|
||||
const EXTRA_ITEM_TO_DETECT_TOO_MANY_GROUPS = 1;
|
||||
|
||||
export const useGraphPieChartWidgetData = ({
|
||||
objectMetadataItemId,
|
||||
configuration,
|
||||
}: UseGraphPieChartWidgetDataProps): UseGraphPieChartWidgetDataResult => {
|
||||
const { objectMetadataItem } = useObjectMetadataItemById({
|
||||
objectId: objectMetadataItemId,
|
||||
});
|
||||
|
||||
const {
|
||||
data: groupByData,
|
||||
loading,
|
||||
error,
|
||||
aggregateOperation,
|
||||
} = useGraphWidgetGroupByQuery({
|
||||
objectMetadataItemId,
|
||||
configuration,
|
||||
limit:
|
||||
PIE_CHART_MAXIMUM_NUMBER_OF_SLICES + EXTRA_ITEM_TO_DETECT_TOO_MANY_GROUPS,
|
||||
});
|
||||
|
||||
const transformedData = useMemo(
|
||||
() =>
|
||||
transformGroupByDataToPieChartData({
|
||||
groupByData,
|
||||
objectMetadataItem,
|
||||
configuration,
|
||||
aggregateOperation,
|
||||
}),
|
||||
[groupByData, objectMetadataItem, configuration, aggregateOperation],
|
||||
);
|
||||
|
||||
return {
|
||||
...transformedData,
|
||||
objectMetadataItem,
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
};
|
||||
+1
-26
@@ -3,7 +3,6 @@ import { type PieChartEnrichedData } from '@/page-layout/widgets/graph/graphWidg
|
||||
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 { createGradientDef } from '@/page-layout/widgets/graph/utils/createGradientDef';
|
||||
import { getColorScheme } from '@/page-layout/widgets/graph/utils/getColorScheme';
|
||||
import { type DatumId } from '@nivo/pie';
|
||||
import { useMemo } from 'react';
|
||||
@@ -11,14 +10,12 @@ import { useMemo } from 'react';
|
||||
type UsePieChartDataProps = {
|
||||
data: PieChartDataItem[];
|
||||
colorRegistry: GraphColorRegistry;
|
||||
id: string;
|
||||
hoveredSliceId: DatumId | null;
|
||||
};
|
||||
|
||||
export const usePieChartData = ({
|
||||
data,
|
||||
colorRegistry,
|
||||
id,
|
||||
hoveredSliceId,
|
||||
}: UsePieChartDataProps) => {
|
||||
const enrichedData = useMemo((): PieChartEnrichedData[] => {
|
||||
@@ -34,7 +31,6 @@ export const usePieChartData = ({
|
||||
});
|
||||
|
||||
const isHovered = hoveredSliceId === item.id;
|
||||
const gradientId = `${colorScheme.name}Gradient-${id}-${index}`;
|
||||
const percentage = calculatePieChartPercentage(item.value, totalValue);
|
||||
|
||||
const angles = calculatePieChartAngles(percentage, cumulativeAngle);
|
||||
@@ -42,32 +38,13 @@ export const usePieChartData = ({
|
||||
|
||||
return {
|
||||
...item,
|
||||
gradientId,
|
||||
colorScheme,
|
||||
isHovered,
|
||||
percentage,
|
||||
middleAngle: angles.middleAngle,
|
||||
};
|
||||
});
|
||||
}, [data, colorRegistry, id, hoveredSliceId]);
|
||||
|
||||
const defs = useMemo(() => {
|
||||
return enrichedData.map((item) =>
|
||||
createGradientDef(
|
||||
item.colorScheme,
|
||||
item.gradientId,
|
||||
item.isHovered,
|
||||
item.middleAngle,
|
||||
),
|
||||
);
|
||||
}, [enrichedData]);
|
||||
|
||||
const fill = useMemo(() => {
|
||||
return enrichedData.map((item) => ({
|
||||
match: { id: item.id },
|
||||
id: item.gradientId,
|
||||
}));
|
||||
}, [enrichedData]);
|
||||
}, [data, colorRegistry, hoveredSliceId]);
|
||||
|
||||
const enrichedDataMap = useMemo(
|
||||
() => new Map(enrichedData.map((item) => [item.id, item])),
|
||||
@@ -77,7 +54,5 @@ export const usePieChartData = ({
|
||||
return {
|
||||
enrichedData,
|
||||
enrichedDataMap,
|
||||
defs,
|
||||
fill,
|
||||
};
|
||||
};
|
||||
|
||||
+13
-4
@@ -5,21 +5,30 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type UsePieChartHandlersProps = {
|
||||
data: PieChartDataItem[];
|
||||
onSliceClick?: (datum: PieChartDataItem) => void;
|
||||
};
|
||||
|
||||
export const usePieChartHandlers = ({ data }: UsePieChartHandlersProps) => {
|
||||
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?.to)) {
|
||||
window.location.href = clickedItem.to;
|
||||
if (isDefined(clickedItem)) {
|
||||
if (isDefined(onSliceClick)) {
|
||||
onSliceClick(clickedItem);
|
||||
} else if (isDefined(clickedItem.to)) {
|
||||
window.location.href = clickedItem.to;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const hasClickableItems = data.some((item) => isDefined(item.to));
|
||||
const hasClickableItems =
|
||||
isDefined(onSliceClick) || data.some((item) => isDefined(item.to));
|
||||
|
||||
return {
|
||||
hoveredSliceId,
|
||||
|
||||
-1
@@ -2,7 +2,6 @@ import { type PieChartDataItem } from '@/page-layout/widgets/graph/graphWidgetPi
|
||||
import { type GraphColorScheme } from '@/page-layout/widgets/graph/types/GraphColorScheme';
|
||||
|
||||
export type PieChartEnrichedData = PieChartDataItem & {
|
||||
gradientId: string;
|
||||
colorScheme: GraphColorScheme;
|
||||
isHovered: boolean;
|
||||
percentage: number;
|
||||
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
||||
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 { computeAggregateValueFromGroupByResult } from '@/page-layout/widgets/graph/utils/computeAggregateValueFromGroupByResult';
|
||||
import { formatDimensionValue } from '@/page-layout/widgets/graph/utils/formatDimensionValue';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type PieChartConfiguration } from '~/generated/graphql';
|
||||
|
||||
type TransformGroupByDataToPieChartDataParams = {
|
||||
groupByData: Record<string, GroupByRawResult[]> | null | undefined;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
configuration: PieChartConfiguration;
|
||||
aggregateOperation: string;
|
||||
};
|
||||
|
||||
type TransformGroupByDataToPieChartDataResult = {
|
||||
data: PieChartDataItem[];
|
||||
hasTooManyGroups: boolean;
|
||||
};
|
||||
|
||||
const EMPTY_PIE_CHART_RESULT: TransformGroupByDataToPieChartDataResult = {
|
||||
data: [],
|
||||
hasTooManyGroups: false,
|
||||
};
|
||||
|
||||
export const transformGroupByDataToPieChartData = ({
|
||||
groupByData,
|
||||
objectMetadataItem,
|
||||
configuration,
|
||||
aggregateOperation,
|
||||
}: TransformGroupByDataToPieChartDataParams): TransformGroupByDataToPieChartDataResult => {
|
||||
if (!isDefined(groupByData)) {
|
||||
return EMPTY_PIE_CHART_RESULT;
|
||||
}
|
||||
|
||||
const groupByField = objectMetadataItem.fields.find(
|
||||
(field: FieldMetadataItem) =>
|
||||
field.id === configuration.groupByFieldMetadataId,
|
||||
);
|
||||
|
||||
const aggregateField = objectMetadataItem.fields.find(
|
||||
(field: FieldMetadataItem) =>
|
||||
field.id === configuration.aggregateFieldMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(groupByField) || !isDefined(aggregateField)) {
|
||||
return EMPTY_PIE_CHART_RESULT;
|
||||
}
|
||||
|
||||
const queryName = getGroupByQueryResultGqlFieldName(objectMetadataItem);
|
||||
const rawResults = groupByData[queryName];
|
||||
|
||||
if (!isDefined(rawResults) || !Array.isArray(rawResults)) {
|
||||
return EMPTY_PIE_CHART_RESULT;
|
||||
}
|
||||
|
||||
// TODO: Add a limit to the query instead of slicing here (issue: twentyhq/core-team-issues#1600)
|
||||
const limitedResults = rawResults.slice(
|
||||
0,
|
||||
PIE_CHART_MAXIMUM_NUMBER_OF_SLICES,
|
||||
);
|
||||
|
||||
const data: PieChartDataItem[] = limitedResults.map((result) => {
|
||||
const dimensionValues = result.groupByDimensionValues;
|
||||
|
||||
const label = isDefined(dimensionValues?.[0])
|
||||
? formatDimensionValue({
|
||||
value: dimensionValues[0],
|
||||
fieldMetadata: groupByField,
|
||||
dateGranularity: configuration.dateGranularity ?? undefined,
|
||||
subFieldName: configuration.groupBySubFieldName ?? undefined,
|
||||
})
|
||||
: '';
|
||||
|
||||
const value = computeAggregateValueFromGroupByResult({
|
||||
rawResult: result,
|
||||
aggregateField,
|
||||
aggregateOperation:
|
||||
configuration.aggregateOperation as unknown as ExtendedAggregateOperations,
|
||||
aggregateOperationFromRawResult: aggregateOperation,
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
return {
|
||||
id: label,
|
||||
value: value,
|
||||
label: label,
|
||||
color: (configuration.color as GraphColor | undefined) ?? undefined,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
data,
|
||||
hasTooManyGroups: rawResults.length > PIE_CHART_MAXIMUM_NUMBER_OF_SLICES,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user