Skip to content

Commit

Permalink
Handling text overflow and adding tooltip for center texts of donut c…
Browse files Browse the repository at this point in the history
…harts
  • Loading branch information
srmukher committed Jan 6, 2023
1 parent 4fbff6b commit 7e3a3c2
Show file tree
Hide file tree
Showing 5 changed files with 488 additions and 106 deletions.
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
Expand Up @@ -29,5 +29,32 @@ export const getStyles = (props: IArcProps): IArcStyles => {
},
},
},
tooltip: {
...theme.fonts.medium,
display: 'flex',
flexDirection: 'column',
padding: '8px',
position: 'absolute',
textAlign: 'center',
top: '0px',
background: theme.semanticColors.bodyBackground,
borderRadius: '2px',
pointerEvents: 'none',
},
nodeTextContainer: {
selectors: {
text: {
selectors: {
[HighContrastSelectorBlack]: {
fill: 'rgb(179, 179, 179)',
},
},
},
},
marginTop: '4px',
marginLeft: '8px',
marginBottom: '4px',
marginRight: '8px',
},
};
};
96 changes: 90 additions & 6 deletions packages/react-charting/src/components/DonutChart/Arc/Arc.tsx
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;
Expand All @@ -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 {
Expand All @@ -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, {
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)}
Expand All @@ -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'}
onMouseOver={this._showTooltip.bind(this, this.props.valueInsideDonut!, isTruncated)}
onMouseOut={this._hideTooltip}
>
{truncatedText}
</text>
</g>
</g>
);
}

public componentDidMount(): void {
this._tooltip = d3Select('body')
.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>();
Expand All @@ -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 {
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(' ') + ' ');
// 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);
}
Expand Down
10 changes: 10 additions & 0 deletions packages/react-charting/src/components/DonutChart/Arc/Arc.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,14 @@ export interface IArcStyles {
* styles for the focus
*/
focusRing: IStyle;

/**
* Style for tool tip
*/
tooltip?: IStyle;

/**
* Style for overflow center text container
*/
nodeTextContainer?: IStyle;
}
Loading

0 comments on commit 7e3a3c2

Please sign in to comment.