Update bar chart design (#15372)
- Implement new dynamic color palettes - Create custom bar component with rounded edges https://github.com/user-attachments/assets/8246f16d-0239-4807-bb4a-9647367575f2
This commit is contained in:
-45
@@ -1,45 +0,0 @@
|
||||
import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem';
|
||||
import { type BarChartEnrichedKey } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartEnrichedKey';
|
||||
import { calculateBarChartEndLineCoordinates } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/calculateBarChartEndLineCoordinates';
|
||||
import { type ComputedBarDatum } from '@nivo/bar';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type BarChartEndLinesProps = {
|
||||
bars: readonly ComputedBarDatum<BarChartDataItem>[];
|
||||
enrichedKeysMap: Map<string, BarChartEnrichedKey>;
|
||||
layout: 'vertical' | 'horizontal';
|
||||
};
|
||||
|
||||
export const BarChartEndLines = ({
|
||||
bars,
|
||||
enrichedKeysMap,
|
||||
layout,
|
||||
}: BarChartEndLinesProps) => {
|
||||
return (
|
||||
<g>
|
||||
{bars.map((bar: ComputedBarDatum<BarChartDataItem>, index: number) => {
|
||||
const enrichedKey = enrichedKeysMap.get(String(bar.data.id));
|
||||
if (!isDefined(enrichedKey)) {
|
||||
return null;
|
||||
}
|
||||
const lineColor = enrichedKey.colorScheme.solid;
|
||||
const { x1, y1, x2, y2 } = calculateBarChartEndLineCoordinates(
|
||||
bar,
|
||||
layout,
|
||||
);
|
||||
|
||||
return (
|
||||
<line
|
||||
key={`${bar.data.id}-${bar.data.indexValue}-endline-${index}`}
|
||||
x1={x1}
|
||||
y1={y1}
|
||||
x2={x2}
|
||||
y2={y2}
|
||||
stroke={lineColor}
|
||||
strokeWidth={1}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</g>
|
||||
);
|
||||
};
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
import { BAR_CHART_HOVER_BRIGHTNESS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartHoverBrightness';
|
||||
import { type BarDatum, type BarItemProps } from '@nivo/bar';
|
||||
import { Text } from '@nivo/text';
|
||||
import { useTheme } from '@nivo/theming';
|
||||
import { useTooltip } from '@nivo/tooltip';
|
||||
import { animated, to } from '@react-spring/web';
|
||||
import { isNumber } from '@sniptt/guards';
|
||||
import { createElement, useCallback, useMemo, type MouseEvent } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type CustomBarItemProps<D extends BarDatum> = BarItemProps<D> & {
|
||||
keys?: string[];
|
||||
groupMode?: 'grouped' | 'stacked';
|
||||
data?: readonly D[];
|
||||
indexBy?: string;
|
||||
layout?: 'vertical' | 'horizontal';
|
||||
};
|
||||
|
||||
const StyledBarRect = styled(animated.rect)<{ $isInteractive?: boolean }>`
|
||||
cursor: ${({ $isInteractive }) => ($isInteractive ? 'pointer' : 'default')};
|
||||
transition: filter 0.15s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
filter: ${({ $isInteractive }) =>
|
||||
$isInteractive ? `brightness(${BAR_CHART_HOVER_BRIGHTNESS})` : 'none'};
|
||||
}
|
||||
`;
|
||||
|
||||
// This is a copy of the BarItem component from @nivo/bar with some design modifications
|
||||
export const CustomBarItem = <D extends BarDatum>({
|
||||
bar: { data: barData, ...bar },
|
||||
style: {
|
||||
borderColor,
|
||||
color,
|
||||
height,
|
||||
labelColor,
|
||||
labelOpacity,
|
||||
labelX,
|
||||
labelY,
|
||||
transform,
|
||||
width,
|
||||
textAnchor,
|
||||
},
|
||||
borderRadius,
|
||||
borderWidth,
|
||||
label,
|
||||
shouldRenderLabel,
|
||||
isInteractive,
|
||||
onClick,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
tooltip,
|
||||
isFocusable,
|
||||
ariaLabel,
|
||||
ariaLabelledBy,
|
||||
ariaDescribedBy,
|
||||
ariaDisabled,
|
||||
ariaHidden,
|
||||
keys,
|
||||
groupMode = 'grouped',
|
||||
data: chartData,
|
||||
indexBy,
|
||||
layout = 'vertical',
|
||||
}: CustomBarItemProps<D>) => {
|
||||
const theme = useTheme();
|
||||
const { showTooltipFromEvent, showTooltipAt, hideTooltip } = useTooltip();
|
||||
|
||||
const renderTooltip = useMemo(
|
||||
() => () => createElement(tooltip, { ...bar, ...barData }),
|
||||
[tooltip, bar, barData],
|
||||
);
|
||||
|
||||
const handleClick = useCallback(
|
||||
(event: MouseEvent<SVGRectElement>) => {
|
||||
onClick?.({ color: bar.color, ...barData }, event);
|
||||
},
|
||||
[bar, barData, onClick],
|
||||
);
|
||||
const handleTooltip = useCallback(
|
||||
(event: MouseEvent<SVGRectElement>) =>
|
||||
showTooltipFromEvent(renderTooltip(), event),
|
||||
[showTooltipFromEvent, renderTooltip],
|
||||
);
|
||||
const handleMouseEnter = useCallback(
|
||||
(event: MouseEvent<SVGRectElement>) => {
|
||||
onMouseEnter?.(barData, event);
|
||||
showTooltipFromEvent(renderTooltip(), event);
|
||||
},
|
||||
[barData, onMouseEnter, showTooltipFromEvent, renderTooltip],
|
||||
);
|
||||
const handleMouseLeave = useCallback(
|
||||
(event: MouseEvent<SVGRectElement>) => {
|
||||
onMouseLeave?.(barData, event);
|
||||
hideTooltip();
|
||||
},
|
||||
[barData, hideTooltip, onMouseLeave],
|
||||
);
|
||||
|
||||
const handleFocus = useCallback(() => {
|
||||
showTooltipAt(renderTooltip(), [bar.absX + bar.width / 2, bar.absY]);
|
||||
}, [showTooltipAt, renderTooltip, bar]);
|
||||
|
||||
const handleBlur = useCallback(() => {
|
||||
hideTooltip();
|
||||
}, [hideTooltip]);
|
||||
|
||||
const isTopBar = useMemo(() => {
|
||||
const isStackedAndValid =
|
||||
groupMode === 'stacked' &&
|
||||
isDefined(keys) &&
|
||||
keys.length > 0 &&
|
||||
isDefined(chartData) &&
|
||||
isDefined(indexBy);
|
||||
|
||||
if (!isStackedAndValid) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const dataPoint = chartData.find(
|
||||
(data) => data[indexBy] === barData.indexValue,
|
||||
);
|
||||
|
||||
if (!isDefined(dataPoint)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const currentKeyIndex = keys.findIndex((key) => key === barData.id);
|
||||
|
||||
if (currentKeyIndex === -1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const keysAboveCurrentKey = keys.slice(currentKeyIndex + 1);
|
||||
const hasBarAbove = keysAboveCurrentKey.some((key) => {
|
||||
const value = dataPoint[key];
|
||||
return isNumber(value) && value > 0;
|
||||
});
|
||||
|
||||
return !hasBarAbove;
|
||||
}, [groupMode, keys, barData, chartData, indexBy]);
|
||||
|
||||
const isHorizontal = layout === 'horizontal';
|
||||
|
||||
return (
|
||||
<animated.g transform={transform}>
|
||||
{isTopBar && (
|
||||
<defs>
|
||||
<clipPath id={`round-corner-${barData.index}`}>
|
||||
<animated.rect
|
||||
x={isHorizontal ? -borderRadius : 0}
|
||||
y={0}
|
||||
rx={borderRadius}
|
||||
ry={borderRadius}
|
||||
width={to(width, (value) =>
|
||||
Math.max(value + (isHorizontal ? borderRadius : 0), 0),
|
||||
)}
|
||||
height={to(height, (value) =>
|
||||
Math.max(value + (isHorizontal ? 0 : borderRadius), 0),
|
||||
)}
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
)}
|
||||
|
||||
<StyledBarRect
|
||||
$isInteractive={isInteractive}
|
||||
clipPath={isTopBar ? `url(#round-corner-${barData.index})` : undefined}
|
||||
width={to(width, (value) => Math.max(value, 0))}
|
||||
height={to(height, (value) => Math.max(value, 0))}
|
||||
fill={color}
|
||||
strokeWidth={borderWidth}
|
||||
stroke={borderColor}
|
||||
focusable={isFocusable}
|
||||
tabIndex={isFocusable ? 0 : undefined}
|
||||
aria-label={ariaLabel ? ariaLabel(barData) : undefined}
|
||||
aria-labelledby={ariaLabelledBy ? ariaLabelledBy(barData) : undefined}
|
||||
aria-describedby={
|
||||
ariaDescribedBy ? ariaDescribedBy(barData) : undefined
|
||||
}
|
||||
aria-disabled={ariaDisabled ? ariaDisabled(barData) : undefined}
|
||||
aria-hidden={ariaHidden ? ariaHidden(barData) : undefined}
|
||||
onMouseEnter={isInteractive ? handleMouseEnter : undefined}
|
||||
onMouseMove={isInteractive ? handleTooltip : undefined}
|
||||
onMouseLeave={isInteractive ? handleMouseLeave : undefined}
|
||||
onClick={isInteractive ? handleClick : undefined}
|
||||
onFocus={isInteractive && isFocusable ? handleFocus : undefined}
|
||||
onBlur={isInteractive && isFocusable ? handleBlur : undefined}
|
||||
data-testid={`bar.item.${barData.id}.${barData.index}`}
|
||||
/>
|
||||
|
||||
{shouldRenderLabel && (
|
||||
<Text
|
||||
x={labelX}
|
||||
y={labelY}
|
||||
textAnchor={textAnchor}
|
||||
dominantBaseline="central"
|
||||
fillOpacity={labelOpacity}
|
||||
style={{
|
||||
...theme.labels.text,
|
||||
// We don't want the label to intercept mouse events
|
||||
pointerEvents: 'none',
|
||||
fill: labelColor,
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
)}
|
||||
</animated.g>
|
||||
);
|
||||
};
|
||||
+20
-26
@@ -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 { BarChartEndLines } from '@/page-layout/widgets/graph/graphWidgetBarChart/components/BarChartEndLines';
|
||||
import { CustomBarItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/components/CustomBarItem';
|
||||
import { BAR_CHART_MARGINS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartMargins';
|
||||
import { useBarChartData } from '@/page-layout/widgets/graph/graphWidgetBarChart/hooks/useBarChartData';
|
||||
import { useBarChartHandlers } from '@/page-layout/widgets/graph/graphWidgetBarChart/hooks/useBarChartHandlers';
|
||||
@@ -19,8 +19,8 @@ import {
|
||||
import { NodeDimensionEffect } from '@/ui/utilities/dimensions/components/NodeDimensionEffect';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { ResponsiveBar, type BarCustomLayerProps } from '@nivo/bar';
|
||||
import { useId, useRef, useState } from 'react';
|
||||
import { ResponsiveBar } from '@nivo/bar';
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const LEGEND_THRESHOLD = 10;
|
||||
@@ -76,7 +76,6 @@ export const GraphWidgetBarChart = ({
|
||||
customFormatter,
|
||||
}: GraphWidgetBarChartProps) => {
|
||||
const theme = useTheme();
|
||||
const instanceId = useId();
|
||||
const colorRegistry = createGraphColorRegistry(theme);
|
||||
const [chartWidth, setChartWidth] = useState<number>(0);
|
||||
const [chartHeight, setChartHeight] = useState<number>(0);
|
||||
@@ -98,17 +97,13 @@ export const GraphWidgetBarChart = ({
|
||||
|
||||
const chartTheme = useBarChartTheme();
|
||||
|
||||
const { barConfigs, enrichedKeys, enrichedKeysMap, defs } = useBarChartData({
|
||||
const { barConfigs, enrichedKeys } = useBarChartData({
|
||||
data,
|
||||
indexBy,
|
||||
keys,
|
||||
series,
|
||||
colorRegistry,
|
||||
id,
|
||||
instanceId,
|
||||
seriesLabels,
|
||||
hoveredBar,
|
||||
layout,
|
||||
});
|
||||
|
||||
const { renderTooltip: getTooltipData } = useBarChartTooltip({
|
||||
@@ -152,22 +147,27 @@ export const GraphWidgetBarChart = ({
|
||||
);
|
||||
};
|
||||
|
||||
const barEndLinesLayer = (props: BarCustomLayerProps<BarChartDataItem>) => {
|
||||
return (
|
||||
<BarChartEndLines
|
||||
bars={props.bars}
|
||||
enrichedKeysMap={enrichedKeysMap}
|
||||
const BarItemWithContext = useMemo(
|
||||
() => (props: any) => (
|
||||
<CustomBarItem
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
keys={keys}
|
||||
groupMode={groupMode}
|
||||
data={data}
|
||||
indexBy={indexBy}
|
||||
layout={layout}
|
||||
/>
|
||||
);
|
||||
};
|
||||
),
|
||||
[keys, groupMode, data, indexBy, layout],
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledContainer id={id}>
|
||||
<GraphWidgetChartContainer
|
||||
ref={containerRef}
|
||||
$isClickable={hasClickableItems}
|
||||
$cursorSelector='svg g[transform] rect[fill^="url(#gradient-"]'
|
||||
$cursorSelector="svg g[transform] rect[fill]"
|
||||
>
|
||||
<NodeDimensionEffect
|
||||
elementRef={containerRef}
|
||||
@@ -177,6 +177,7 @@ export const GraphWidgetBarChart = ({
|
||||
}}
|
||||
/>
|
||||
<ResponsiveBar
|
||||
barComponent={BarItemWithContext}
|
||||
data={data}
|
||||
keys={keys}
|
||||
indexBy={indexBy}
|
||||
@@ -192,15 +193,7 @@ export const GraphWidgetBarChart = ({
|
||||
}}
|
||||
indexScale={{ type: 'band', round: true }}
|
||||
colors={(datum) => getBarChartColor(datum, barConfigs, theme)}
|
||||
defs={defs}
|
||||
layers={[
|
||||
'grid',
|
||||
'axes',
|
||||
'bars',
|
||||
barEndLinesLayer,
|
||||
'markers',
|
||||
'legends',
|
||||
]}
|
||||
layers={['grid', 'axes', 'bars', 'markers', 'legends']}
|
||||
axisTop={null}
|
||||
axisRight={null}
|
||||
axisBottom={axisBottomConfig}
|
||||
@@ -226,6 +219,7 @@ export const GraphWidgetBarChart = ({
|
||||
}}
|
||||
onMouseLeave={() => setHoveredBar(null)}
|
||||
theme={chartTheme}
|
||||
borderRadius={parseInt(theme.border.radius.sm)}
|
||||
/>
|
||||
</GraphWidgetChartContainer>
|
||||
<GraphWidgetLegend
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
export const BAR_CHART_HOVER_BRIGHTNESS = 0.85;
|
||||
+25
-133
@@ -47,10 +47,6 @@ describe('useBarChartData', () => {
|
||||
keys: ['sales', 'costs'],
|
||||
series: mockSeries,
|
||||
colorRegistry: mockColorRegistry,
|
||||
id: 'test-chart',
|
||||
instanceId: 'instance-1',
|
||||
hoveredBar: null,
|
||||
layout: 'vertical',
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -66,10 +62,6 @@ describe('useBarChartData', () => {
|
||||
keys: ['sales', 'costs'],
|
||||
series: mockSeries,
|
||||
colorRegistry: mockColorRegistry,
|
||||
id: 'test-chart',
|
||||
instanceId: 'instance-1',
|
||||
hoveredBar: null,
|
||||
layout: 'vertical',
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -77,14 +69,18 @@ describe('useBarChartData', () => {
|
||||
expect(result.current.barConfigs[0]).toMatchObject({
|
||||
key: 'sales',
|
||||
indexValue: 'Jan',
|
||||
gradientId: 'gradient-test-chart-instance-1-sales-0-0',
|
||||
colorScheme: mockColorRegistry.green,
|
||||
colorScheme: {
|
||||
name: 'green',
|
||||
gradient: mockColorRegistry.green.gradient,
|
||||
},
|
||||
});
|
||||
expect(result.current.barConfigs[1]).toMatchObject({
|
||||
key: 'costs',
|
||||
indexValue: 'Jan',
|
||||
gradientId: 'gradient-test-chart-instance-1-costs-0-1',
|
||||
colorScheme: mockColorRegistry.purple,
|
||||
colorScheme: {
|
||||
name: 'purple',
|
||||
gradient: mockColorRegistry.purple.gradient,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -96,25 +92,28 @@ describe('useBarChartData', () => {
|
||||
keys: ['sales', 'costs'],
|
||||
series: mockSeries,
|
||||
colorRegistry: mockColorRegistry,
|
||||
id: 'test-chart',
|
||||
instanceId: 'instance-1',
|
||||
hoveredBar: null,
|
||||
layout: 'vertical',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current.enrichedKeys).toEqual([
|
||||
{
|
||||
key: 'sales',
|
||||
colorScheme: mockColorRegistry.green,
|
||||
label: 'Sales',
|
||||
expect(result.current.enrichedKeys).toHaveLength(2);
|
||||
expect(result.current.enrichedKeys[0]).toMatchObject({
|
||||
key: 'sales',
|
||||
label: 'Sales',
|
||||
colorScheme: {
|
||||
name: 'green',
|
||||
gradient: mockColorRegistry.green.gradient,
|
||||
},
|
||||
{
|
||||
key: 'costs',
|
||||
colorScheme: mockColorRegistry.purple,
|
||||
label: 'Costs',
|
||||
});
|
||||
expect(result.current.enrichedKeys[0].colorScheme.solid).toBeDefined();
|
||||
expect(result.current.enrichedKeys[1]).toMatchObject({
|
||||
key: 'costs',
|
||||
label: 'Costs',
|
||||
colorScheme: {
|
||||
name: 'purple',
|
||||
gradient: mockColorRegistry.purple.gradient,
|
||||
},
|
||||
]);
|
||||
});
|
||||
expect(result.current.enrichedKeys[1].colorScheme.solid).toBeDefined();
|
||||
});
|
||||
|
||||
it('should use series labels when series config is not provided', () => {
|
||||
@@ -125,11 +124,7 @@ describe('useBarChartData', () => {
|
||||
keys: ['sales', 'costs'],
|
||||
series: undefined,
|
||||
colorRegistry: mockColorRegistry,
|
||||
id: 'test-chart',
|
||||
instanceId: 'instance-1',
|
||||
seriesLabels: { sales: 'Revenue', costs: 'Expenses' },
|
||||
hoveredBar: null,
|
||||
layout: 'vertical',
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -137,70 +132,6 @@ describe('useBarChartData', () => {
|
||||
expect(result.current.enrichedKeys[1].label).toBe('Expenses');
|
||||
});
|
||||
|
||||
it('should handle hover state for bars', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useBarChartData({
|
||||
data: mockData,
|
||||
indexBy: 'month',
|
||||
keys: ['sales', 'costs'],
|
||||
series: mockSeries,
|
||||
colorRegistry: mockColorRegistry,
|
||||
id: 'test-chart',
|
||||
instanceId: 'instance-1',
|
||||
hoveredBar: { key: 'sales', indexValue: 'Feb' },
|
||||
layout: 'vertical',
|
||||
}),
|
||||
);
|
||||
|
||||
const hoveredDef = result.current.defs.find(
|
||||
(def) => def.id === 'gradient-test-chart-instance-1-sales-1-0',
|
||||
);
|
||||
expect(hoveredDef?.colors).toEqual([
|
||||
{ offset: 0, color: 'green4' },
|
||||
{ offset: 100, color: 'green3' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should generate vertical gradients for vertical layout', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useBarChartData({
|
||||
data: mockData,
|
||||
indexBy: 'month',
|
||||
keys: ['sales'],
|
||||
series: mockSeries,
|
||||
colorRegistry: mockColorRegistry,
|
||||
id: 'test-chart',
|
||||
instanceId: 'instance-1',
|
||||
hoveredBar: null,
|
||||
layout: 'vertical',
|
||||
}),
|
||||
);
|
||||
|
||||
const def = result.current.defs[0];
|
||||
expect(def.y1).toBe('0%');
|
||||
expect(def.y2).toBe('100%');
|
||||
});
|
||||
|
||||
it('should generate horizontal gradients for horizontal layout', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useBarChartData({
|
||||
data: mockData,
|
||||
indexBy: 'month',
|
||||
keys: ['sales'],
|
||||
series: mockSeries,
|
||||
colorRegistry: mockColorRegistry,
|
||||
id: 'test-chart',
|
||||
instanceId: 'instance-1',
|
||||
hoveredBar: null,
|
||||
layout: 'horizontal',
|
||||
}),
|
||||
);
|
||||
|
||||
const def = result.current.defs[0];
|
||||
expect(def.x1).toBe('0%');
|
||||
expect(def.x2).toBe('100%');
|
||||
});
|
||||
|
||||
it('should handle empty data', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useBarChartData({
|
||||
@@ -209,15 +140,10 @@ describe('useBarChartData', () => {
|
||||
keys: ['sales', 'costs'],
|
||||
series: mockSeries,
|
||||
colorRegistry: mockColorRegistry,
|
||||
id: 'test-chart',
|
||||
instanceId: 'instance-1',
|
||||
hoveredBar: null,
|
||||
layout: 'vertical',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current.barConfigs).toEqual([]);
|
||||
expect(result.current.defs).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle empty keys', () => {
|
||||
@@ -228,16 +154,11 @@ describe('useBarChartData', () => {
|
||||
keys: [],
|
||||
series: mockSeries,
|
||||
colorRegistry: mockColorRegistry,
|
||||
id: 'test-chart',
|
||||
instanceId: 'instance-1',
|
||||
hoveredBar: null,
|
||||
layout: 'vertical',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current.barConfigs).toEqual([]);
|
||||
expect(result.current.enrichedKeys).toEqual([]);
|
||||
expect(result.current.defs).toEqual([]);
|
||||
});
|
||||
|
||||
it('should fall back to key name when no label is provided', () => {
|
||||
@@ -248,40 +169,11 @@ describe('useBarChartData', () => {
|
||||
keys: ['sales', 'costs'],
|
||||
series: undefined,
|
||||
colorRegistry: mockColorRegistry,
|
||||
id: 'test-chart',
|
||||
instanceId: 'instance-1',
|
||||
seriesLabels: undefined,
|
||||
hoveredBar: null,
|
||||
layout: 'vertical',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current.enrichedKeys[0].label).toBe('sales');
|
||||
expect(result.current.enrichedKeys[1].label).toBe('costs');
|
||||
});
|
||||
|
||||
it('should recalculate when instanceId changes', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ instanceId }) =>
|
||||
useBarChartData({
|
||||
data: mockData,
|
||||
indexBy: 'month',
|
||||
keys: ['sales', 'costs'],
|
||||
series: mockSeries,
|
||||
colorRegistry: mockColorRegistry,
|
||||
id: 'test-chart',
|
||||
instanceId,
|
||||
hoveredBar: null,
|
||||
layout: 'vertical',
|
||||
}),
|
||||
{ initialProps: { instanceId: 'instance-1' } },
|
||||
);
|
||||
|
||||
const firstBarConfigs = result.current.barConfigs;
|
||||
|
||||
rerender({ instanceId: 'instance-2' });
|
||||
|
||||
expect(result.current.barConfigs).not.toBe(firstBarConfigs);
|
||||
expect(result.current.barConfigs[0].gradientId).toContain('instance-2');
|
||||
});
|
||||
});
|
||||
|
||||
+16
-43
@@ -3,7 +3,6 @@ import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBa
|
||||
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 { createGradientDef } from '@/page-layout/widgets/graph/utils/createGradientDef';
|
||||
import { getColorScheme } from '@/page-layout/widgets/graph/utils/getColorScheme';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
@@ -13,11 +12,8 @@ type UseBarChartDataProps = {
|
||||
keys: string[];
|
||||
series?: BarChartSeries[];
|
||||
colorRegistry: GraphColorRegistry;
|
||||
id: string;
|
||||
instanceId: string;
|
||||
seriesLabels?: Record<string, string>;
|
||||
hoveredBar: { key: string; indexValue: string | number } | null;
|
||||
layout: 'vertical' | 'horizontal';
|
||||
groupMode?: 'grouped' | 'stacked';
|
||||
};
|
||||
|
||||
export const useBarChartData = ({
|
||||
@@ -26,11 +22,7 @@ export const useBarChartData = ({
|
||||
keys,
|
||||
series,
|
||||
colorRegistry,
|
||||
id,
|
||||
instanceId,
|
||||
seriesLabels,
|
||||
hoveredBar,
|
||||
layout,
|
||||
}: UseBarChartDataProps) => {
|
||||
const seriesConfigMap = useMemo(
|
||||
() => new Map<string, BarChartSeries>(series?.map((s) => [s.key, s]) || []),
|
||||
@@ -38,35 +30,35 @@ export const useBarChartData = ({
|
||||
);
|
||||
|
||||
const barConfigs = useMemo((): BarChartConfig[] => {
|
||||
return data.flatMap((dataPoint, dataIndex) => {
|
||||
return data.flatMap((dataPoint) => {
|
||||
const indexValue = dataPoint[indexBy];
|
||||
return keys.map((key, keyIndex): BarChartConfig => {
|
||||
const seriesConfig = seriesConfigMap.get(key);
|
||||
const colorScheme = getColorScheme(
|
||||
colorRegistry,
|
||||
seriesConfig?.color,
|
||||
keyIndex,
|
||||
);
|
||||
const sanitizedKey = key.replace(/\s+/g, '-');
|
||||
const gradientId = `gradient-${id}-${instanceId}-${sanitizedKey}-${dataIndex}-${keyIndex}`;
|
||||
const colorScheme = getColorScheme({
|
||||
registry: colorRegistry,
|
||||
colorName: seriesConfig?.color,
|
||||
fallbackIndex: keyIndex,
|
||||
totalGroups: keys.length,
|
||||
});
|
||||
|
||||
return {
|
||||
key,
|
||||
indexValue,
|
||||
gradientId,
|
||||
colorScheme,
|
||||
};
|
||||
});
|
||||
});
|
||||
}, [data, indexBy, keys, colorRegistry, id, instanceId, seriesConfigMap]);
|
||||
}, [data, indexBy, keys, colorRegistry, seriesConfigMap]);
|
||||
|
||||
const enrichedKeys: BarChartEnrichedKey[] = keys.map((key, index) => {
|
||||
const seriesConfig = seriesConfigMap.get(key);
|
||||
const colorScheme = getColorScheme(
|
||||
colorRegistry,
|
||||
seriesConfig?.color,
|
||||
index,
|
||||
);
|
||||
const colorScheme = getColorScheme({
|
||||
registry: colorRegistry,
|
||||
colorName: seriesConfig?.color,
|
||||
fallbackIndex: index,
|
||||
totalGroups: keys.length,
|
||||
});
|
||||
|
||||
return {
|
||||
key,
|
||||
colorScheme,
|
||||
@@ -74,28 +66,9 @@ export const useBarChartData = ({
|
||||
};
|
||||
});
|
||||
|
||||
const enrichedKeysMap = useMemo(
|
||||
() => new Map(enrichedKeys.map((item) => [item.key, item])),
|
||||
[enrichedKeys],
|
||||
);
|
||||
|
||||
const defs = barConfigs.map((bar) => {
|
||||
const isHovered =
|
||||
hoveredBar?.key === bar.key && hoveredBar?.indexValue === bar.indexValue;
|
||||
return createGradientDef(
|
||||
bar.colorScheme,
|
||||
bar.gradientId,
|
||||
isHovered,
|
||||
layout === 'horizontal' ? 0 : 90,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
seriesConfigMap,
|
||||
barConfigs,
|
||||
enrichedKeys,
|
||||
enrichedKeysMap,
|
||||
defs,
|
||||
};
|
||||
};
|
||||
|
||||
-1
@@ -3,6 +3,5 @@ import { type GraphColorScheme } from '@/page-layout/widgets/graph/types/GraphCo
|
||||
export type BarChartConfig = {
|
||||
key: string;
|
||||
indexValue: string | number;
|
||||
gradientId: string;
|
||||
colorScheme: GraphColorScheme;
|
||||
};
|
||||
|
||||
+1
-1
@@ -14,5 +14,5 @@ export const getBarChartColor = (
|
||||
if (!isDefined(bar)) {
|
||||
return theme.border.color.light;
|
||||
}
|
||||
return `url(#${bar.gradientId})`;
|
||||
return bar.colorScheme.solid;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user