-
Notifications
You must be signed in to change notification settings - Fork 851
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 8 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,149 @@ | ||
| 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 ) => { | ||
| const s = scale as AxisScale; | ||
| return s && 'bandwidth' in s ? s?.bandwidth() ?? 0 : 0; | ||
| }; | ||
chihsuan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
chihsuan marked this conversation as resolved.
Outdated
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 = { | ||
| // Offset y to convert from baseline to top-left positioning because svg text is positioned by baseline, but html div is positioned by top-left. | ||
| transform: 'translateY(-100%)', | ||
| // Apply compatible SVG text styles | ||
| 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" | ||
| // dy * 2: The div's translateY(-100%) and visx's pre-calculated y offset | ||
| // create a compound effect that requires doubling dy to match original text position. | ||
| style={ { transform: `translateY(calc(${ dy ?? '0' } * 2))` } } | ||
| > | ||
chihsuan marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| <div | ||
| style={ textStyles } | ||
| title={ formattedValue || undefined } | ||
| aria-label={ formattedValue || undefined } | ||
chihsuan marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
chihsuan marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| > | ||
| { formattedValue } | ||
| </div> | ||
| </foreignObject> | ||
| ); | ||
| }; | ||
|
|
||
| export const createTruncatedTickComponent = ( axis: 'x' | 'y' ) => ( props: TickRendererProps ) => { | ||
| return <TruncatedTickComponent { ...props } axis={ axis } />; | ||
| }; | ||
Uh oh!
There was an error while loading. Please reload this page.