From 42d263678979c48582813f9da3379e6cc0eeca2a Mon Sep 17 00:00:00 2001 From: srmukher Date: Thu, 19 Jan 2023 12:25:29 +0530 Subject: [PATCH] Wrapping followed by truncation of inner text --- .../src/components/DonutChart/Arc/Arc.tsx | 71 +++------- .../react-charting/src/utilities/utilities.ts | 133 ++++++++++++++++-- 2 files changed, 137 insertions(+), 67 deletions(-) diff --git a/packages/react-charting/src/components/DonutChart/Arc/Arc.tsx b/packages/react-charting/src/components/DonutChart/Arc/Arc.tsx index fa1231ad8cf6f..67381e12c510a 100644 --- a/packages/react-charting/src/components/DonutChart/Arc/Arc.tsx +++ b/packages/react-charting/src/components/DonutChart/Arc/Arc.tsx @@ -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'; @@ -31,6 +31,10 @@ export class Arc extends React.Component { return null; } + public constructor(props: IArcProps) { + super(props); + } + public updateChart(newProps: IArcProps): void { _updateChart(newProps); } @@ -46,14 +50,6 @@ export class Arc extends React.Component { 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 ( @@ -81,10 +77,10 @@ export class Arc extends React.Component { 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} @@ -92,11 +88,13 @@ export class Arc extends React.Component { } 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 { @@ -111,45 +109,8 @@ export class Arc extends React.Component { 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) diff --git a/packages/react-charting/src/utilities/utilities.ts b/packages/react-charting/src/utilities/utilities.ts index 0260dff4beeaf..7089e8d86072d 100644 --- a/packages/react-charting/src/utilities/utilities.ts +++ b/packages/react-charting/src/utilities/utilities.ts @@ -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(); @@ -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) @@ -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) === '...'; }