nitin
2026-01-15 18:10:28 +05:30
committed by GitHub
parent d8f0115fef
commit 55b4ac4314
5 changed files with 92 additions and 75 deletions
@@ -1,7 +1,8 @@
import { LEGEND_HIGHLIGHT_DIMMED_OPACITY } from '@/page-layout/widgets/graph/constants/LegendHighlightDimmedOpacity.constant';
import { BAR_CHART_CONSTANTS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartConstants';
import { graphWidgetHoveredSliceIndexComponentState } from '@/page-layout/widgets/graph/graphWidgetBarChart/states/graphWidgetHoveredSliceIndexComponentState';
import { graphWidgetIsSliceHoveredComponentFamilySelector } from '@/page-layout/widgets/graph/graphWidgetBarChart/states/graphWidgetIsSliceHoveredComponentFamilySelector';
import { graphWidgetHighlightedLegendIdComponentState } from '@/page-layout/widgets/graph/states/graphWidgetHighlightedLegendIdComponentState';
import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { type BarDatum, type BarItemProps } from '@nivo/bar';
import { animated, to } from '@react-spring/web';
@@ -62,18 +63,15 @@ export const CustomBarItem = <D extends BarDatum>({
graphWidgetHighlightedLegendIdComponentState,
);
const hoveredSliceIndex = useRecoilComponentValue(
graphWidgetHoveredSliceIndexComponentState,
const isSliceHovered = useRecoilComponentFamilyValue(
graphWidgetIsSliceHoveredComponentFamilySelector,
String(barData.indexValue),
);
const isDimmed =
isDefined(highlightedLegendId) &&
String(highlightedLegendId) !== String(barData.id);
const isSliceHovered =
isDefined(hoveredSliceIndex) &&
String(barData.indexValue) === hoveredSliceIndex;
const isNegativeValue = useMemo(
() => isNumber(barData.value) && barData.value < 0,
[barData.value],
@@ -3,14 +3,11 @@ import { type BarChartSlice } from '@/page-layout/widgets/graph/graphWidgetBarCh
import { computeSliceHighlightPosition } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/computeSliceHighlightPosition';
import { computeSlicesFromBars } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/computeSlicesFromBars';
import { findSliceAtPosition } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/findSliceAtPosition';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { useTheme } from '@emotion/react';
import { type BarDatum, type ComputedBarDatum } from '@nivo/bar';
import { animated, useSpring } from '@react-spring/web';
import { useMemo, type MouseEvent } from 'react';
import { useRecoilCallback } from 'recoil';
import { AnimatePresence, motion } from 'framer-motion';
import { useCallback, useMemo, type MouseEvent } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { BarChartLayout } from '~/generated/graphql';
@@ -44,13 +41,8 @@ export const CustomSliceHoverLayer = ({
onSliceLeave,
}: CustomSliceHoverLayerProps) => {
const theme = useTheme();
const hoveredSliceIndex = useRecoilComponentValue(
graphWidgetHoveredSliceIndexComponentState,
);
const hoveredSliceIndexState = useRecoilComponentCallbackState(
graphWidgetHoveredSliceIndexComponentState,
);
const setHoveredSliceIndex = useSetRecoilComponentState(
const hoveredSliceIndexValue = useRecoilComponentValue(
graphWidgetHoveredSliceIndexComponentState,
);
@@ -61,43 +53,36 @@ export const CustomSliceHoverLayer = ({
[bars, isVerticalLayout],
);
const handleMouseMove = useRecoilCallback(
({ snapshot }) =>
(event: MouseEvent<SVGRectElement>) => {
const sliceData = findSliceAtPosition({
event,
slices,
marginLeft,
marginTop,
isVerticalLayout,
});
const currentHoveredSliceIndex = snapshot
.getLoadable(hoveredSliceIndexState)
.getValue();
const handleMouseMove = useCallback(
(event: MouseEvent<SVGRectElement>) => {
const sliceData = findSliceAtPosition({
event,
slices,
marginLeft,
marginTop,
isVerticalLayout,
});
if (!isDefined(sliceData)) {
if (isDefined(currentHoveredSliceIndex)) {
setHoveredSliceIndex(null);
onSliceHover(null);
}
return;
if (!isDefined(sliceData)) {
if (isDefined(hoveredSliceIndexValue)) {
onSliceHover(null);
}
return;
}
if (sliceData.slice.indexValue === currentHoveredSliceIndex) {
return;
}
if (sliceData.slice.indexValue === hoveredSliceIndexValue) {
return;
}
setHoveredSliceIndex(sliceData.slice.indexValue);
onSliceHover(sliceData);
},
onSliceHover(sliceData);
},
[
hoveredSliceIndexState,
setHoveredSliceIndex,
onSliceHover,
slices,
marginLeft,
marginTop,
isVerticalLayout,
hoveredSliceIndexValue,
],
);
@@ -126,11 +111,11 @@ export const CustomSliceHoverLayer = ({
};
const hoveredSlice = useMemo(() => {
if (!isDefined(hoveredSliceIndex)) {
if (!isDefined(hoveredSliceIndexValue)) {
return null;
}
return slices.find((slice) => slice.indexValue === hoveredSliceIndex);
}, [slices, hoveredSliceIndex]);
return slices.find((slice) => slice.indexValue === hoveredSliceIndexValue);
}, [slices, hoveredSliceIndexValue]);
const highlightPosition = computeSliceHighlightPosition({
sliceCenter: hoveredSlice?.sliceCenter ?? null,
@@ -139,31 +124,34 @@ export const CustomSliceHoverLayer = ({
innerHeight,
});
const { opacity } = useSpring({
opacity: isDefined(hoveredSlice) ? 1 : 0,
config: {
tension: 300,
friction: 30,
},
});
if (bars.length === 0) {
return null;
}
return (
<g>
<animated.g
transform={`translate(${highlightPosition.x}, ${highlightPosition.y})`}
opacity={opacity}
>
<rect
width={highlightPosition.width}
height={highlightPosition.height}
fill={theme.background.transparent.medium}
style={{ pointerEvents: 'none' }}
/>
</animated.g>
<AnimatePresence>
{isDefined(hoveredSlice) && (
<motion.g
key="highlight"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{
duration: theme.animation.duration.fast,
ease: 'easeInOut',
}}
transform={`translate(${highlightPosition.x}, ${highlightPosition.y})`}
>
<rect
width={highlightPosition.width}
height={highlightPosition.height}
fill={theme.background.transparent.medium}
style={{ pointerEvents: 'none' }}
/>
</motion.g>
)}
</AnimatePresence>
<rect
x={0}
@@ -107,6 +107,15 @@ export const GraphWidgetBarChart = ({
const [chartHeight, setChartHeight] = useState<number>(0);
const containerRef = useRef<HTMLDivElement>(null);
const debouncedSetChartDimensions = useDebouncedCallback(
(width: number, height: number) => {
setChartWidth(width);
setChartHeight(height);
},
300,
);
const setActiveBarTooltip = useSetRecoilComponentState(
graphWidgetBarTooltipComponentState,
);
@@ -193,6 +202,7 @@ export const GraphWidgetBarChart = ({
) => {
if (isDefined(sliceData)) {
debouncedHideTooltip.cancel();
setHoveredSliceIndex(sliceData.slice.indexValue);
setActiveBarTooltip({
slice: sliceData.slice,
offsetLeft: sliceData.offsetLeft,
@@ -305,8 +315,7 @@ export const GraphWidgetBarChart = ({
<NodeDimensionEffect
elementRef={containerRef}
onDimensionChange={({ width, height }) => {
setChartWidth(width);
setChartHeight(height);
debouncedSetChartDimensions(width, height);
}}
/>
<ResponsiveBar
@@ -0,0 +1,19 @@
import { graphWidgetHoveredSliceIndexComponentState } from '@/page-layout/widgets/graph/graphWidgetBarChart/states/graphWidgetHoveredSliceIndexComponentState';
import { WidgetComponentInstanceContext } from '@/page-layout/widgets/states/contexts/WidgetComponentInstanceContext';
import { createComponentFamilySelector } from '@/ui/utilities/state/component-state/utils/createComponentFamilySelector';
import { isDefined } from 'twenty-shared/utils';
export const graphWidgetIsSliceHoveredComponentFamilySelector =
createComponentFamilySelector<boolean, string>({
key: 'graphWidgetIsSliceHoveredComponentFamilySelector',
componentInstanceContext: WidgetComponentInstanceContext,
get:
({ instanceId, familyKey }: { instanceId: string; familyKey: string }) =>
({ get }) => {
const hoveredSliceIndex = get(
graphWidgetHoveredSliceIndexComponentState.atomFamily({ instanceId }),
);
return isDefined(hoveredSliceIndex) && hoveredSliceIndex === familyKey;
},
});
@@ -85,7 +85,7 @@ export const GraphWidgetLineChart = ({
id,
rangeMin,
rangeMax,
omitNullValues: _omitNullValues = false,
omitNullValues = false,
displayType,
groupMode,
colorMode,
@@ -101,6 +101,11 @@ export const GraphWidgetLineChart = ({
const containerRef = useRef<HTMLDivElement>(null);
const [chartWidth, setChartWidth] = useState(0);
const debouncedSetChartWidth = useDebouncedCallback(
(width: number) => setChartWidth(width),
300,
);
const formatOptions: GraphValueFormatOptions = {
displayType,
decimals,
@@ -195,7 +200,7 @@ export const GraphWidgetLineChart = ({
formatValue={(value) => formatGraphValue(value, formatOptions)}
offset={theme.spacingMultiplicator * 2}
groupMode={groupMode}
omitNullValues={_omitNullValues}
omitNullValues={omitNullValues}
enablePointLabel={enablePointLabel}
/>
);
@@ -293,9 +298,7 @@ export const GraphWidgetLineChart = ({
>
<NodeDimensionEffect
elementRef={containerRef}
onDimensionChange={({ width }) => {
setChartWidth(width);
}}
onDimensionChange={({ width }) => debouncedSetChartWidth(width)}
/>
<ResponsiveLine
data={nivoData}