nitin
2025-12-04 22:52:21 +05:30
committed by GitHub
parent aede5bab34
commit 8f756937ae
8 changed files with 228 additions and 80 deletions
@@ -17,6 +17,7 @@ type GraphWidgetFloatingTooltipProps = {
onGraphWidgetTooltipClick?: () => void;
onMouseEnter?: () => void;
onMouseLeave?: () => void;
disablePointerEvents?: boolean;
};
export const GraphWidgetFloatingTooltip = ({
@@ -28,6 +29,7 @@ export const GraphWidgetFloatingTooltip = ({
onGraphWidgetTooltipClick,
onMouseEnter,
onMouseLeave,
disablePointerEvents = false,
}: GraphWidgetFloatingTooltipProps) => {
const theme = useTheme();
@@ -44,7 +46,11 @@ export const GraphWidgetFloatingTooltip = ({
<FloatingPortal root={boundary}>
<div
ref={refs.setFloating}
style={{ ...floatingStyles, zIndex: theme.lastLayerZIndex }}
style={{
...floatingStyles,
zIndex: theme.lastLayerZIndex,
pointerEvents: disablePointerEvents ? 'none' : 'auto',
}}
role="tooltip"
aria-live="polite"
onMouseEnter={onMouseEnter}
@@ -1,7 +1,9 @@
import { type Meta, type StoryObj } from '@storybook/react';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { GRAPH_WIDGET_TEST_INSTANCE_ID } from '@/page-layout/widgets/graph/__tests__/GraphWidgetTestWrapper';
import { GraphWidgetPieChart } from '@/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChart';
import { GraphWidgetComponentInstanceContext } from '@/page-layout/widgets/graph/states/contexts/GraphWidgetComponentInstanceContext';
import { CatalogDecorator, ComponentDecorator } from 'twenty-ui/testing';
import {
AggregateOperations,
@@ -34,6 +36,13 @@ const meta: Meta<typeof GraphWidgetPieChart> = {
title: 'Modules/PageLayout/Widgets/GraphWidgetPieChart',
component: GraphWidgetPieChart,
decorators: [
(Story) => (
<GraphWidgetComponentInstanceContext.Provider
value={{ instanceId: GRAPH_WIDGET_TEST_INSTANCE_ID }}
>
<Story />
</GraphWidgetComponentInstanceContext.Provider>
),
ComponentDecorator,
I18nFrontDecorator,
ObjectMetadataItemsDecorator,
@@ -360,6 +369,13 @@ export const Storage: Story = {
export const Catalog: Story = {
decorators: [
(Story) => (
<GraphWidgetComponentInstanceContext.Provider
value={{ instanceId: GRAPH_WIDGET_TEST_INSTANCE_ID }}
>
<Story />
</GraphWidgetComponentInstanceContext.Provider>
),
CatalogDecorator,
I18nFrontDecorator,
ObjectMetadataItemsDecorator,
@@ -0,0 +1,69 @@
import { GraphWidgetFloatingTooltip } from '@/page-layout/widgets/graph/components/GraphWidgetFloatingTooltip';
import { graphWidgetPieTooltipComponentState } from '@/page-layout/widgets/graph/graphWidgetPieChart/states/graphWidgetPieTooltipComponentState';
import { type PieChartDataItem } from '@/page-layout/widgets/graph/graphWidgetPieChart/types/PieChartDataItem';
import { type PieChartEnrichedData } from '@/page-layout/widgets/graph/graphWidgetPieChart/types/PieChartEnrichedData';
import { getPieChartTooltipData } from '@/page-layout/widgets/graph/graphWidgetPieChart/utils/getPieChartTooltipData';
import { createVirtualElementFromContainerOffset } from '@/page-layout/widgets/graph/utils/createVirtualElementFromContainerOffset';
import { type GraphValueFormatOptions } from '@/page-layout/widgets/graph/utils/graphFormatters';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { isDefined } from 'twenty-shared/utils';
type GraphPieChartTooltipProps = {
containerId: string;
enrichedData: PieChartEnrichedData[];
formatOptions: GraphValueFormatOptions;
displayType?: string;
onSliceClick?: (datum: PieChartDataItem) => void;
};
export const GraphPieChartTooltip = ({
containerId,
enrichedData,
formatOptions,
displayType,
onSliceClick,
}: GraphPieChartTooltipProps) => {
const tooltipState = useRecoilComponentValue(
graphWidgetPieTooltipComponentState,
);
if (!isDefined(tooltipState)) {
return null;
}
const containerElement = document.getElementById(containerId);
if (!isDefined(containerElement)) {
return null;
}
const tooltipData = getPieChartTooltipData({
datum: tooltipState.datum,
enrichedData,
formatOptions,
displayType,
});
const handleTooltipClick: (() => void) | undefined = isDefined(onSliceClick)
? () => onSliceClick(tooltipState.datum.data)
: undefined;
if (!isDefined(tooltipData)) {
return null;
}
const reference = createVirtualElementFromContainerOffset(
containerElement,
tooltipState.offsetLeft,
tooltipState.offsetTop,
);
return (
<GraphWidgetFloatingTooltip
reference={reference}
boundary={containerElement}
items={[tooltipData.tooltipItem]}
disablePointerEvents
onGraphWidgetTooltipClick={handleTooltipClick}
/>
);
};
@@ -1,22 +1,20 @@
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 { GraphPieChartTooltip } from '@/page-layout/widgets/graph/graphWidgetPieChart/components/GraphPieChartTooltip';
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 { usePieChartTooltip } from '@/page-layout/widgets/graph/graphWidgetPieChart/hooks/usePieChartTooltip';
import { graphWidgetPieTooltipComponentState } from '@/page-layout/widgets/graph/graphWidgetPieChart/states/graphWidgetPieTooltipComponentState';
import { type PieChartDataItem } from '@/page-layout/widgets/graph/graphWidgetPieChart/types/PieChartDataItem';
import { getPieChartFormattedValue } from '@/page-layout/widgets/graph/graphWidgetPieChart/utils/getPieChartFormattedValue';
import { createGraphColorRegistry } from '@/page-layout/widgets/graph/utils/createGraphColorRegistry';
import { type GraphValueFormatOptions } from '@/page-layout/widgets/graph/utils/graphFormatters';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import {
ResponsivePie,
type ComputedDatum,
type PieTooltipProps,
} from '@nivo/pie';
import { useMemo } from 'react';
import { ResponsivePie, type ComputedDatum } from '@nivo/pie';
import { useMemo, useRef, type MouseEvent as ReactMouseEvent } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { type PieChartConfiguration } from '~/generated/graphql';
@@ -75,6 +73,10 @@ export const GraphWidgetPieChart = ({
}: GraphWidgetPieChartProps) => {
const theme = useTheme();
const colorRegistry = createGraphColorRegistry(theme);
const containerRef = useRef<HTMLDivElement>(null);
const setActivePieTooltip = useSetRecoilComponentState(
graphWidgetPieTooltipComponentState,
);
const formatOptions: GraphValueFormatOptions = {
displayType,
@@ -89,32 +91,28 @@ export const GraphWidgetPieChart = ({
colorRegistry,
});
const { createTooltipData } = usePieChartTooltip({
enrichedData,
formatOptions,
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;
const handleSliceMove = (
datum: ComputedDatum<PieChartDataItem>,
event: ReactMouseEvent<SVGPathElement>,
) => {
if (!isDefined(containerRef.current)) return;
const handleTooltipClick: (() => void) | undefined = isDefined(onSliceClick)
? () => handleSliceClick(datum)
: undefined;
const containerRect = containerRef.current.getBoundingClientRect();
setActivePieTooltip({
datum,
offsetLeft: event.clientX - containerRect.left,
offsetTop: event.clientY - containerRect.top,
});
};
return (
<GraphWidgetTooltip
items={[tooltipData.tooltipItem]}
onGraphWidgetTooltipClick={handleTooltipClick}
/>
);
const handleSliceLeave = () => {
setActivePieTooltip(null);
};
const hasNoData = useMemo(
@@ -130,8 +128,10 @@ export const GraphWidgetPieChart = ({
return (
<StyledContainer id={id}>
<GraphWidgetChartContainer
ref={containerRef}
$isClickable={!hasNoData && isDefined(onSliceClick)}
$cursorSelector="svg g path"
onMouseLeave={handleSliceLeave}
>
<StyledPieChartWrapper preventHover={hasNoData}>
<ResponsivePie
@@ -143,15 +143,19 @@ export const GraphWidgetPieChart = ({
borderWidth={0}
enableArcLinkLabels={showDataLabels && !hasNoData}
enableArcLabels={false}
tooltip={hasNoData ? () => null : renderTooltip}
tooltip={() => null}
onClick={hasNoData ? undefined : (datum) => handleSliceClick(datum)}
onMouseMove={hasNoData ? undefined : handleSliceMove}
onMouseLeave={hasNoData ? undefined : handleSliceLeave}
layers={['arcs', 'arcLinkLabels']}
arcLinkLabel={(datum: ComputedDatum<PieChartDataItem>) => {
const tooltipData = createTooltipData(datum);
return (
tooltipData?.tooltipItem.formattedValue ||
datum.data.value.toString()
);
const formattedValue = getPieChartFormattedValue({
datum,
enrichedData,
formatOptions,
displayType,
});
return formattedValue ?? datum.data.value.toString();
}}
arcLinkLabelsDiagonalLength={10}
arcLinkLabelsStraightLength={10}
@@ -173,6 +177,13 @@ export const GraphWidgetPieChart = ({
/>
</StyledPieChartWrapper>
</GraphWidgetChartContainer>
<GraphPieChartTooltip
containerId={id}
enrichedData={enrichedData}
formatOptions={formatOptions}
displayType={displayType}
onSliceClick={onSliceClick}
/>
<GraphWidgetLegend
show={showLegend && !hasNoData}
items={enrichedData.map((item) => ({
@@ -1,47 +0,0 @@
import { type PieChartEnrichedData } from '@/page-layout/widgets/graph/graphWidgetPieChart/types/PieChartEnrichedData';
import {
formatGraphValue,
type GraphValueFormatOptions,
} from '@/page-layout/widgets/graph/utils/graphFormatters';
import { type ComputedDatum } from '@nivo/pie';
import { isDefined } from 'twenty-shared/utils';
type UsePieChartTooltipProps = {
enrichedData: PieChartEnrichedData[];
formatOptions: GraphValueFormatOptions;
displayType?: string;
};
export const usePieChartTooltip = ({
enrichedData,
formatOptions,
displayType,
}: UsePieChartTooltipProps) => {
const createTooltipData = (
datum: ComputedDatum<{ id: string; value: number; label?: string }>,
) => {
const item = enrichedData.find(
(enrichedDataItem) => enrichedDataItem.id === datum.id,
);
if (!isDefined(item)) return null;
const formattedValue =
displayType === 'percentage'
? formatGraphValue(item.percentage / 100, formatOptions)
: `${formatGraphValue(item.value, formatOptions)} (${item.percentage.toFixed(1)}%)`;
return {
tooltipItem: {
key: item.id,
label: item.id,
formattedValue,
value: item.value,
dotColor: item.colorScheme.solid,
},
};
};
return {
createTooltipData,
};
};
@@ -0,0 +1,14 @@
import { GraphWidgetComponentInstanceContext } from '@/page-layout/widgets/graph/states/contexts/GraphWidgetComponentInstanceContext';
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
import { type ComputedDatum } from '@nivo/pie';
import { type PieChartDataItem } from '../types/PieChartDataItem';
export const graphWidgetPieTooltipComponentState = createComponentState<{
datum: ComputedDatum<PieChartDataItem>;
offsetLeft: number;
offsetTop: number;
} | null>({
key: 'graphWidgetPieTooltipComponentState',
defaultValue: null,
componentInstanceContext: GraphWidgetComponentInstanceContext,
});
@@ -0,0 +1,32 @@
import {
formatGraphValue,
type GraphValueFormatOptions,
} from '@/page-layout/widgets/graph/utils/graphFormatters';
import { type ComputedDatum } from '@nivo/pie';
import { isDefined } from 'twenty-shared/utils';
import { type PieChartDataItem } from '../types/PieChartDataItem';
import { type PieChartEnrichedData } from '../types/PieChartEnrichedData';
type GetPieChartFormattedValueParams = {
datum: ComputedDatum<PieChartDataItem>;
enrichedData: PieChartEnrichedData[];
formatOptions: GraphValueFormatOptions;
displayType?: string;
};
export const getPieChartFormattedValue = ({
datum,
enrichedData,
formatOptions,
displayType,
}: GetPieChartFormattedValueParams): string | null => {
const item = enrichedData.find(
(enrichedDataItem) => enrichedDataItem.id === datum.id,
);
if (!isDefined(item)) return null;
return displayType === 'percentage'
? formatGraphValue(item.percentage / 100, formatOptions)
: `${formatGraphValue(item.value, formatOptions)} (${item.percentage.toFixed(1)}%)`;
};
@@ -0,0 +1,47 @@
import { type GraphWidgetTooltipItem } from '@/page-layout/widgets/graph/components/GraphWidgetTooltip';
import { type PieChartDataItem } from '@/page-layout/widgets/graph/graphWidgetPieChart/types/PieChartDataItem';
import { type PieChartEnrichedData } from '@/page-layout/widgets/graph/graphWidgetPieChart/types/PieChartEnrichedData';
import { type GraphValueFormatOptions } from '@/page-layout/widgets/graph/utils/graphFormatters';
import { type ComputedDatum } from '@nivo/pie';
import { isDefined } from 'twenty-shared/utils';
import { getPieChartFormattedValue } from './getPieChartFormattedValue';
type GetPieChartTooltipDataParams = {
datum: ComputedDatum<PieChartDataItem>;
enrichedData: PieChartEnrichedData[];
formatOptions: GraphValueFormatOptions;
displayType?: string;
};
export const getPieChartTooltipData = ({
datum,
enrichedData,
formatOptions,
displayType,
}: GetPieChartTooltipDataParams): {
tooltipItem: GraphWidgetTooltipItem;
} | null => {
const item = enrichedData.find(
(enrichedDataItem) => enrichedDataItem.id === datum.id,
);
if (!isDefined(item)) return null;
const formattedValue = getPieChartFormattedValue({
datum,
enrichedData,
formatOptions,
displayType,
});
if (!isDefined(formattedValue)) return null;
return {
tooltipItem: {
key: item.id,
label: item.id,
formattedValue,
value: item.value,
dotColor: item.colorScheme.solid,
},
};
};