update chart tooltip designs (#15426)
closes https://discord.com/channels/1130383047699738754/1430845815634657320 ~~TODO: add truncation~~
This commit is contained in:
+103
-29
@@ -3,23 +3,36 @@ import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { IconArrowUpRight } from 'twenty-ui/display';
|
||||
|
||||
const StyledTooltipContent = styled.div`
|
||||
const StyledTooltip = styled.div`
|
||||
background: ${({ theme }) => theme.background.primary};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
box-shadow: ${({ theme }) => theme.boxShadow.strong};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(3)};
|
||||
padding: ${({ theme }) => theme.spacing(3)};
|
||||
gap: 2px;
|
||||
max-width: min(300px, calc(100vw - 40px));
|
||||
min-width: 160px;
|
||||
pointer-events: none;
|
||||
`;
|
||||
|
||||
const StyledTooltipContent = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(3)};
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledTooltipRow = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.extraLight};
|
||||
display: flex;
|
||||
font-size: ${({ theme }) => theme.font.size.xs};
|
||||
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledTooltipRowContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
@@ -31,22 +44,68 @@ const StyledDot = styled.div<{ $color: string }>`
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
const StyledTooltipValue = styled.span`
|
||||
margin-left: auto;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const StyledTooltipLink = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
cursor: default;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
height: ${({ theme }) => theme.spacing(6)};
|
||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||
padding-inline: ${({ theme }) => theme.spacing(2)};
|
||||
line-height: 140%;
|
||||
`;
|
||||
|
||||
const StyledTooltipSeparator = styled.div`
|
||||
background-color: ${({ theme }) => theme.border.color.light};
|
||||
min-height: 1px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledTooltipHeader = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
font-size: ${({ theme }) => theme.font.size.xs};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
line-height: 140%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const StyledTooltipRowRightContent = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: ${({ theme }) => theme.font.size.xs};
|
||||
color: ${({ theme }) => theme.font.color.extraLight};
|
||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledTooltipLabel = styled.span`
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const StyledTooltipValue = styled.span`
|
||||
flex-shrink: 0;
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const StyledHorizontalSectionPadding = styled.div<{
|
||||
$addTop?: boolean;
|
||||
$addBottom?: boolean;
|
||||
}>`
|
||||
padding-inline: ${({ theme }) => theme.spacing(1)};
|
||||
margin-top: ${({ $addTop, theme }) => ($addTop ? theme.spacing(1) : 0)};
|
||||
margin-bottom: ${({ $addBottom, theme }) =>
|
||||
$addBottom ? theme.spacing(1) : 0};
|
||||
`;
|
||||
|
||||
export type GraphWidgetTooltipItem = {
|
||||
@@ -58,32 +117,47 @@ export type GraphWidgetTooltipItem = {
|
||||
type GraphWidgetTooltipProps = {
|
||||
items: GraphWidgetTooltipItem[];
|
||||
showClickHint?: boolean;
|
||||
title?: string;
|
||||
indexLabel?: string;
|
||||
};
|
||||
|
||||
export const GraphWidgetTooltip = ({
|
||||
items,
|
||||
showClickHint = false,
|
||||
title,
|
||||
indexLabel,
|
||||
}: GraphWidgetTooltipProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<StyledTooltipContent>
|
||||
{title && <StyledTooltipHeader>{title}</StyledTooltipHeader>}
|
||||
{items.map((item, index) => (
|
||||
<StyledTooltipRow key={index}>
|
||||
<StyledDot $color={item.dotColor} />
|
||||
<span>{item.label}</span>
|
||||
<StyledTooltipValue>{item.formattedValue}</StyledTooltipValue>
|
||||
</StyledTooltipRow>
|
||||
))}
|
||||
<StyledTooltip>
|
||||
<StyledHorizontalSectionPadding $addTop $addBottom={!showClickHint}>
|
||||
<StyledTooltipContent>
|
||||
{indexLabel && (
|
||||
<StyledTooltipHeader>{indexLabel}</StyledTooltipHeader>
|
||||
)}
|
||||
<StyledTooltipRowContainer>
|
||||
{items.map((item, index) => (
|
||||
<StyledTooltipRow key={index}>
|
||||
<StyledDot $color={item.dotColor} />
|
||||
<StyledTooltipRowRightContent>
|
||||
<StyledTooltipLabel>{item.label}</StyledTooltipLabel>
|
||||
<StyledTooltipValue>{item.formattedValue}</StyledTooltipValue>
|
||||
</StyledTooltipRowRightContent>
|
||||
</StyledTooltipRow>
|
||||
))}
|
||||
</StyledTooltipRowContainer>
|
||||
</StyledTooltipContent>
|
||||
</StyledHorizontalSectionPadding>
|
||||
{showClickHint && (
|
||||
<StyledTooltipLink>
|
||||
<span>{t`Click to see data`}</span>
|
||||
<IconArrowUpRight size={theme.icon.size.sm} />
|
||||
</StyledTooltipLink>
|
||||
<>
|
||||
<StyledTooltipSeparator />
|
||||
<StyledHorizontalSectionPadding $addBottom>
|
||||
<StyledTooltipLink>
|
||||
<span>{t`Click to see data`}</span>
|
||||
<IconArrowUpRight size={theme.icon.size.sm} />
|
||||
</StyledTooltipLink>
|
||||
</StyledHorizontalSectionPadding>
|
||||
</>
|
||||
)}
|
||||
</StyledTooltipContent>
|
||||
</StyledTooltip>
|
||||
);
|
||||
};
|
||||
|
||||
+120
@@ -253,6 +253,8 @@ export const Stacked: Story = {
|
||||
data={args.data}
|
||||
indexBy={args.indexBy}
|
||||
keys={args.keys}
|
||||
groupMode={args.groupMode}
|
||||
seriesLabels={args.seriesLabels}
|
||||
showLegend={args.showLegend}
|
||||
showGrid={args.showGrid}
|
||||
xAxisLabel={args.xAxisLabel}
|
||||
@@ -492,6 +494,124 @@ export const Currency: Story = {
|
||||
),
|
||||
};
|
||||
|
||||
export const GroupedWithAllBarsTooltip: Story = {
|
||||
args: {
|
||||
data: [
|
||||
{
|
||||
period: 'January',
|
||||
lastYear: 18000,
|
||||
thisYear: 19500,
|
||||
to: '/comparison/january',
|
||||
},
|
||||
{
|
||||
period: 'February',
|
||||
lastYear: 20000,
|
||||
thisYear: 20000,
|
||||
to: '/comparison/february',
|
||||
},
|
||||
{
|
||||
period: 'March',
|
||||
lastYear: 22000,
|
||||
thisYear: 24500,
|
||||
to: '/comparison/march',
|
||||
},
|
||||
{
|
||||
period: 'April',
|
||||
lastYear: 21000,
|
||||
thisYear: 23000,
|
||||
to: '/comparison/april',
|
||||
},
|
||||
],
|
||||
indexBy: 'period',
|
||||
keys: ['lastYear', 'thisYear'],
|
||||
seriesLabels: {
|
||||
lastYear: 'Last year',
|
||||
thisYear: 'This year',
|
||||
},
|
||||
displayType: 'shortNumber',
|
||||
showLegend: true,
|
||||
showGrid: true,
|
||||
xAxisLabel: 'Period',
|
||||
yAxisLabel: 'Revenue',
|
||||
groupMode: 'grouped',
|
||||
id: 'bar-chart-grouped-all-tooltip',
|
||||
enableGroupTooltip: true,
|
||||
},
|
||||
render: (args) => (
|
||||
<Container>
|
||||
<GraphWidgetBarChart
|
||||
data={args.data}
|
||||
indexBy={args.indexBy}
|
||||
keys={args.keys}
|
||||
seriesLabels={args.seriesLabels}
|
||||
displayType={args.displayType}
|
||||
showLegend={args.showLegend}
|
||||
showGrid={args.showGrid}
|
||||
xAxisLabel={args.xAxisLabel}
|
||||
yAxisLabel={args.yAxisLabel}
|
||||
groupMode={args.groupMode}
|
||||
id={args.id}
|
||||
enableGroupTooltip={args.enableGroupTooltip}
|
||||
/>
|
||||
</Container>
|
||||
),
|
||||
};
|
||||
|
||||
export const GroupedDefaultTooltip: Story = {
|
||||
args: {
|
||||
data: [
|
||||
{
|
||||
period: 'January',
|
||||
lastYear: 18000,
|
||||
thisYear: 19500,
|
||||
to: '/comparison/january',
|
||||
},
|
||||
{
|
||||
period: 'February',
|
||||
lastYear: 20000,
|
||||
thisYear: 20000,
|
||||
to: '/comparison/february',
|
||||
},
|
||||
{
|
||||
period: 'March',
|
||||
lastYear: 22000,
|
||||
thisYear: 24500,
|
||||
to: '/comparison/march',
|
||||
},
|
||||
],
|
||||
indexBy: 'period',
|
||||
keys: ['lastYear', 'thisYear'],
|
||||
seriesLabels: {
|
||||
lastYear: 'Last year',
|
||||
thisYear: 'This year',
|
||||
},
|
||||
displayType: 'shortNumber',
|
||||
showLegend: true,
|
||||
showGrid: true,
|
||||
xAxisLabel: 'Period',
|
||||
yAxisLabel: 'Revenue',
|
||||
groupMode: 'grouped',
|
||||
id: 'bar-chart-grouped-default',
|
||||
},
|
||||
render: (args) => (
|
||||
<Container>
|
||||
<GraphWidgetBarChart
|
||||
data={args.data}
|
||||
indexBy={args.indexBy}
|
||||
keys={args.keys}
|
||||
seriesLabels={args.seriesLabels}
|
||||
displayType={args.displayType}
|
||||
showLegend={args.showLegend}
|
||||
showGrid={args.showGrid}
|
||||
xAxisLabel={args.xAxisLabel}
|
||||
yAxisLabel={args.yAxisLabel}
|
||||
groupMode={args.groupMode}
|
||||
id={args.id}
|
||||
/>
|
||||
</Container>
|
||||
),
|
||||
};
|
||||
|
||||
export const Catalog: Story = {
|
||||
decorators: [CatalogDecorator],
|
||||
parameters: {
|
||||
|
||||
+29
-11
@@ -25,6 +25,7 @@ export const Default: Story = {
|
||||
},
|
||||
],
|
||||
showClickHint: false,
|
||||
indexLabel: 'March 09, 2024',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -45,21 +46,38 @@ export const MultipleItems: Story = {
|
||||
args: {
|
||||
items: [
|
||||
{
|
||||
label: 'Q1',
|
||||
formattedValue: '$12,345',
|
||||
label: 'Last year',
|
||||
formattedValue: '20k',
|
||||
dotColor: 'blue',
|
||||
},
|
||||
{
|
||||
label: 'Q2',
|
||||
formattedValue: '$23,456',
|
||||
dotColor: 'green',
|
||||
},
|
||||
{
|
||||
label: 'Q3',
|
||||
formattedValue: '$34,567',
|
||||
dotColor: 'red',
|
||||
label: 'This year',
|
||||
formattedValue: '20k',
|
||||
dotColor: 'purple',
|
||||
},
|
||||
],
|
||||
showClickHint: false,
|
||||
showClickHint: true,
|
||||
indexLabel: 'February 2',
|
||||
},
|
||||
};
|
||||
|
||||
export const SuperLongText: Story = {
|
||||
args: {
|
||||
items: [
|
||||
{
|
||||
label:
|
||||
'Total Annual Recurring Revenue (North America Region including Canada)',
|
||||
formattedValue: '$2,450,000',
|
||||
dotColor: 'blue',
|
||||
},
|
||||
{
|
||||
label: 'Customer Acquisition Cost (Marketing & Sales Combined)',
|
||||
formattedValue: '$125,500',
|
||||
dotColor: 'purple',
|
||||
},
|
||||
],
|
||||
showClickHint: true,
|
||||
indexLabel:
|
||||
'Q4 2024 Financial Year End (October - December) - North America Regional Performance Summary',
|
||||
},
|
||||
};
|
||||
|
||||
+8
-2
@@ -42,6 +42,7 @@ type GraphWidgetBarChartProps = {
|
||||
seriesLabels?: Record<string, string>;
|
||||
rangeMin?: number;
|
||||
rangeMax?: number;
|
||||
enableGroupTooltip?: boolean;
|
||||
} & GraphValueFormatOptions;
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
@@ -69,6 +70,7 @@ export const GraphWidgetBarChart = ({
|
||||
seriesLabels,
|
||||
rangeMin,
|
||||
rangeMax,
|
||||
enableGroupTooltip,
|
||||
displayType,
|
||||
decimals,
|
||||
prefix,
|
||||
@@ -81,6 +83,9 @@ export const GraphWidgetBarChart = ({
|
||||
const [chartHeight, setChartHeight] = useState<number>(0);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const shouldEnableGroupTooltip =
|
||||
enableGroupTooltip ?? groupMode === 'stacked';
|
||||
|
||||
const formatOptions: GraphValueFormatOptions = {
|
||||
displayType,
|
||||
decimals,
|
||||
@@ -112,6 +117,7 @@ export const GraphWidgetBarChart = ({
|
||||
data,
|
||||
indexBy,
|
||||
formatOptions,
|
||||
enableGroupTooltip: shouldEnableGroupTooltip,
|
||||
});
|
||||
|
||||
const isLargeChart = data.length * keys.length > LABEL_THRESHOLD;
|
||||
@@ -140,9 +146,9 @@ export const GraphWidgetBarChart = ({
|
||||
|
||||
return (
|
||||
<GraphWidgetTooltip
|
||||
items={[tooltipData.tooltipItem]}
|
||||
items={tooltipData.tooltipItems}
|
||||
showClickHint={tooltipData.showClickHint}
|
||||
title={tooltipData.title}
|
||||
indexLabel={tooltipData.indexLabel}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+27
-14
@@ -13,6 +13,7 @@ type UseBarChartTooltipProps = {
|
||||
data: BarChartDataItem[];
|
||||
indexBy: string;
|
||||
formatOptions: GraphValueFormatOptions;
|
||||
enableGroupTooltip?: boolean;
|
||||
};
|
||||
|
||||
export const useBarChartTooltip = ({
|
||||
@@ -21,26 +22,38 @@ export const useBarChartTooltip = ({
|
||||
data,
|
||||
indexBy,
|
||||
formatOptions,
|
||||
enableGroupTooltip = true,
|
||||
}: UseBarChartTooltipProps) => {
|
||||
const renderTooltip = (datum: ComputedDatum<BarDatum>) => {
|
||||
const hoveredKey = hoveredBar?.key;
|
||||
if (!isDefined(hoveredKey)) return null;
|
||||
|
||||
const enrichedKey = enrichedKeys.find((item) => item.key === hoveredKey);
|
||||
if (!enrichedKey) return null;
|
||||
|
||||
const dataItem = data.find((d) => d[indexBy] === datum.indexValue);
|
||||
const seriesValue = Number(datum.data[hoveredKey] || 0);
|
||||
const tooltipItem = {
|
||||
label: enrichedKey.label,
|
||||
formattedValue: formatGraphValue(seriesValue, formatOptions),
|
||||
dotColor: enrichedKey.colorScheme.solid,
|
||||
};
|
||||
|
||||
let keysToShow: BarChartEnrichedKey[];
|
||||
|
||||
if (enableGroupTooltip) {
|
||||
keysToShow = enrichedKeys;
|
||||
} else {
|
||||
const hoveredKey = hoveredBar?.key;
|
||||
if (!isDefined(hoveredKey)) return null;
|
||||
|
||||
const enrichedKey = enrichedKeys.find((item) => item.key === hoveredKey);
|
||||
if (!isDefined(enrichedKey)) return null;
|
||||
|
||||
keysToShow = [enrichedKey];
|
||||
}
|
||||
|
||||
const tooltipItems = keysToShow.map((enrichedKey) => {
|
||||
const seriesValue = Number(datum.data[enrichedKey.key] ?? 0);
|
||||
return {
|
||||
label: enrichedKey.label,
|
||||
formattedValue: formatGraphValue(seriesValue, formatOptions),
|
||||
dotColor: enrichedKey.colorScheme.solid,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
tooltipItem,
|
||||
tooltipItems,
|
||||
showClickHint: isDefined(dataItem?.to),
|
||||
title: String(datum.indexValue),
|
||||
indexLabel: String(datum.indexValue),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
+2
-2
@@ -144,7 +144,7 @@ export const GraphWidgetLineChart = ({
|
||||
<GraphWidgetTooltip
|
||||
items={tooltipData.items}
|
||||
showClickHint={tooltipData.showClickHint}
|
||||
title={tooltipData.title}
|
||||
indexLabel={tooltipData.indexLabel}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -158,7 +158,7 @@ export const GraphWidgetLineChart = ({
|
||||
<GraphWidgetTooltip
|
||||
items={tooltipData.items}
|
||||
showClickHint={tooltipData.showClickHint}
|
||||
title={tooltipData.title}
|
||||
indexLabel={tooltipData.indexLabel}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+3
-3
@@ -31,7 +31,7 @@ export const useLineChartTooltip = ({
|
||||
return {
|
||||
items: [],
|
||||
showClickHint: false,
|
||||
title: undefined,
|
||||
indexLabel: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ export const useLineChartTooltip = ({
|
||||
return {
|
||||
items: tooltipItems,
|
||||
showClickHint: hasClickablePoint,
|
||||
title: isDefined(xValue) ? String(xValue) : undefined,
|
||||
indexLabel: isDefined(xValue) ? String(xValue) : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -90,7 +90,7 @@ export const useLineChartTooltip = ({
|
||||
},
|
||||
],
|
||||
showClickHint: isDefined(dataPoint?.to),
|
||||
title: isDefined(point.data.x) ? String(point.data.x) : undefined,
|
||||
indexLabel: isDefined(point.data.x) ? String(point.data.x) : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user