Skip to content

Commit 405c6e1

Browse files
committed
Improving labels further by leveraging css and following DRY
1 parent a24b4b6 commit 405c6e1

7 files changed

Lines changed: 55 additions & 251 deletions

File tree

js/packages/react-ui/src/components/Charts/AreaChart/AreaChart.tsx

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import { LabelTooltipProvider } from "../shared/LabelTooltip/LabelTooltip";
1919
import { LegendItem, XAxisTickVariant } from "../types";
2020
import {
2121
findNearestSnapPosition,
22-
getOptimalXAxisTickFormatter,
2322
getSnapPositions,
2423
getWidthOfData,
2524
getWidthOfGroup,
@@ -138,11 +137,6 @@ const AreaChartComponent = <T extends AreaChartData>({
138137
return height ?? 296 + maxLabelHeight;
139138
}, [height, maxLabelHeight]);
140139

141-
// Calculate optimal tick formatter for collision detection and truncation
142-
const xAxisTickFormatter = useMemo(() => {
143-
return getOptimalXAxisTickFormatter(data, effectiveContainerWidth);
144-
}, [data, effectiveContainerWidth]);
145-
146140
// Check scroll boundaries
147141
const updateScrollState = useCallback(() => {
148142
if (mainContainerRef.current) {
@@ -337,7 +331,6 @@ const AreaChartComponent = <T extends AreaChartData>({
337331
axisLine={false}
338332
textAnchor="middle"
339333
interval={0}
340-
tickFormatter={xAxisTickFormatter}
341334
height={maxLabelHeight}
342335
tick={
343336
<XAxisTick

js/packages/react-ui/src/components/Charts/BarChart/BarChart.tsx

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ import { BarChartData, BarChartVariant } from "./types";
3232
import {
3333
BAR_WIDTH,
3434
findNearestSnapPosition,
35-
getOptimalXAxisTickFormatter,
3635
getPadding,
3736
getRadiusArray,
3837
getSnapPositions,
@@ -247,11 +246,6 @@ const BarChartComponent = <T extends BarChartData>({
247246

248247
const id = useId();
249248

250-
// Get the optimal X-axis tick formatter based on available space
251-
const xAxisTickFormatter = useMemo(() => {
252-
return getOptimalXAxisTickFormatter(data, categoryKey as string, variant);
253-
}, [data, categoryKey, variant]);
254-
255249
const yAxis = useMemo(() => {
256250
if (!showYAxis) {
257251
return null;
@@ -379,7 +373,6 @@ const BarChartComponent = <T extends BarChartData>({
379373
tickLine={false}
380374
axisLine={false}
381375
textAnchor={"middle"}
382-
tickFormatter={xAxisTickFormatter}
383376
interval={0}
384377
height={maxLabelHeight}
385378
tick={

js/packages/react-ui/src/components/Charts/BarChart/utils/BarChartUtils.ts

Lines changed: 0 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { getDataKeys } from "../../utils/dataUtils";
2-
import { getCanvasContext } from "../../utils/styleUtils";
32
import { BarChartVariant } from "../types";
43

54
export const BAR_WIDTH = 16;
@@ -115,76 +114,6 @@ const getRadiusArray = (
115114
return [radius, radius, radius, radius];
116115
};
117116

118-
/**
119-
* INTERNAL HELPER FUNCTION
120-
* This function returns the formatter for the X-axis tick values with intelligent truncation.
121-
* @param groupWidth - The width available for each group/category (optional)
122-
* @param variant - The chart variant (affects truncation logic)
123-
* @returns The formatter for the X-axis tick values.
124-
* internally used by the XAxis component reCharts
125-
*/
126-
const getXAxisTickFormatter = (groupWidth?: number, variant: BarChartVariant = "grouped") => {
127-
const PADDING = 2; // Safety padding for better visual spacing
128-
const context = getCanvasContext();
129-
130-
return (value: string) => {
131-
const stringValue = String(value);
132-
133-
// Fallback for SSR, or if canvas/groupWidth is not available.
134-
if (!context || !groupWidth) {
135-
if (variant === "stacked") {
136-
return stringValue.slice(0, 3);
137-
} else {
138-
return stringValue.length > 3 ? `${stringValue.slice(0, 3)}...` : stringValue;
139-
}
140-
}
141-
142-
const availableWidth = Math.max(0, groupWidth - PADDING);
143-
144-
if (context.measureText(stringValue).width <= availableWidth) {
145-
return stringValue; // Full text fits.
146-
}
147-
148-
// If text overflows, find the best truncation point with ellipsis.
149-
let low = 0;
150-
let high = stringValue.length;
151-
let result = "";
152-
while (low <= high) {
153-
const mid = Math.floor((low + high) / 2);
154-
if (mid === 0) {
155-
low = mid + 1;
156-
continue;
157-
}
158-
const truncated = stringValue.substring(0, mid) + "...";
159-
if (context.measureText(truncated).width <= availableWidth) {
160-
result = truncated;
161-
low = mid + 1;
162-
} else {
163-
high = mid - 1;
164-
}
165-
}
166-
167-
return result;
168-
};
169-
};
170-
171-
/**
172-
* Helper function to get the optimal X-axis tick formatter with calculated group width
173-
* @param data - The chart data
174-
* @param categoryKey - The category key
175-
* @param variant - The chart variant
176-
* @returns The optimized formatter function
177-
*/
178-
const getOptimalXAxisTickFormatter = (
179-
data: Array<Record<string, string | number>>,
180-
categoryKey: string,
181-
variant: BarChartVariant,
182-
) => {
183-
// Calculate the available width per group
184-
const groupWidth = getWidthOfGroup(data, categoryKey, variant);
185-
return getXAxisTickFormatter(groupWidth, variant);
186-
};
187-
188117
/**
189118
* This function returns the scroll amount for the chart, used for the scroll amount of the chart.
190119
* This can also be used to calculate the width of each group/category.
@@ -287,7 +216,6 @@ const getChartHeight = (containerWidth: number): number => {
287216
export {
288217
findNearestSnapPosition,
289218
getChartHeight,
290-
getOptimalXAxisTickFormatter,
291219
getPadding,
292220
getRadiusArray,
293221
getSnapPositions,

js/packages/react-ui/src/components/Charts/LineChart/LineChart.tsx

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import { LabelTooltipProvider } from "../shared/LabelTooltip/LabelTooltip";
1919
import { LegendItem, XAxisTickVariant } from "../types";
2020
import {
2121
findNearestSnapPosition,
22-
getOptimalXAxisTickFormatter,
2322
getSnapPositions,
2423
getWidthOfData,
2524
getWidthOfGroup,
@@ -138,11 +137,6 @@ export const LineChart = <T extends LineChartData>({
138137
return height ?? 296 + maxLabelHeight;
139138
}, [height, maxLabelHeight]);
140139

141-
// Calculate optimal tick formatter for collision detection and truncation
142-
const xAxisTickFormatter = useMemo(() => {
143-
return getOptimalXAxisTickFormatter(data, effectiveContainerWidth);
144-
}, [data, effectiveContainerWidth]);
145-
146140
// Check scroll boundaries
147141
const updateScrollState = useCallback(() => {
148142
if (mainContainerRef.current) {
@@ -347,7 +341,6 @@ export const LineChart = <T extends LineChartData>({
347341
height={maxLabelHeight}
348342
textAnchor="middle"
349343
interval={0}
350-
tickFormatter={xAxisTickFormatter}
351344
tick={
352345
<XAxisTick
353346
variant={tickVariant}

js/packages/react-ui/src/components/Charts/shared/XAxisTick/XAxisTick.tsx

Lines changed: 43 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -24,94 +24,78 @@ interface XAxisTickProps {
2424
tickFormatter?: (value: any) => string;
2525
index?: number;
2626
visibleTicksCount?: number;
27-
variant?: XAxisTickVariant;
28-
widthOfGroup?: number;
29-
labelHeight?: number;
27+
variant: XAxisTickVariant;
28+
widthOfGroup: number;
29+
labelHeight: number;
3030
}
3131

3232
const XAxisTick = React.forwardRef<SVGGElement, XAxisTickProps>((props, ref) => {
3333
const {
3434
x,
3535
y,
3636
payload,
37-
tickFormatter,
3837
className,
3938
variant = "multiLine",
4039
widthOfGroup = 70,
4140
labelHeight = 20,
4241
} = props;
4342

44-
const value = String(payload?.value || "");
43+
const rawValue = payload?.value;
44+
const value = String(rawValue || "");
4545

4646
const foreignObjectRef = useRef<SVGForeignObjectElement>(null);
4747
const spanRef = useRef<HTMLSpanElement>(null);
48-
const [isMultiLineTruncated, setIsMultiLineTruncated] = useState(false);
48+
const [isTruncated, setIsTruncated] = useState(false);
4949

50-
// Check if multiLine text is truncated
50+
// Check if text is truncated
5151
useLayoutEffect(() => {
52-
if (variant === "multiLine" && spanRef.current) {
52+
if (spanRef.current) {
5353
const element = spanRef.current;
54-
// Check if the text is clamped by comparing scrollHeight with clientHeight
55-
const isTruncated = element.scrollHeight > element.clientHeight;
56-
setIsMultiLineTruncated(isTruncated);
54+
const isCurrentlyTruncated =
55+
variant === "multiLine"
56+
? element.scrollHeight > element.clientHeight
57+
: element.scrollWidth > element.clientWidth;
58+
setIsTruncated(isCurrentlyTruncated);
5759
}
5860
}, [value, variant, widthOfGroup]);
5961

6062
if (x === undefined || y === undefined) {
6163
return null;
6264
}
6365

64-
if (variant === "multiLine") {
65-
// The x position from Recharts is the center of the group
66-
// To center the foreignObject, we need to offset by half of widthOfGroup
67-
const calX = x - widthOfGroup / 2;
68-
const calWidth = widthOfGroup - 4;
66+
// The x position from Recharts is the center of the group
67+
// To center the foreignObject, we need to offset by half of widthOfGroup
68+
const calX = x - widthOfGroup / 2;
69+
const calWidth = variant === "multiLine" ? widthOfGroup - 4 : widthOfGroup - 10;
6970

70-
return (
71-
<g ref={ref} transform={`translate(${calX},${y})`}>
72-
<foreignObject
73-
ref={foreignObjectRef}
74-
transform="translate(0, 0)"
75-
width={calWidth}
76-
height={labelHeight} // Initial height, will be updated by useLayoutEffect
77-
className="crayon-chart-x-axis-tick-foreign"
78-
>
79-
<div
80-
style={{
81-
width: "100%",
82-
height: "100%",
83-
boxSizing: "border-box",
84-
}}
85-
>
86-
<LabelTooltip content={value} side="top" disabled={!isMultiLineTruncated}>
87-
<span
88-
ref={spanRef}
89-
style={{
90-
textAlign: "center",
91-
wordBreak: "break-word",
92-
}}
93-
className="crayon-chart-x-axis-tick-multi-line"
94-
>
95-
{value}
96-
</span>
97-
</LabelTooltip>
98-
</div>
99-
</foreignObject>
100-
</g>
101-
);
102-
}
103-
104-
const displayValue = tickFormatter ? tickFormatter(payload?.value) : value;
105-
// Check if displayValue ends with dots (indicating truncation)
106-
const isTruncated = displayValue.endsWith("...");
71+
const spanClassName =
72+
variant === "multiLine"
73+
? "crayon-chart-x-axis-tick-multi-line"
74+
: "crayon-chart-x-axis-tick-single-line";
10775

10876
return (
109-
<g ref={ref} transform={`translate(${x},${y})`} className={className}>
110-
<LabelTooltip content={value} side="top" disabled={!isTruncated}>
111-
<text x={0} y={0} dy={12} textAnchor={"middle"} className="crayon-chart-x-axis-tick">
112-
{displayValue}
113-
</text>
114-
</LabelTooltip>
77+
<g ref={ref} transform={`translate(${calX},${y})`} className={className}>
78+
<foreignObject
79+
ref={foreignObjectRef}
80+
transform="translate(0, 0)"
81+
width={calWidth}
82+
height={labelHeight}
83+
className="crayon-chart-x-axis-tick-foreign"
84+
>
85+
<div className="crayon-chart-x-axis-tick-container">
86+
<LabelTooltip content={value} side="top" disabled={!isTruncated}>
87+
<span
88+
ref={spanRef}
89+
style={{
90+
textAlign: "center",
91+
}}
92+
className={spanClassName}
93+
>
94+
{value}
95+
</span>
96+
</LabelTooltip>
97+
</div>
98+
</foreignObject>
11599
</g>
116100
);
117101
});
Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
11
@use "../../../../cssUtils" as cssUtils;
22

3-
.crayon-chart-x-axis-tick {
4-
@include cssUtils.typography(label, extra-small);
5-
fill: cssUtils.$secondary-text;
6-
}
7-
8-
.crayon-chart-x-axis-tick-multi-line-container {
3+
.crayon-chart-x-axis-tick-container {
94
// this is the div inside foreignObject
105
@include cssUtils.typography(label, extra-small);
116
color: cssUtils.$secondary-text;
7+
width: 100%;
8+
height: 100%;
9+
box-sizing: border-box;
1210
}
1311

1412
.crayon-chart-x-axis-tick-multi-line {
15-
color: cssUtils.$secondary-text;
1613
margin: 0;
1714
padding: 0;
1815
display: -webkit-box;
@@ -24,3 +21,11 @@
2421
word-break: break-word;
2522
min-width: 0;
2623
}
24+
25+
.crayon-chart-x-axis-tick-single-line {
26+
white-space: nowrap;
27+
overflow: hidden;
28+
text-overflow: ellipsis;
29+
width: 100%;
30+
display: block;
31+
}

0 commit comments

Comments
 (0)