Skip to content

Commit

Permalink
Wrapping followed by truncation of inner text
Browse files Browse the repository at this point in the history
  • Loading branch information
srmukher committed Feb 2, 2023
1 parent 7e3a3c2 commit 42d2636
Show file tree
Hide file tree
Showing 2 changed files with 137 additions and 67 deletions.
71 changes: 16 additions & 55 deletions packages/react-charting/src/components/DonutChart/Arc/Arc.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* 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 { classNamesFunction, getId } from '@fluentui/react/lib/Utilities';
import { getStyles } from './Arc.styles';
import { IChartDataPoint } from '../index';
import { IArcProps, IArcStyles } from './index';
Expand Down Expand Up @@ -31,6 +31,10 @@ export class Arc extends React.Component<IArcProps, IArcState> {
return null;
}

public constructor(props: IArcProps) {
super(props);
}

public updateChart(newProps: IArcProps): void {
_updateChart(newProps);
}
Expand All @@ -46,14 +50,6 @@ export class Arc extends React.Component<IArcProps, IArcState> {
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}>
Expand Down Expand Up @@ -81,22 +77,24 @@ export class Arc extends React.Component<IArcProps, IArcState> {
className={this._classNames.insideDonutString}
y={5}
id={'Donut_center_text'}
onMouseOver={this._showTooltip.bind(this, this.props.valueInsideDonut!, isTruncated)}
onMouseOver={this._showTooltip.bind(this, this.props.valueInsideDonut)}
onMouseOut={this._hideTooltip}
>
{truncatedText}
{this.props.valueInsideDonut}
</text>
</g>
</g>
);
}

public componentDidMount(): void {
this._tooltip = d3Select('body')
.append('div')
.attr('id', 'Donut_tooltip')
.attr('class', this._classNames.tooltip!)
.style('opacity', 0);
if (!this._tooltip) {
this._tooltip = d3Select('body')
.append('div')
.attr('id', getId('_Donut_tooltip_'))
.attr('class', this._classNames.tooltip!)
.style('opacity', 0);
}
}

