diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidgetFloatingTooltip.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidgetFloatingTooltip.tsx index 4e319fd481..d363b1c101 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidgetFloatingTooltip.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidgetFloatingTooltip.tsx @@ -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 = ({
= { title: 'Modules/PageLayout/Widgets/GraphWidgetPieChart', component: GraphWidgetPieChart, decorators: [ + (Story) => ( + + + + ), ComponentDecorator, I18nFrontDecorator, ObjectMetadataItemsDecorator, @@ -360,6 +369,13 @@ export const Storage: Story = { export const Catalog: Story = { decorators: [ + (Story) => ( + + + + ), CatalogDecorator, I18nFrontDecorator, ObjectMetadataItemsDecorator, diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/components/GraphPieChartTooltip.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/components/GraphPieChartTooltip.tsx new file mode 100644 index 0000000000..50251847d8 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/components/GraphPieChartTooltip.tsx @@ -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 ( + + ); +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChart.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChart.tsx index ae063d786e..c1e7e0b006 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChart.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChart.tsx @@ -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(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) => { if (isDefined(onSliceClick)) { onSliceClick(datum.data); } }; - const renderTooltip = ({ datum }: PieTooltipProps) => { - const tooltipData = createTooltipData(datum); - if (!isDefined(tooltipData)) return null; + const handleSliceMove = ( + datum: ComputedDatum, + event: ReactMouseEvent, + ) => { + 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 ( - - ); + const handleSliceLeave = () => { + setActivePieTooltip(null); }; const hasNoData = useMemo( @@ -130,8 +128,10 @@ export const GraphWidgetPieChart = ({ return ( null : renderTooltip} + tooltip={() => null} onClick={hasNoData ? undefined : (datum) => handleSliceClick(datum)} + onMouseMove={hasNoData ? undefined : handleSliceMove} + onMouseLeave={hasNoData ? undefined : handleSliceLeave} layers={['arcs', 'arcLinkLabels']} arcLinkLabel={(datum: ComputedDatum) => { - 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 = ({ /> + ({ diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/hooks/usePieChartTooltip.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/hooks/usePieChartTooltip.ts deleted file mode 100644 index 970395f113..0000000000 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/hooks/usePieChartTooltip.ts +++ /dev/null @@ -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, - }; -}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/states/graphWidgetPieTooltipComponentState.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/states/graphWidgetPieTooltipComponentState.ts new file mode 100644 index 0000000000..6955e3479f --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/states/graphWidgetPieTooltipComponentState.ts @@ -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; + offsetLeft: number; + offsetTop: number; +} | null>({ + key: 'graphWidgetPieTooltipComponentState', + defaultValue: null, + componentInstanceContext: GraphWidgetComponentInstanceContext, +}); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/utils/getPieChartFormattedValue.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/utils/getPieChartFormattedValue.ts new file mode 100644 index 0000000000..e38f4835a0 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/utils/getPieChartFormattedValue.ts @@ -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; + 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)}%)`; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/utils/getPieChartTooltipData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/utils/getPieChartTooltipData.ts new file mode 100644 index 0000000000..9a9f7d95ca --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/utils/getPieChartTooltipData.ts @@ -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; + 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, + }, + }; +};