-
Notifications
You must be signed in to change notification settings - Fork 2.8k
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
Handling central text overflow and adding tooltip for donut charts #26192
base: master
Are you sure you want to change the base?
Changes from 1 commit
7e3a3c2
42d2636
322ca58
62a5b7f
d1d70a7
66c1fa7
2457a45
8ce67c1
60789fd
b9e2c85
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,7 @@ | ||
{ | ||
"type": "patch", | ||
"comment": "Bug Fix", | ||
"packageName": "@fluentui/react-charting", | ||
"email": "[email protected]", | ||
"dependentChangeType": "patch" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,13 @@ | ||
/* eslint-disable @typescript-eslint/no-explicit-any */ | ||
import * as React from 'react'; | ||
import * as shape from 'd3-shape'; | ||
import { classNamesFunction } from '@fluentui/react/lib/Utilities'; | ||
import { getStyles } from './Arc.styles'; | ||
import { IChartDataPoint } from '../index'; | ||
import { IArcProps, IArcStyles } from './index'; | ||
import { wrapTextInsideDonut } from '../../../utilities/index'; | ||
import { select as d3Select } from 'd3-selection'; | ||
import { IProcessedStyleSet } from '../../../Styling'; | ||
|
||
export interface IArcState { | ||
isCalloutVisible?: boolean; | ||
|
@@ -19,6 +22,8 @@ export class Arc extends React.Component<IArcProps, IArcState> { | |
|
||
public state: {} = {}; | ||
|
||
private _tooltip: any; | ||
private _classNames: IProcessedStyleSet<IArcStyles>; | ||
private currentRef = React.createRef<SVGPathElement>(); | ||
|
||
public static getDerivedStateFromProps(nextProps: Readonly<IArcProps>): Partial<IArcState> | null { | ||
|
@@ -33,24 +38,33 @@ export class Arc extends React.Component<IArcProps, IArcState> { | |
public render(): JSX.Element { | ||
const { arc, href, focusedArcId } = this.props; | ||
const getClassNames = classNamesFunction<IArcProps, IArcStyles>(); | ||
const classNames = getClassNames(getStyles, { | ||
this._classNames = getClassNames(getStyles, { | ||
srmukher marked this conversation as resolved.
Show resolved
Hide resolved
|
||
color: this.props.color, | ||
href: href!, | ||
theme: this.props.theme!, | ||
}); | ||
const id = this.props.uniqText! + this.props.data!.data.legend!.replace(/\s+/, '') + this.props.data!.data.data; | ||
const opacity: number = | ||
this.props.activeArc === this.props.data!.data.legend || this.props.activeArc === '' ? 1 : 0.1; | ||
let truncatedText: string = ''; | ||
if (this.props.valueInsideDonut !== null && this.props.valueInsideDonut !== undefined) { | ||
truncatedText = this._getTruncatedText( | ||
this.props.valueInsideDonut!.toString(), | ||
this.props.innerRadius! * 2 - TEXT_PADDING, | ||
); | ||
} | ||
const isTruncated: boolean = truncatedText.slice(-3) === '...'; | ||
|
||
return ( | ||
<g ref={this.currentRef}> | ||
{!!focusedArcId && focusedArcId === id && ( | ||
<path id={id + 'focusRing'} d={arc(this.props.focusData)} className={classNames.focusRing} /> | ||
<path id={id + 'focusRing'} d={arc(this.props.focusData)} className={this._classNames.focusRing} /> | ||
)} | ||
<path | ||
id={id} | ||
d={arc(this.props.data)} | ||
onFocus={this._onFocus.bind(this, this.props.data!.data, id)} | ||
className={classNames.root} | ||
className={this._classNames.root} | ||
data-is-focusable={true} | ||
onMouseOver={this._hoverOn.bind(this, this.props.data!.data)} | ||
onMouseMove={this._hoverOn.bind(this, this.props.data!.data)} | ||
|
@@ -61,13 +75,30 @@ export class Arc extends React.Component<IArcProps, IArcState> { | |
aria-label={this._getAriaLabel()} | ||
role="img" | ||
/> | ||
<text textAnchor={'middle'} className={classNames.insideDonutString} y={5}> | ||
{this.props.valueInsideDonut!} | ||
</text> | ||
<g className={this._classNames.nodeTextContainer}> | ||
<text | ||
textAnchor={'middle'} | ||
className={this._classNames.insideDonutString} | ||
y={5} | ||
id={'Donut_center_text'} | ||
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. dont hardcode ids 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. Since the wrapping utility has dependency on this Id, need to keep this Id hardcoded |
||
onMouseOver={this._showTooltip.bind(this, this.props.valueInsideDonut!, isTruncated)} | ||
onMouseOut={this._hideTooltip} | ||
> | ||
{truncatedText} | ||
</text> | ||
</g> | ||
</g> | ||
); | ||
} | ||
|
||
public componentDidMount(): void { | ||
this._tooltip = d3Select('body') | ||
srmukher marked this conversation as resolved.
Show resolved
Hide resolved
|
||
.append('div') | ||
.attr('id', 'Donut_tooltip') | ||
.attr('class', this._classNames.tooltip!) | ||
.style('opacity', 0); | ||
} | ||
|
||
public componentDidUpdate(): void { | ||
const { href } = this.props; | ||
const getClassNames = classNamesFunction<IArcProps, IArcStyles>(); | ||
|
@@ -80,6 +111,59 @@ export class Arc extends React.Component<IArcProps, IArcState> { | |
wrapTextInsideDonut(classNames.insideDonutString, this.props.innerRadius! * 2 - TEXT_PADDING); | ||
} | ||
|
||
private _getTruncatedText(text: string, maxWidth: number): string { | ||
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. How does this work along with wrapTextInsideDonut, if wrapping and truncation both are enabled on the same text 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. wrapTextInsideDonut ensures that the words are placed in a new line if the current line length exceeds the maximum allowable width inside the donut. However, if a single word itself (even when placed in a new line) exceeds the maxWidth, it does not truncate the word. Thus, _getTruncatedText() determines if truncation is required for a single word or a group of words when inner text exceeds the maximum allowable width. |
||
const words = text.split(/\s+/).reverse(); | ||
let word: string = ''; | ||
const line: string[] = []; | ||
let truncatedText = text; | ||
const tspan = d3Select('#Donut_center_text').text(null).append('tspan'); | ||
let ellipsisLength = 0; | ||
|
||
if (tspan.node() !== null && tspan.node() !== undefined) { | ||
// Determine the ellipsis length for word truncation. | ||
tspan.text('...'); | ||
ellipsisLength = tspan.node()!.getComputedTextLength(); | ||
tspan.text(null); | ||
truncatedText = ''; | ||
|
||
while ((word = words.pop()!)) { | ||
line.push(word); | ||
tspan.text(line.join(' ') + ' '); | ||
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. You are truncating here without wrapping. First try to solve via wrapping and then by truncation. And lets discuss what all possible scenarios are there. 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. Made the change to wrap first and then truncate. Added 7 corner cases too that were handled. 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. In case 5 and case 6, you mentioned you are truncating vertically and horizontally, but you are truncating it only once. Can you clarify on this. 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. Atishay, this change is now outdated and I have moved it to the utilities.ts in wrapTextInsideDonut(). After wrapping, if the text exceeds either horizontally (exceeds the central width) or vertically (if it goes beyond the inner circle vertically), then we are adding the ellipsis and breaking. If we want to continue truncation even after the text is truncated one way, we will need to add ellipsis both horizontally and vertically might not be a good experience. 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. My comment is on the description that you have provided for case 5 and case 6. Here you are truncating only once and not both ways. Can you update the description. 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. Updated description. |
||
// Determine if truncation is required. If yes, append the ellipsis and break. | ||
if (tspan.node()!.getComputedTextLength() > maxWidth - ellipsisLength && line.length) { | ||
line.pop(); | ||
while (tspan.node()!.getComputedTextLength() > maxWidth - ellipsisLength) { | ||
word = word.slice(0, -1); | ||
tspan.text(word); | ||
} | ||
word += '...'; | ||
line.push(word); | ||
tspan.text(line.join(' ')); | ||
break; | ||
} | ||
} | ||
truncatedText = tspan.text(); | ||
tspan.text(null); | ||
} | ||
return truncatedText; | ||
} | ||
|
||
private _showTooltip = (text: string | number, checkTruncated: boolean, evt: any) => { | ||
if (checkTruncated && text !== null && text !== undefined && this._tooltip) { | ||
this._tooltip.style('opacity', 0.9); | ||
this._tooltip | ||
.html(text) | ||
.style('left', evt.pageX + 'px') | ||
.style('top', evt.pageY - 28 + 'px'); | ||
} | ||
}; | ||
|
||
private _hideTooltip = () => { | ||
if (this._tooltip) { | ||
this._tooltip.style('opacity', 0); | ||
} | ||
}; | ||
|
||
private _onFocus(data: IChartDataPoint, id: string): void { | ||
this.props.onFocusCallback!(data, id, this.currentRef.current); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there a default theme always available
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, the theme is coming as a prop from the parent component, which is a required property