Design adjustments on Bar chart (#15028)
- Make the ticks dynamic when we resize the graph - Fix formatting - Fix gradient color not working when the index have a space in it - Fix the maximum number of groups for a grouped by graph - Update the tooltip design and display the group Video: https://github.com/user-attachments/assets/9f304b6c-3dec-4ce2-9127-41d27f393d90 --------- Co-authored-by: Marie Stoppa <marie.stoppa@essec.edu>
This commit is contained in:
+40
-18
@@ -2,26 +2,30 @@ import { GraphWidgetChartContainer } from '@/page-layout/widgets/graph/component
|
||||
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 { 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';
|
||||
import { useBarChartTheme } from '@/page-layout/widgets/graph/graphWidgetBarChart/hooks/useBarChartTheme';
|
||||
import { useBarChartTooltip } from '@/page-layout/widgets/graph/graphWidgetBarChart/hooks/useBarChartTooltip';
|
||||
import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem';
|
||||
import { type BarChartSeries } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartSeries';
|
||||
import { getBarChartAxisBottomConfig } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartAxisBottomConfig';
|
||||
import { getBarChartAxisLeftConfig } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartAxisLeftConfig';
|
||||
import { getBarChartAxisConfigs } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartAxisConfigs';
|
||||
import { getBarChartColor } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartColor';
|
||||
import { createGraphColorRegistry } from '@/page-layout/widgets/graph/utils/createGraphColorRegistry';
|
||||
import {
|
||||
formatGraphValue,
|
||||
type GraphValueFormatOptions,
|
||||
} from '@/page-layout/widgets/graph/utils/graphFormatters';
|
||||
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 } from 'react';
|
||||
import { useId, useRef, useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const LEGEND_THRESHOLD = 10;
|
||||
const LABEL_THRESHOLD = 15;
|
||||
|
||||
type GraphWidgetBarChartProps = {
|
||||
data: BarChartDataItem[];
|
||||
indexBy: string;
|
||||
@@ -70,6 +74,9 @@ export const GraphWidgetBarChart = ({
|
||||
const theme = useTheme();
|
||||
const instanceId = useId();
|
||||
const colorRegistry = createGraphColorRegistry(theme);
|
||||
const [chartWidth, setChartWidth] = useState<number>(0);
|
||||
const [chartHeight, setChartHeight] = useState<number>(0);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const formatOptions: GraphValueFormatOptions = {
|
||||
displayType,
|
||||
@@ -108,19 +115,25 @@ export const GraphWidgetBarChart = ({
|
||||
formatOptions,
|
||||
});
|
||||
|
||||
const axisBottomConfig = getBarChartAxisBottomConfig(
|
||||
layout,
|
||||
xAxisLabel,
|
||||
yAxisLabel,
|
||||
formatOptions,
|
||||
);
|
||||
const isLargeChart = data.length * keys.length > LABEL_THRESHOLD;
|
||||
const areThereTooManyKeys = keys.length > LEGEND_THRESHOLD;
|
||||
|
||||
const axisLeftConfig = getBarChartAxisLeftConfig(
|
||||
layout,
|
||||
xAxisLabel,
|
||||
yAxisLabel,
|
||||
formatOptions,
|
||||
);
|
||||
const shouldShowLabels = showValues && !isLargeChart;
|
||||
|
||||
const shouldShowLegend = showLegend && !areThereTooManyKeys;
|
||||
|
||||
const { axisBottom: axisBottomConfig, axisLeft: axisLeftConfig } =
|
||||
getBarChartAxisConfigs({
|
||||
width: chartWidth,
|
||||
height: chartHeight,
|
||||
data,
|
||||
layout,
|
||||
indexBy,
|
||||
xAxisLabel,
|
||||
yAxisLabel,
|
||||
formatOptions,
|
||||
axisFontSize: chartTheme.axis.ticks.text.fontSize,
|
||||
});
|
||||
|
||||
const renderTooltip = (datum: Parameters<typeof getTooltipData>[0]) => {
|
||||
const tooltipData = getTooltipData(datum);
|
||||
@@ -130,6 +143,7 @@ export const GraphWidgetBarChart = ({
|
||||
<GraphWidgetTooltip
|
||||
items={[tooltipData.tooltipItem]}
|
||||
showClickHint={tooltipData.showClickHint}
|
||||
title={tooltipData.title}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -147,14 +161,22 @@ export const GraphWidgetBarChart = ({
|
||||
return (
|
||||
<StyledContainer id={id}>
|
||||
<GraphWidgetChartContainer
|
||||
ref={containerRef}
|
||||
$isClickable={hasClickableItems}
|
||||
$cursorSelector='svg g[transform] rect[fill^="url(#gradient-"]'
|
||||
>
|
||||
<NodeDimensionEffect
|
||||
elementRef={containerRef}
|
||||
onDimensionChange={({ width, height }) => {
|
||||
setChartWidth(width);
|
||||
setChartHeight(height);
|
||||
}}
|
||||
/>
|
||||
<ResponsiveBar
|
||||
data={data}
|
||||
keys={keys}
|
||||
indexBy={indexBy}
|
||||
margin={{ top: 20, right: 20, bottom: 60, left: 70 }}
|
||||
margin={BAR_CHART_MARGINS}
|
||||
padding={0.3}
|
||||
groupMode={groupMode}
|
||||
layout={layout}
|
||||
@@ -178,7 +200,7 @@ export const GraphWidgetBarChart = ({
|
||||
enableGridY={layout === 'vertical' && showGrid}
|
||||
gridXValues={layout === 'horizontal' ? 5 : undefined}
|
||||
gridYValues={layout === 'vertical' ? 5 : undefined}
|
||||
enableLabel={showValues}
|
||||
enableLabel={shouldShowLabels}
|
||||
labelSkipWidth={12}
|
||||
labelSkipHeight={12}
|
||||
labelTextColor={theme.font.color.primary}
|
||||
@@ -198,7 +220,7 @@ export const GraphWidgetBarChart = ({
|
||||
/>
|
||||
</GraphWidgetChartContainer>
|
||||
<GraphWidgetLegend
|
||||
show={showLegend}
|
||||
show={shouldShowLegend}
|
||||
items={enrichedKeys.map((item) => {
|
||||
const total = data.reduce(
|
||||
(sum, d) => sum + Number(d[item.key] || 0),
|
||||
|
||||
+1
@@ -54,6 +54,7 @@ export const GraphWidgetBarChartRenderer = ({
|
||||
yAxisLabel={yAxisLabel}
|
||||
showValues={showDataLabels}
|
||||
id={widget.id}
|
||||
displayType="shortNumber"
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
export const BAR_CHART_MARGINS = {
|
||||
top: 20,
|
||||
right: 20,
|
||||
bottom: 60,
|
||||
left: 70,
|
||||
} as const;
|
||||
+2
-1
@@ -47,7 +47,8 @@ export const useBarChartData = ({
|
||||
seriesConfig?.color,
|
||||
keyIndex,
|
||||
);
|
||||
const gradientId = `gradient-${id}-${instanceId}-${key}-${dataIndex}-${keyIndex}`;
|
||||
const sanitizedKey = key.replace(/\s+/g, '-');
|
||||
const gradientId = `gradient-${id}-${instanceId}-${sanitizedKey}-${dataIndex}-${keyIndex}`;
|
||||
|
||||
return {
|
||||
key,
|
||||
|
||||
+1
@@ -40,6 +40,7 @@ export const useBarChartTooltip = ({
|
||||
return {
|
||||
tooltipItem,
|
||||
showClickHint: isDefined(dataItem?.to),
|
||||
title: String(datum.indexValue),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import { BAR_CHART_MARGINS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartMargins';
|
||||
import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem';
|
||||
|
||||
const MINIMUM_WIDTH_PER_TICK = 100;
|
||||
|
||||
export const computeBarChartCategoryTickValues = ({
|
||||
width,
|
||||
data,
|
||||
indexBy,
|
||||
}: {
|
||||
width: number;
|
||||
data: BarChartDataItem[];
|
||||
indexBy: string;
|
||||
}): (string | number)[] => {
|
||||
if (width === 0 || data.length === 0) return [];
|
||||
|
||||
const horizontalMargins = BAR_CHART_MARGINS.left + BAR_CHART_MARGINS.right;
|
||||
const availableWidth = width - horizontalMargins;
|
||||
const numberOfTicks = Math.floor(availableWidth / MINIMUM_WIDTH_PER_TICK);
|
||||
|
||||
if (numberOfTicks <= 0) return [];
|
||||
if (numberOfTicks === 1) return [data[0][indexBy] as string | number];
|
||||
if (numberOfTicks >= data.length)
|
||||
return data.map((item) => item[indexBy] as string | number);
|
||||
|
||||
const step = (data.length - 1) / (numberOfTicks - 1);
|
||||
|
||||
return Array.from({ length: numberOfTicks }, (_, i) => {
|
||||
const index = Math.min(Math.round(i * step), data.length - 1);
|
||||
return data[index][indexBy] as string | number;
|
||||
});
|
||||
};
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
const MIN_TICK_SPACING_HEIGHT_RATIO = 2.5;
|
||||
|
||||
type ComputeBarChartValueTickCountProps = {
|
||||
height: number;
|
||||
axisFontSize: number;
|
||||
};
|
||||
|
||||
export const computeBarChartValueTickCount = ({
|
||||
height,
|
||||
axisFontSize,
|
||||
}: ComputeBarChartValueTickCountProps): number => {
|
||||
const minHeightPerTick = axisFontSize * MIN_TICK_SPACING_HEIGHT_RATIO;
|
||||
return Math.max(1, Math.floor(height / minHeightPerTick));
|
||||
};
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
import {
|
||||
formatGraphValue,
|
||||
type GraphValueFormatOptions,
|
||||
} from '@/page-layout/widgets/graph/utils/graphFormatters';
|
||||
|
||||
export const getBarChartAxisBottomConfig = (
|
||||
layout: 'vertical' | 'horizontal',
|
||||
xAxisLabel?: string,
|
||||
yAxisLabel?: string,
|
||||
formatOptions?: GraphValueFormatOptions,
|
||||
) => {
|
||||
return layout === 'vertical'
|
||||
? {
|
||||
tickSize: 0,
|
||||
tickPadding: 5,
|
||||
tickRotation: 0,
|
||||
legend: xAxisLabel,
|
||||
legendPosition: 'middle' as const,
|
||||
legendOffset: 40,
|
||||
}
|
||||
: {
|
||||
tickSize: 0,
|
||||
tickPadding: 5,
|
||||
tickRotation: 0,
|
||||
legend: yAxisLabel,
|
||||
legendPosition: 'middle' as const,
|
||||
legendOffset: 40,
|
||||
format: (value: number) => formatGraphValue(value, formatOptions || {}),
|
||||
};
|
||||
};
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
import { BAR_CHART_MARGINS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartMargins';
|
||||
import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem';
|
||||
import { computeBarChartCategoryTickValues } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/computeBarChartCategoryTickValues';
|
||||
import { computeBarChartValueTickCount } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/computeBarChartValueTickCount';
|
||||
import { truncateTickLabel } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/truncateTickLabel';
|
||||
import {
|
||||
formatGraphValue,
|
||||
type GraphValueFormatOptions,
|
||||
} from '@/page-layout/widgets/graph/utils/graphFormatters';
|
||||
|
||||
const AVERAGE_CHARACTER_WIDTH_RATIO = 0.6;
|
||||
const MIN_TICK_LABEL_LENGTH = 5;
|
||||
const MAX_LEFT_AXIS_LABEL_LENGTH = 20;
|
||||
|
||||
type GetBarChartAxisConfigsProps = {
|
||||
width: number;
|
||||
height: number;
|
||||
data: BarChartDataItem[];
|
||||
layout: 'vertical' | 'horizontal';
|
||||
indexBy: string;
|
||||
xAxisLabel?: string;
|
||||
yAxisLabel?: string;
|
||||
formatOptions?: GraphValueFormatOptions;
|
||||
axisFontSize?: number;
|
||||
};
|
||||
|
||||
export const getBarChartAxisConfigs = ({
|
||||
width,
|
||||
height,
|
||||
data,
|
||||
layout,
|
||||
indexBy,
|
||||
xAxisLabel,
|
||||
yAxisLabel,
|
||||
formatOptions,
|
||||
axisFontSize = 11,
|
||||
}: GetBarChartAxisConfigsProps) => {
|
||||
const categoryTickValues = computeBarChartCategoryTickValues({
|
||||
width,
|
||||
data,
|
||||
indexBy,
|
||||
});
|
||||
|
||||
const availableWidth =
|
||||
width - (BAR_CHART_MARGINS.left + BAR_CHART_MARGINS.right);
|
||||
const availableHeight =
|
||||
height - (BAR_CHART_MARGINS.top + BAR_CHART_MARGINS.bottom);
|
||||
const widthPerTick =
|
||||
categoryTickValues.length > 0
|
||||
? availableWidth / categoryTickValues.length
|
||||
: 0;
|
||||
const averageCharacterWidth = axisFontSize * AVERAGE_CHARACTER_WIDTH_RATIO;
|
||||
const maxLabelLength = Math.max(
|
||||
MIN_TICK_LABEL_LENGTH,
|
||||
Math.floor(widthPerTick / averageCharacterWidth),
|
||||
);
|
||||
|
||||
const numberOfValueTicks = computeBarChartValueTickCount({
|
||||
height: availableHeight,
|
||||
axisFontSize,
|
||||
});
|
||||
|
||||
if (layout === 'vertical') {
|
||||
return {
|
||||
axisBottom: {
|
||||
tickSize: 0,
|
||||
tickPadding: 5,
|
||||
tickRotation: 0,
|
||||
tickValues: categoryTickValues,
|
||||
legend: xAxisLabel,
|
||||
legendPosition: 'middle' as const,
|
||||
legendOffset: 40,
|
||||
format: (value: string | number) =>
|
||||
truncateTickLabel(String(value), maxLabelLength),
|
||||
},
|
||||
axisLeft: {
|
||||
tickSize: 0,
|
||||
tickPadding: 5,
|
||||
tickRotation: 0,
|
||||
tickValues: numberOfValueTicks,
|
||||
legend: yAxisLabel,
|
||||
legendPosition: 'middle' as const,
|
||||
legendOffset: -50,
|
||||
format: (value: number) => formatGraphValue(value, formatOptions || {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
axisBottom: {
|
||||
tickSize: 0,
|
||||
tickPadding: 5,
|
||||
tickRotation: 0,
|
||||
tickValues: numberOfValueTicks,
|
||||
legend: yAxisLabel,
|
||||
legendPosition: 'middle' as const,
|
||||
legendOffset: 40,
|
||||
format: (value: number) => formatGraphValue(value, formatOptions || {}),
|
||||
},
|
||||
axisLeft: {
|
||||
tickSize: 0,
|
||||
tickPadding: 5,
|
||||
tickRotation: 0,
|
||||
tickValues: categoryTickValues,
|
||||
legend: xAxisLabel,
|
||||
legendPosition: 'middle' as const,
|
||||
legendOffset: -50,
|
||||
format: (value: string | number) =>
|
||||
truncateTickLabel(String(value), MAX_LEFT_AXIS_LABEL_LENGTH),
|
||||
},
|
||||
};
|
||||
};
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
import {
|
||||
formatGraphValue,
|
||||
type GraphValueFormatOptions,
|
||||
} from '@/page-layout/widgets/graph/utils/graphFormatters';
|
||||
|
||||
export const getBarChartAxisLeftConfig = (
|
||||
layout: 'vertical' | 'horizontal',
|
||||
xAxisLabel?: string,
|
||||
yAxisLabel?: string,
|
||||
formatOptions?: GraphValueFormatOptions,
|
||||
) => {
|
||||
return layout === 'vertical'
|
||||
? {
|
||||
tickSize: 0,
|
||||
tickPadding: 5,
|
||||
tickRotation: 0,
|
||||
legend: yAxisLabel,
|
||||
legendPosition: 'middle' as const,
|
||||
legendOffset: -50,
|
||||
format: (value: number) => formatGraphValue(value, formatOptions || {}),
|
||||
}
|
||||
: {
|
||||
tickSize: 0,
|
||||
tickPadding: 5,
|
||||
tickRotation: 0,
|
||||
legend: xAxisLabel,
|
||||
legendPosition: 'middle' as const,
|
||||
legendOffset: -50,
|
||||
};
|
||||
};
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
export const truncateTickLabel = (
|
||||
value: string | number,
|
||||
maxLength: number,
|
||||
): string => {
|
||||
const stringValue = String(value);
|
||||
if (maxLength < 4 || stringValue.length <= maxLength) {
|
||||
return stringValue;
|
||||
}
|
||||
return `${stringValue.slice(0, maxLength - 3)}...`;
|
||||
};
|
||||
Reference in New Issue
Block a user