-
Notifications
You must be signed in to change notification settings - Fork 856
Charts: Add labelOverflow ellipsis option for bar chart axis labels #46656
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 16 commits
6106971
be17496
f7299d2
c3f157c
7962ef4
ca007fa
e1b7e15
97aff7a
8c77d0b
edad9de
cb32e32
e240a6d
aedc9c0
ed18e54
37fe880
affdc65
66eaba3
8198ce7
e61bb0e
6b4d738
9f0c5a6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| Significance: minor | ||
| Type: added | ||
|
|
||
| Add labelOverflow ellipsis option to truncate long axis labels for bar chart. | ||
chihsuan marked this conversation as resolved.
Show resolved
Hide resolved
chihsuan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| export { useBarChartOptions } from './use-bar-chart-options'; | ||
| export { createTruncatedTickComponent } from './truncated-tick-component'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| import { DataContext } from '@visx/xychart'; | ||
| import { useContext } from 'react'; | ||
| import type { AxisScale, TickRendererProps } from '@visx/axis'; | ||
| import type { FC, CSSProperties } from 'react'; | ||
|
|
||
| /** | ||
| * Get the bandwidth of a scale | ||
| * | ||
| * @param scale - The scale to get the bandwidth of | ||
| * @return The bandwidth of the scale | ||
| */ | ||
| const getScaleBandwidth = < Scale extends AxisScale >( scale?: Scale ) => { | ||
| return scale && 'bandwidth' in scale ? scale.bandwidth() ?? 0 : 0; | ||
| }; | ||
chihsuan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| interface TruncatedTickComponentProps extends TickRendererProps { | ||
| /** Which axis this tick belongs to */ | ||
| axis: 'x' | 'y'; | ||
| } | ||
|
|
||
chihsuan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| /** | ||
| * Minimum width in pixels for tick labels when scale bandwidth is very small. | ||
| * Prevents labels from collapsing to unreadable widths on dense charts. | ||
| * | ||
| * Trade-off: When bandwidth is less than this minimum (e.g., many bars in a narrow chart), | ||
| * adjacent labels may overlap since each label uses this minimum width regardless of | ||
| * available space. This prioritizes label readability over preventing overlap. | ||
| * | ||
| * For very dense charts where overlap occurs, consider: | ||
| * - Using `numTicks` option to reduce the number of displayed labels | ||
| * - Using `tickFormat` to abbreviate label text | ||
| * - Increasing chart width or reducing data points | ||
| */ | ||
| const MIN_TICK_LABEL_WIDTH = 20; | ||
|
|
||
| /** | ||
| * A tick component that renders labels with text truncation (ellipsis) when they exceed | ||
| * the available bandwidth. Shows the full text on hover via native title attribute. | ||
| * | ||
| * Uses foreignObject to embed HTML within SVG, enabling CSS text-overflow: ellipsis. | ||
| * Inherits text styles from tickLabelProps passed by visx Axis component. | ||
| * | ||
| * Note: A minimum label width (MIN_TICK_LABEL_WIDTH) is enforced to keep labels readable. | ||
| * On very dense charts where bandwidth < 20px, this may cause label overlap. | ||
| * See MIN_TICK_LABEL_WIDTH documentation for mitigation strategies. | ||
| * | ||
| * @param props - The props for the truncated tick component | ||
| * @param props.x - The x position of the tick | ||
| * @param props.y - The y position of the tick | ||
| * @param props.formattedValue - The formatted value of the tick | ||
| * @param props.axis - The axis this tick belongs to | ||
| * @param props.textAnchor - The text anchor of the tick | ||
| * @param props.fill - The fill color of the tick | ||
| * @param props.dy - The dy offset of the tick | ||
| * | ||
| * @return The truncated tick component | ||
| */ | ||
| export const TruncatedTickComponent: FC< TruncatedTickComponentProps > = ( { | ||
| x, | ||
| y, | ||
| formattedValue, | ||
| axis, | ||
| textAnchor, | ||
| fill, | ||
| dy, | ||
| ...textProps | ||
| } ) => { | ||
| // Get max width of the tick label | ||
| const { xScale, yScale } = useContext( DataContext ) || {}; | ||
| const scale = axis === 'x' ? xScale : yScale; | ||
| const bandwidth = getScaleBandwidth( scale ); | ||
| const maxWidth = Math.max( bandwidth, MIN_TICK_LABEL_WIDTH ); | ||
|
|
||
| // Map SVG textAnchor to CSS textAlign | ||
| let textAlign: 'left' | 'right' | 'center' = 'center'; | ||
| if ( textAnchor === 'start' ) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just curious: how do we set the
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Our interface doesn’t currently support the textAnchor prop, but you can apply it by customizing the bar chart: <Axis
{ ...chartOptions.axis.x }
tickLabelProps={ {
textAnchor: 'start',
} }
/> |
||
| textAlign = 'left'; | ||
| } else if ( textAnchor === 'end' ) { | ||
| textAlign = 'right'; | ||
chihsuan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } else if ( textAnchor === 'middle' ) { | ||
| textAlign = 'center'; | ||
| } | ||
|
|
||
| // Calculate x offset based on text alignment | ||
| let xOffset = 0; | ||
| if ( textAlign === 'center' ) { | ||
| xOffset = -maxWidth / 2; | ||
| } else if ( textAlign === 'right' ) { | ||
| xOffset = -maxWidth; | ||
| } | ||
|
|
||
| // Extract compatible style properties from SVG text props | ||
| const { fontSize, fontFamily, fontWeight, fontStyle, letterSpacing, opacity } = textProps as { | ||
| fontSize?: CSSProperties[ 'fontSize' ]; | ||
| fontFamily?: CSSProperties[ 'fontFamily' ]; | ||
| fontWeight?: CSSProperties[ 'fontWeight' ]; | ||
| fontStyle?: CSSProperties[ 'fontStyle' ]; | ||
| letterSpacing?: CSSProperties[ 'letterSpacing' ]; | ||
| opacity?: CSSProperties[ 'opacity' ]; | ||
| }; | ||
|
|
||
| const textStyles: CSSProperties = { | ||
| /** | ||
| * SVG <text> elements are vertically aligned to the baseline by default, but HTML <div> elements inside <foreignObject> | ||
| * are positioned relative to the top-left corner. To visually align the tick label like SVG text, | ||
| * we shift the div up by 100% of its height and adjust by twice the SVG dy value (from visx) to approximate original placement. | ||
| */ | ||
| transform: `translateY(calc(-100% + ${ dy ?? '0' } * 2))`, | ||
chihsuan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // Safari doesn't work well with foreignObject, this is a workaround to position the div correctly. | ||
| position: 'fixed', | ||
chihsuan marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| // Apply SVG-like font properties from visx text props to the HTML div. | ||
chihsuan marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| fontSize, | ||
| fontFamily, | ||
| fontWeight, | ||
| fontStyle, | ||
| letterSpacing, | ||
| opacity, | ||
| // Convert svg text styles to CSS styles for the div | ||
| color: fill ?? 'inherit', | ||
| textAlign, | ||
| // Ensure text is truncated with ellipsis, remains on one line, and shows the full value in a tooltip on hover. | ||
| // The surrounding div uses CSS to handle overflow, and the 'title' attribute is set for accessibility. | ||
| width: maxWidth, | ||
| overflow: 'hidden', | ||
| textOverflow: 'ellipsis', | ||
| whiteSpace: 'nowrap', | ||
| cursor: 'default', | ||
| pointerEvents: 'auto', | ||
| }; | ||
|
|
||
| return ( | ||
| <foreignObject x={ x + xOffset } y={ y } width={ maxWidth } overflow="visible"> | ||
| <div | ||
|
Check failure on line 132 in projects/js-packages/charts/src/charts/bar-chart/private/truncated-tick-component.tsx
|
||
| style={ textStyles } | ||
| title={ formattedValue || undefined } | ||
| > | ||
| { formattedValue } | ||
| </div> | ||
| </foreignObject> | ||
| ); | ||
chihsuan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }; | ||
|
|
||
| export const createTruncatedTickComponent = ( axis: 'x' | 'y' ) => ( props: TickRendererProps ) => { | ||
| return <TruncatedTickComponent { ...props } axis={ axis } />; | ||
| }; | ||
chihsuan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Uh oh!
There was an error while loading. Please reload this page.