public componentDidUpdate(): void {
Expand All @@ -111,45 +109,8 @@ 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) {
private _showTooltip = (text: string | number, evt: any) => {
if (text !== null && text !== undefined && this._tooltip) {
this._tooltip.style('opacity', 0.9);
this._tooltip
.html(text)
Expand Down
133 changes: 121 additions & 12 deletions packages/react-charting/src/utilities/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1076,8 +1076,10 @@ export function rotateXAxisLabels(rotateLabelProps: IRotateLabelProps) {
return Math.floor(maxHeight / 1.414); // Compute maxHeight/tanInverse(45) to get the vertical height of labels.
}

export function wrapTextInsideDonut(selectorClass: string, maxWidth: number) {
export function wrapTextInsideDonut(selectorClass: string, maxWidth: number): string {
let idx: number = 0;
let value: string = '';

d3SelectAll(`.${selectorClass}`).each(function () {
const text = d3Select(this);
const words = text.text().split(/\s+/).reverse();
Expand All @@ -1086,6 +1088,8 @@ export function wrapTextInsideDonut(selectorClass: string, maxWidth: number) {
let lineNumber: number = 0;
const lineHeight = 1.1; // ems
const y = text.attr('y');
const ellipsis: string = '...';
let isTruncationRequired: boolean = false;

let tspan = text
.text(null)
Expand All @@ -1095,22 +1099,127 @@ export function wrapTextInsideDonut(selectorClass: string, maxWidth: number) {
.attr('y', y)
.attr('dy', lineNumber++ * lineHeight + 'em');

// tspanEllipsis is used for checking if truncation is required vertically/horizontally
// before appending the text to the inner donut text
let tspanEllipsis = d3Select('#Donut_center_text').text(null).append('tspan').attr('opacity', 0);

// Determine the ellipsis length for word truncation.
tspanEllipsis.text(ellipsis);
const ellipsisLength = tspanEllipsis.node()!.getComputedTextLength();
tspanEllipsis.text(null);

// Value concatinates and saves the final truncated string and returns it back to the donut chart
// to handle mouse over and mouse out scenarios
value = '';

while ((word = words.pop()!)) {
line.push(word);
tspan.text(line.join(' ') + ' ');
if (tspan.node()!.getComputedTextLength() > maxWidth && line.length > 1) {
line.pop();
tspan.text(line.join(' ') + ' ');
line = [word];
tspan = text
.append('tspan')
.attr('id', `WordBreakId-${idx}-${lineNumber}`)
.attr('x', 0)
.attr('y', y)
.attr('dy', lineNumber++ * lineHeight + 'em')
.text(word);
tspanEllipsis.text(line.join(' ') + ' ');

// Determine if wrapping is required
if (tspan.node()!.getComputedTextLength() > maxWidth - ellipsisLength && line.length > 1) {
while (tspan.node()!.getComputedTextLength() > maxWidth - ellipsisLength) {
line.pop();
tspan.text(line.join(' ') + ' ');
tspanEllipsis.text(line.join(' ') + ' ');
}
// Determine if truncation is required vertically
// If truncation is not required vertically, append a new line while taking care of horizontal truncation
if (tspan.node()!.getBoundingClientRect().y < maxWidth) {
line = [word];
tspanEllipsis.text(word);

// Determine if truncation is appending the text exceeds maximum width vertically or horizontally
while (
tspanEllipsis.node()!.getComputedTextLength() > maxWidth - ellipsisLength ||
tspanEllipsis.node()!.getBoundingClientRect().y > maxWidth
) {
word = line.pop()!;
word = word.slice(0, -1);
line = [word];
tspanEllipsis.text(word);
isTruncationRequired = true;
}

// If after truncation, the word becomes empty, append the ellipsis to the last line
if (word.length === 0) {
tspan.text(tspan.text().trim() + ellipsis);
value = value.trim() + ellipsis;
break;
}
// Trim whitespaces if any
word = word.trim();

// Append the ellipsis only if the word was truncated and word is not the last word in the sentence.
if (isTruncationRequired && !isTextTruncated(word)) {
// Append '.' only as much required
while (!isTextTruncated(word)) {
word = word + '.';
}
}
tspan = text
.append('tspan')
.attr('id', `WordBreakId-${idx}-${lineNumber}`)
.attr('x', 0)
.attr('y', y)
.attr('dy', lineNumber++ * lineHeight + 'em')
.text(word);
tspanEllipsis = d3Select('#Donut_center_text')
.append('tspan')
.attr('id', `WordBreakId-${idx}-${lineNumber}`)
.attr('x', 0)
.attr('y', y)
.attr('dy', lineNumber++ * lineHeight + 'em')
.text(word);
value += word + ' ';

// If truncation was done either verticaly or horizontally, break
if (isTruncationRequired) {
tspanEllipsis.text(null);
break;
}
} else {
// If truncation is required vertically, append ellipsis and break
tspan.text(line.join(' ') + ellipsis);
value += ellipsis;
break;
}
} else {
// If there is just 1 line which exceeds the max width horizontally,
// no wrapping required, only truncate
tspanEllipsis.text(tspanEllipsis.text().trim());
tspan.text(tspan.text().trim());
while (tspanEllipsis.node()!.getComputedTextLength() > maxWidth - ellipsisLength) {
word = line.pop()!.trim();
word = word.slice(0, -1);
line = [word];
tspanEllipsis.text(word);
isTruncationRequired = true;
}
// Trim whitespaces if any
word = word.trim();
// Append the ellipsis only if the word was truncated
if (isTruncationRequired && !isTextTruncated(word)) {
// Append '.' only as much required
while (!isTextTruncated(word)) {
word = word + '.';
}
value += word;
line = [word];
tspan.text(word);
break;
}
// If no truncation is required
value += word + ' ';
}
}
tspanEllipsis.text(null);
idx += 1;
});
return value.trim();
}

export function isTextTruncated(text: string): boolean {
return text.slice(-3) === '...';
}

0 comments on commit 42d2636

Please sign in to comment.