@unovis/ts 1.1.1-beta.0 → 1.1.1-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/axis/config.d.ts +0 -3
- package/components/axis/config.js +0 -1
- package/components/axis/config.js.map +1 -1
- package/components/axis/index.d.ts +1 -8
- package/components/axis/index.js +29 -24
- package/components/axis/index.js.map +1 -1
- package/components/chord-diagram/modules/label.js +4 -2
- package/components/chord-diagram/modules/label.js.map +1 -1
- package/components/donut/index.js +1 -2
- package/components/donut/index.js.map +1 -1
- package/components/flow-legend/style.js +2 -2
- package/components/flow-legend/style.js.map +1 -1
- package/components/graph/modules/node/index.js +3 -3
- package/components/graph/modules/node/index.js.map +1 -1
- package/components/graph/modules/panel/index.js +3 -3
- package/components/graph/modules/panel/index.js.map +1 -1
- package/components/graph/style.js +2 -2
- package/components/graph/style.js.map +1 -1
- package/components/leaflet-map/index.d.ts +33 -0
- package/components/leaflet-map/index.js +30 -7
- package/components/leaflet-map/index.js.map +1 -1
- package/components/leaflet-map/modules/node.js +8 -7
- package/components/leaflet-map/modules/node.js.map +1 -1
- package/components/leaflet-map/modules/utils.d.ts +1 -1
- package/components/leaflet-map/modules/utils.js +4 -5
- package/components/leaflet-map/modules/utils.js.map +1 -1
- package/components/leaflet-map/style.d.ts +33 -1
- package/components/leaflet-map/style.js +54 -86
- package/components/leaflet-map/style.js.map +1 -1
- package/components/nested-donut/config.d.ts +59 -0
- package/components/nested-donut/config.js +19 -0
- package/components/nested-donut/config.js.map +1 -0
- package/components/nested-donut/index.d.ts +38 -0
- package/components/nested-donut/index.js +133 -0
- package/components/nested-donut/index.js.map +1 -0
- package/components/nested-donut/modules/arc.d.ts +17 -0
- package/components/nested-donut/modules/arc.js +50 -0
- package/components/nested-donut/modules/arc.js.map +1 -0
- package/components/nested-donut/modules/label.d.ts +7 -0
- package/components/nested-donut/modules/label.js +34 -0
- package/components/nested-donut/modules/label.js.map +1 -0
- package/components/nested-donut/style.d.ts +26 -0
- package/components/nested-donut/style.js +70 -0
- package/components/nested-donut/style.js.map +1 -0
- package/components/nested-donut/types.d.ts +24 -0
- package/components/nested-donut/types.js +11 -0
- package/components/nested-donut/types.js.map +1 -0
- package/components/sankey/modules/label.js +2 -2
- package/components/sankey/modules/label.js.map +1 -1
- package/components/sankey/style.js +2 -2
- package/components/sankey/style.js.map +1 -1
- package/components/vis-controls/style.js +2 -2
- package/components/vis-controls/style.js.map +1 -1
- package/components/xy-labels/config.d.ts +2 -2
- package/components/xy-labels/config.js.map +1 -1
- package/components.d.ts +2 -0
- package/components.js +1 -0
- package/components.js.map +1 -1
- package/containers/single-container/index.js +4 -1
- package/containers/single-container/index.js.map +1 -1
- package/containers/xy-container/index.js +1 -1
- package/containers/xy-container/index.js.map +1 -1
- package/core/component/index.d.ts +5 -1
- package/core/component/index.js +9 -1
- package/core/component/index.js.map +1 -1
- package/core/container/index.d.ts +3 -2
- package/core/container/index.js +12 -7
- package/core/container/index.js.map +1 -1
- package/index.d.ts +1 -0
- package/index.js +4 -1
- package/index.js.map +1 -1
- package/package.json +1 -1
- package/styles/index.d.ts +6 -1
- package/styles/index.js +18 -3
- package/styles/index.js.map +1 -1
- package/types/svg.d.ts +18 -0
- package/types/svg.js +22 -0
- package/types/svg.js.map +1 -0
- package/types/text.d.ts +27 -18
- package/types/text.js +1 -6
- package/types/text.js.map +1 -1
- package/types.d.ts +1 -0
- package/types.js +2 -1
- package/types.js.map +1 -1
- package/utils/data.js +1 -1
- package/utils/data.js.map +1 -1
- package/utils/style.d.ts +6 -0
- package/utils/style.js +24 -0
- package/utils/style.js.map +1 -0
- package/utils/svg.d.ts +3 -0
- package/utils/text.d.ts +134 -13
- package/utils/text.js +361 -126
- package/utils/text.js.map +1 -1
- package/utils/type.d.ts +5 -0
- package/components/axis/modules/tick.d.ts +0 -5
- package/components/axis/modules/tick.js +0 -48
- package/components/axis/modules/tick.js.map +0 -1
package/utils/text.js
CHANGED
|
@@ -1,51 +1,76 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { sum } from 'd3-array';
|
|
2
|
+
import { TrimMode, VerticalAlign, TextAlign } from '../types/text.js';
|
|
3
|
+
import { flatten, isArray, merge } from './data.js';
|
|
4
|
+
import { getTextAnchorFromTextAlign } from '../types/svg.js';
|
|
5
|
+
import { getFontWidthToHeightRatio, UNOVIS_TEXT_SEPARATOR_DEFAULT, UNOVIS_TEXT_DEFAULT } from '../styles/index.js';
|
|
3
6
|
|
|
4
|
-
|
|
5
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Converts a kebab-case string to camelCase.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} str - The kebab-case string to be converted.
|
|
11
|
+
* @returns {string} The resulting camelCase string.
|
|
12
|
+
*/
|
|
13
|
+
function kebabCaseToCamel(str) {
|
|
14
|
+
return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Converts a given string to kebab-case.
|
|
18
|
+
* @param {string} str - The input string to be converted to kebab-case.
|
|
19
|
+
* @returns {string} - The kebab-cased string.
|
|
20
|
+
*/
|
|
21
|
+
function kebabCase(str) {
|
|
22
|
+
var _a;
|
|
23
|
+
return (_a = str.match(/[A-Z]{2,}(?=[A-Z][a-z0-9]*|\b)|[A-Z]?[a-z0-9]*|[A-Z]|[0-9]+/g)) === null || _a === void 0 ? void 0 : _a.filter(Boolean).map(x => x.toLowerCase()).join('-');
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Trims the input string from the start, leaving only the specified maximum length.
|
|
27
|
+
* @param {string} [str=''] - The input string to be trimmed.
|
|
28
|
+
* @param {number} [maxLength=15] - The maximum allowed length of the trimmed string.
|
|
29
|
+
* @returns {string} - The trimmed string.
|
|
30
|
+
*/
|
|
31
|
+
function trimStringStart(str = '', maxLength = 15) {
|
|
6
32
|
return str.length > maxLength ? `…${str.substr(str.length - maxLength, maxLength)}` : str;
|
|
7
33
|
}
|
|
8
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Trims the input string from the middle, leaving only the specified maximum length.
|
|
36
|
+
* @param {string} [str=''] - The input string to be trimmed.
|
|
37
|
+
* @param {number} [maxLength=15] - The maximum allowed length of the trimmed string.
|
|
38
|
+
* @returns {string} - The trimmed string.
|
|
39
|
+
*/
|
|
40
|
+
function trimStringMiddle(str = '', maxLength = 15) {
|
|
9
41
|
const dist = Math.floor((maxLength - 3) / 2);
|
|
10
42
|
return str.length > maxLength ? `${str.substr(0, dist)}…${str.substr(-dist, dist)}` : str;
|
|
11
43
|
}
|
|
12
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Trims the input string from the end, leaving only the specified maximum length.
|
|
46
|
+
* @param {string} [str=''] - The input string to be trimmed.
|
|
47
|
+
* @param {number} [maxLength=15] - The maximum allowed length of the trimmed string.
|
|
48
|
+
* @returns {string} - The trimmed string.
|
|
49
|
+
*/
|
|
50
|
+
function trimStringEnd(str = '', maxLength = 15) {
|
|
13
51
|
return str.length > maxLength ? `${str.substr(0, maxLength)}…` : str;
|
|
14
52
|
}
|
|
15
|
-
|
|
16
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Trims the input string according to the specified trim mode.
|
|
55
|
+
* @param {string} [str=''] - The input string to be trimmed.
|
|
56
|
+
* @param {number} [length=15] - The maximum allowed length of the trimmed string.
|
|
57
|
+
* @param {TrimMode} [type=TrimMode.Middle] - The trim mode to be applied.
|
|
58
|
+
* @returns {string} - The trimmed string.
|
|
59
|
+
*/
|
|
60
|
+
function trimString(str = '', length = 15, type = TrimMode.Middle) {
|
|
61
|
+
let result = trimStringEnd(str, length);
|
|
17
62
|
if (type === TrimMode.Start)
|
|
18
|
-
result =
|
|
63
|
+
result = trimStringStart(str, length);
|
|
19
64
|
else if (type === TrimMode.Middle)
|
|
20
|
-
result =
|
|
65
|
+
result = trimStringMiddle(str, length);
|
|
21
66
|
return result;
|
|
22
67
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
textLength -= 1;
|
|
30
|
-
svgTextSelection.text(trimText(text, text.length - i, trimType));
|
|
31
|
-
i = i + 1;
|
|
32
|
-
textBBox = svgTextSelection.node().getBBox();
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
function breakTspan(tspan, width, word = '') {
|
|
36
|
-
const tspanText = tspan.text();
|
|
37
|
-
const coeff = width / tspan.node().getComputedTextLength();
|
|
38
|
-
if (coeff < 1) {
|
|
39
|
-
const cutIndex = Math.floor(coeff * tspanText.length) || 1;
|
|
40
|
-
const head = tspanText.substr(0, cutIndex);
|
|
41
|
-
const tail = tspanText.substr(cutIndex, tspanText.length);
|
|
42
|
-
tspan.text(head);
|
|
43
|
-
return `${tail}${word}`;
|
|
44
|
-
}
|
|
45
|
-
else {
|
|
46
|
-
return word;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
68
|
+
/**
|
|
69
|
+
* Splits the input string according to the specified separators.
|
|
70
|
+
* @param {string} text - The input string to be split.
|
|
71
|
+
* @param {string[]} [separators=[' ']] - The array of separators to be used for splitting.
|
|
72
|
+
* @returns {string[]} - The array of split words.
|
|
73
|
+
*/
|
|
49
74
|
function splitString(text, separators = [' ']) {
|
|
50
75
|
let result = [text];
|
|
51
76
|
for (let i = 0; i < separators.length; i++) {
|
|
@@ -53,96 +78,22 @@ function splitString(text, separators = [' ']) {
|
|
|
53
78
|
result.forEach((d, id) => {
|
|
54
79
|
const separated = d.split(sep);
|
|
55
80
|
const words = separated.map((word, j) => `${word}${j === separated.length - 1 ? '' : sep}`);
|
|
56
|
-
// result.splice(i, 0, ...words)
|
|
57
81
|
result[id] = words;
|
|
58
82
|
});
|
|
59
83
|
result = flatten(result);
|
|
60
84
|
}
|
|
61
85
|
return result;
|
|
62
86
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
text = trimText(text, length, trimType); // By the number of characters
|
|
71
|
-
if (!length && trimOnly && width) { // By provided width if `trimOnly` is set
|
|
72
|
-
trimSVGTextToPixel(element, width, trimType);
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
// Wrap
|
|
76
|
-
const separators = (isArray(separator) ? separator : [separator]);
|
|
77
|
-
const words = splitString(text, separators);
|
|
78
|
-
const x = parseFloat(element.attr('x')) || 0;
|
|
79
|
-
element.text('');
|
|
80
|
-
let tspan = element.append('tspan').attr('x', x);
|
|
81
|
-
let tspanText = `${words[0]}`;
|
|
82
|
-
tspan.text(tspanText);
|
|
83
|
-
let tspanCount = 1;
|
|
84
|
-
words.forEach((word, i) => {
|
|
85
|
-
if (i === 0)
|
|
86
|
-
return;
|
|
87
|
-
const _text = `${tspanText}${word}`;
|
|
88
|
-
tspan.text(_text);
|
|
89
|
-
const _wrapText = wrapMode === WrapMode.FontSize ? fontSize * _text.length * widthToHeightRatio > width
|
|
90
|
-
: tspan.node().getComputedTextLength() > width;
|
|
91
|
-
if (_wrapText) {
|
|
92
|
-
tspan.text(tspanText.trim());
|
|
93
|
-
if (wordBreak)
|
|
94
|
-
word = breakTspan(tspan, width, word);
|
|
95
|
-
tspan = element.append('tspan')
|
|
96
|
-
.attr('x', x)
|
|
97
|
-
.attr('dy', '1.2em')
|
|
98
|
-
.text(word);
|
|
99
|
-
tspanCount += 1;
|
|
100
|
-
tspanText = word;
|
|
101
|
-
}
|
|
102
|
-
else
|
|
103
|
-
tspanText += word;
|
|
104
|
-
});
|
|
105
|
-
if (wordBreak) {
|
|
106
|
-
let numTspan;
|
|
107
|
-
if (wrapMode === WrapMode.FontSize) {
|
|
108
|
-
numTspan = Math.ceil(tspan.text().length * fontSize * widthToHeightRatio / (width || 1));
|
|
109
|
-
}
|
|
110
|
-
else {
|
|
111
|
-
numTspan = Math.ceil(tspan.node().getComputedTextLength() / (width || 1));
|
|
112
|
-
}
|
|
113
|
-
for (let i = 0; i < numTspan; i++) {
|
|
114
|
-
const word = breakTspan(tspan, width || 1);
|
|
115
|
-
if (word) {
|
|
116
|
-
tspan = element.append('tspan')
|
|
117
|
-
.attr('x', x)
|
|
118
|
-
.attr('dy', '1.2em')
|
|
119
|
-
.text(word);
|
|
120
|
-
tspanCount += 1;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
// Vertical Align
|
|
125
|
-
let addY = -(tspanCount - 1) * 0.5 + dy;
|
|
126
|
-
if (verticalAlign === VerticalAlign.Bottom)
|
|
127
|
-
addY = dy;
|
|
128
|
-
else if (verticalAlign === VerticalAlign.Top)
|
|
129
|
-
addY = -(tspanCount - 1) - (1 - dy);
|
|
130
|
-
element.attr('dy', `${addY}em`);
|
|
131
|
-
}
|
|
132
|
-
function cutString(str, renderedWidth, maxWidth) {
|
|
133
|
-
const coeff = maxWidth / renderedWidth;
|
|
134
|
-
if (coeff >= 1)
|
|
135
|
-
return [str, ''];
|
|
136
|
-
const cutIndex = Math.floor(coeff * str.length) || 1;
|
|
137
|
-
const head = str.substr(0, cutIndex);
|
|
138
|
-
const tail = str.substr(cutIndex, str.length);
|
|
139
|
-
return [head, tail];
|
|
140
|
-
}
|
|
141
|
-
function wrapSVGText(textElement, options) {
|
|
87
|
+
/**
|
|
88
|
+
* Wraps an SVG text element to fit within the specified width.
|
|
89
|
+
* @param {Selection<SVGTextElement, any, SVGElement, any>} textElement - The SVG text element to be wrapped.
|
|
90
|
+
* @param {number} width - The maximum allowed width for the text element.
|
|
91
|
+
* @param {(string | string[])} [separator=[' ', '-', '.', ',']] - The separator(s) to be used for wrapping.
|
|
92
|
+
*/
|
|
93
|
+
function wrapSVGText(textElement, width, separator = [' ', '-', '.', ',']) {
|
|
142
94
|
const text = textElement.text();
|
|
143
95
|
if (!text)
|
|
144
96
|
return;
|
|
145
|
-
const { width, separator = [' ', '-', '.', ','], } = options;
|
|
146
97
|
// Wrap
|
|
147
98
|
const separators = (isArray(separator) ? separator : [separator]);
|
|
148
99
|
const words = splitString(text, separators);
|
|
@@ -169,40 +120,324 @@ function wrapSVGText(textElement, options) {
|
|
|
169
120
|
tspanContent += word;
|
|
170
121
|
});
|
|
171
122
|
}
|
|
172
|
-
|
|
123
|
+
/**
|
|
124
|
+
* Trims an SVG text element based on the specified max width, trim type, and other options.
|
|
125
|
+
* @param {Selection<SVGTextElement, any, SVGElement, any>} svgTextSelection - The D3 selection of the SVG text element to be trimmed.
|
|
126
|
+
* @param {number} [maxWidth=50] - The maximum width of the text element.
|
|
127
|
+
* @param {TrimMode} [trimType=TrimMode.Middle] - The type of trim (start, middle, or end).
|
|
128
|
+
* @param {boolean} [fastMode=true] - Whether to use a fast estimation method for text length calculation.
|
|
129
|
+
* @param {number} [fontSize=0] - The font size of the text.
|
|
130
|
+
* @param {number} [fontWidthToHeightRatio=getFontWidthToHeightRatio()] - The font width to height ratio.
|
|
131
|
+
* @returns {boolean} True if the text was trimmed, false otherwise.
|
|
132
|
+
*/
|
|
133
|
+
function trimSVGText(svgTextSelection, maxWidth, trimType, fastMode, fontSize, fontWidthToHeightRatio) {
|
|
134
|
+
var _a;
|
|
135
|
+
if (maxWidth === void 0) { maxWidth = 50; }
|
|
136
|
+
if (trimType === void 0) { trimType = TrimMode.Middle; }
|
|
137
|
+
if (fastMode === void 0) { fastMode = true; }
|
|
138
|
+
if (fontSize === void 0) { fontSize = +((_a = window.getComputedStyle(svgTextSelection.node())) === null || _a === void 0 ? void 0 : _a.fontSize) || 0; }
|
|
139
|
+
if (fontWidthToHeightRatio === void 0) { fontWidthToHeightRatio = getFontWidthToHeightRatio(); }
|
|
173
140
|
const text = svgTextSelection.text();
|
|
174
141
|
const textLength = text.length;
|
|
175
|
-
const textWidth = fastMode ? fontSize * textLength *
|
|
142
|
+
const textWidth = fastMode ? fontSize * textLength * fontWidthToHeightRatio : svgTextSelection.node().getComputedTextLength();
|
|
176
143
|
const tolerance = 1.1;
|
|
177
144
|
const maxCharacters = Math.ceil(textLength * maxWidth / (tolerance * textWidth));
|
|
178
145
|
if (maxCharacters < textLength) {
|
|
179
|
-
svgTextSelection.text(
|
|
146
|
+
svgTextSelection.text(trimString(text, maxCharacters, trimType));
|
|
180
147
|
return true;
|
|
181
148
|
}
|
|
182
149
|
return false;
|
|
183
150
|
}
|
|
184
|
-
|
|
185
|
-
|
|
151
|
+
/**
|
|
152
|
+
* Estimates the length of a string in pixels.
|
|
153
|
+
* @param {string} str - The string to be measured.
|
|
154
|
+
* @param {number} fontSize - The font size of the string.
|
|
155
|
+
* @param {number} [fontWidthToHeightRatio=getFontWidthToHeightRatio()] - The font width to height ratio.
|
|
156
|
+
* @returns {number} The estimated length of the string in pixels.
|
|
157
|
+
*/
|
|
158
|
+
function estimateStringPixelLength(str, fontSize, fontWidthToHeightRatio = getFontWidthToHeightRatio()) {
|
|
159
|
+
return str.length * fontSize * fontWidthToHeightRatio || 0;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Calculates the precise length of a string in pixels.
|
|
163
|
+
* @param {string} str - The string to be measured.
|
|
164
|
+
* @param {string} [fontFamily] - The font family of the string.
|
|
165
|
+
* @param {(string | number)} [fontSize] - The font size of the string.
|
|
166
|
+
* @returns {number} The precise length of the string in pixels.
|
|
167
|
+
*/
|
|
168
|
+
function getPreciseStringLengthPx(str, fontFamily, fontSize) {
|
|
169
|
+
const svgNS = 'http://www.w3.org/2000/svg';
|
|
170
|
+
const svg = document.createElementNS(svgNS, 'svg');
|
|
171
|
+
const text = document.createElementNS(svgNS, 'text');
|
|
172
|
+
text.textContent = str;
|
|
173
|
+
text.setAttribute('font-size', `${fontSize}`);
|
|
174
|
+
text.setAttribute('font-family', fontFamily);
|
|
175
|
+
svg.appendChild(text);
|
|
176
|
+
document.body.appendChild(svg);
|
|
177
|
+
const length = text.getComputedTextLength();
|
|
178
|
+
document.body.removeChild(svg);
|
|
179
|
+
return length;
|
|
186
180
|
}
|
|
187
|
-
|
|
181
|
+
/**
|
|
182
|
+
* Estimates the dimensions of an SVG text element.
|
|
183
|
+
*
|
|
184
|
+
* @export
|
|
185
|
+
* @param {Selection<SVGTextElement, any, SVGElement, any>} svgTextSelection - The D3 selection of the SVG text element.
|
|
186
|
+
* @param {number} fontSize - The font size.
|
|
187
|
+
* @param {number} [dy=0.32] - The line height scaling factor.
|
|
188
|
+
* @param {boolean} [fastMode=true] - Whether to use a fast estimation method or a more accurate one.
|
|
189
|
+
* @param {number} [fontWidthToHeightRatio] - The font width-to-height ratio.
|
|
190
|
+
* @returns {{width: number, height: number}} - The estimated dimensions of the text element.
|
|
191
|
+
*/
|
|
192
|
+
function estimateTextSize(svgTextSelection, fontSize, dy = 0.32, fastMode = true, fontWidthToHeightRatio) {
|
|
193
|
+
fontWidthToHeightRatio = fontWidthToHeightRatio || getFontWidthToHeightRatio();
|
|
188
194
|
const tspanSelection = svgTextSelection.selectAll('tspan');
|
|
189
195
|
const lines = tspanSelection.size() || 1;
|
|
190
196
|
const height = svgTextSelection.text() ? 0.85 * fontSize * lines * (1 + dy) - dy : 0;
|
|
191
197
|
let width = 0;
|
|
192
198
|
if (tspanSelection.empty()) {
|
|
193
199
|
const textLength = svgTextSelection.text().length;
|
|
194
|
-
width = fastMode ? fontSize * textLength *
|
|
200
|
+
width = fastMode ? fontSize * textLength * fontWidthToHeightRatio : svgTextSelection.node().getComputedTextLength();
|
|
195
201
|
}
|
|
196
202
|
else {
|
|
197
203
|
for (const tspan of tspanSelection.nodes()) {
|
|
198
204
|
const tspanTextLength = tspan.textContent.length;
|
|
199
|
-
const w = fastMode ? fontSize * tspanTextLength *
|
|
205
|
+
const w = fastMode ? fontSize * tspanTextLength * fontWidthToHeightRatio : tspan.getComputedTextLength();
|
|
200
206
|
if (w > width)
|
|
201
207
|
width = w;
|
|
202
208
|
}
|
|
203
209
|
}
|
|
204
210
|
return { width, height };
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Breaks a text block into lines based on the specified width.
|
|
214
|
+
*
|
|
215
|
+
* @param {UnovisText} textBlock - The text block to break into lines.
|
|
216
|
+
* @param {number | undefined} [width=undefined] - The maximum width of a line in pixels.
|
|
217
|
+
* @param {(number | undefined)} [height=undefined] - The height limit for the wrapped text in pixels.
|
|
218
|
+
* @param {boolean} [fastMode=true] - Whether to use a fast estimation method or a more accurate one.
|
|
219
|
+
* @param {string | string[]} [separator] - The word separators.
|
|
220
|
+
* @returns {string[]} - The text split into lines.
|
|
221
|
+
*/
|
|
222
|
+
function breakTextIntoLines(textBlock, width = undefined, fastMode = true, separator = UNOVIS_TEXT_SEPARATOR_DEFAULT, wordBreak = false) {
|
|
223
|
+
const text = `${textBlock.text}`;
|
|
224
|
+
if (!text)
|
|
225
|
+
return [];
|
|
226
|
+
const separators = Array.isArray(separator) ? separator : [separator];
|
|
227
|
+
const splitByNewLine = text.split('\n');
|
|
228
|
+
return splitByNewLine.map((str) => {
|
|
229
|
+
const lines = [];
|
|
230
|
+
if (!width)
|
|
231
|
+
return [str];
|
|
232
|
+
const words = splitString(str, separators);
|
|
233
|
+
let line = '';
|
|
234
|
+
for (let i = 0; i < words.length; i += 1) {
|
|
235
|
+
const textLengthPx = fastMode
|
|
236
|
+
? estimateStringPixelLength(line + words[i], textBlock.fontSize, textBlock.fontWidthToHeightRatio)
|
|
237
|
+
: getPreciseStringLengthPx(line + words[i], textBlock.fontFamily, textBlock.fontSize);
|
|
238
|
+
if (textLengthPx < width || i === 0) {
|
|
239
|
+
line += words[i];
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
lines.push(line.trim());
|
|
243
|
+
line = words[i];
|
|
244
|
+
}
|
|
245
|
+
// Word break functionality
|
|
246
|
+
if (wordBreak) {
|
|
247
|
+
while (line.length > 0) {
|
|
248
|
+
const subLineLengthPx = fastMode
|
|
249
|
+
? estimateStringPixelLength(line, textBlock.fontSize, textBlock.fontWidthToHeightRatio)
|
|
250
|
+
: getPreciseStringLengthPx(line, textBlock.fontFamily, textBlock.fontSize);
|
|
251
|
+
if (subLineLengthPx > width) {
|
|
252
|
+
let breakIndex = (line.trim()).length - 2; // Place at least 2 characters onto the next line
|
|
253
|
+
while (breakIndex > 0) {
|
|
254
|
+
const subLine = `${line.substring(0, breakIndex)}-`; // Use hyphen when force breaking words
|
|
255
|
+
const subLinePx = fastMode
|
|
256
|
+
? estimateStringPixelLength(subLine, textBlock.fontSize, textBlock.fontWidthToHeightRatio)
|
|
257
|
+
: getPreciseStringLengthPx(subLine, textBlock.fontFamily, textBlock.fontSize);
|
|
258
|
+
if (subLinePx <= width) {
|
|
259
|
+
lines.push(subLine.trim());
|
|
260
|
+
line = line.substring(breakIndex);
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
breakIndex--;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (i === words.length - 1)
|
|
272
|
+
lines.push(line.trim());
|
|
273
|
+
}
|
|
274
|
+
return lines;
|
|
275
|
+
}).flat();
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Wraps a text or array of texts to fit within specified width and height, if provided.
|
|
279
|
+
*
|
|
280
|
+
* @export
|
|
281
|
+
* @param {UnovisText | UnovisText[]} text - The text or array of texts to wrap.
|
|
282
|
+
* @param {number | undefined} [width=undefined] - The maximum width of a line in pixels.
|
|
283
|
+
* @param {boolean} [fastMode=true] - Whether to use a fast estimation method or a more accurate one.
|
|
284
|
+
* @param {string | string[]} [separator] - The word separators.
|
|
285
|
+
* @returns {UnovisWrappedText[]} - The wrapped texts.
|
|
286
|
+
*/
|
|
287
|
+
function getWrappedText(text, width = undefined, height = undefined, fastMode = true, separator = UNOVIS_TEXT_SEPARATOR_DEFAULT, wordBreak = false) {
|
|
288
|
+
// Merge input text with default values and convert it to an array if it's not already
|
|
289
|
+
const textArrays = Array.isArray(text) ? text.map(t => merge(UNOVIS_TEXT_DEFAULT, t)) : [merge(UNOVIS_TEXT_DEFAULT, text)];
|
|
290
|
+
// Break input text into lines based on width and separator
|
|
291
|
+
const textWrapped = textArrays.map(block => breakTextIntoLines(block, width, fastMode, separator, wordBreak));
|
|
292
|
+
const firstBlock = textArrays[0];
|
|
293
|
+
let h = -firstBlock.fontSize * (firstBlock.lineHeight - 1);
|
|
294
|
+
const blocks = [];
|
|
295
|
+
// Process each text block and its lines based on height limit
|
|
296
|
+
textArrays.forEach((text, i) => {
|
|
297
|
+
let lines = textWrapped[i];
|
|
298
|
+
const prevBlock = i > 0 ? blocks[i - 1] : undefined;
|
|
299
|
+
const prevBlockMarginBottomPx = prevBlock ? prevBlock.marginBottom : 0;
|
|
300
|
+
const marginTopPx = text.marginTop;
|
|
301
|
+
const effectiveMarginPx = Math.max(prevBlockMarginBottomPx, marginTopPx);
|
|
302
|
+
h += effectiveMarginPx;
|
|
303
|
+
const dh = text.fontSize * text.lineHeight;
|
|
304
|
+
// Iterate over lines and handle text overflow based on the height limit if provided
|
|
305
|
+
for (let k = 0; k < lines.length; k += 1) {
|
|
306
|
+
const line = lines[k];
|
|
307
|
+
h += dh;
|
|
308
|
+
if (height && (h + dh) > height && (k !== lines.length - 1)) {
|
|
309
|
+
const textLengthPx = fastMode
|
|
310
|
+
? estimateStringPixelLength(line, text.fontSize, text.fontWidthToHeightRatio)
|
|
311
|
+
: getPreciseStringLengthPx(line, text.fontFamily, text.fontSize);
|
|
312
|
+
if (textLengthPx < width) {
|
|
313
|
+
lines[k] += ' …';
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
lines[k] = `${lines[k].substr(0, lines[k].length - 2)}…`;
|
|
317
|
+
}
|
|
318
|
+
lines = lines.slice(0, k + 1);
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
// Create wrapped text block with its calculated properties
|
|
323
|
+
blocks.push(Object.assign(Object.assign({}, text), { _lines: lines, _estimatedHeight: h - ((prevBlock === null || prevBlock === void 0 ? void 0 : prevBlock._estimatedHeight) || 0) }));
|
|
324
|
+
});
|
|
325
|
+
return blocks;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Renders a text or array of texts to SVG tspan strings.
|
|
329
|
+
*
|
|
330
|
+
* @param {UnovisWrappedText[]} blocks - The wrapped text blocks.
|
|
331
|
+
* @param {number} [x=0] - The x-coordinate for the tspan elements.
|
|
332
|
+
* @param {number} [y] - The y-coordinate for the tspan elements.
|
|
333
|
+
* @returns {string[]} - The SVG tspan strings.
|
|
334
|
+
*/
|
|
335
|
+
function renderTextToTspanStrings(blocks, x = 0, y) {
|
|
336
|
+
return blocks.map((b, i) => {
|
|
337
|
+
const prevBlock = i > 0 ? blocks[i - 1] : undefined;
|
|
338
|
+
const prevBlockMarginBottomEm = prevBlock ? prevBlock.marginBottom / prevBlock.fontSize : 0;
|
|
339
|
+
const marginTopEm = b.marginTop / b.fontSize;
|
|
340
|
+
const marginEm = Math.max(prevBlockMarginBottomEm, marginTopEm);
|
|
341
|
+
const attributes = {
|
|
342
|
+
fontSize: b.fontSize,
|
|
343
|
+
fontFamily: b.fontFamily,
|
|
344
|
+
fill: b.color,
|
|
345
|
+
y: (i === 0) && y,
|
|
346
|
+
};
|
|
347
|
+
const attributesString = Object.entries(attributes)
|
|
348
|
+
.filter(([_, value]) => value)
|
|
349
|
+
.map(([key, value]) => `${kebabCase(key)}="${escape(value.toString())}"`)
|
|
350
|
+
.join(' ');
|
|
351
|
+
return `<tspan xmlns="http://www.w3.org/2000/svg" ${attributesString}>${b._lines.map((line, k) => {
|
|
352
|
+
let dy;
|
|
353
|
+
if (i === 0 && k === 0)
|
|
354
|
+
dy = 0.8 + marginEm;
|
|
355
|
+
else if (k === 0)
|
|
356
|
+
dy = marginEm + b.lineHeight;
|
|
357
|
+
else
|
|
358
|
+
dy = b.lineHeight;
|
|
359
|
+
return `<tspan x="${x}" dy="${dy}em">${line.length ? line : ' '}</tspan>`;
|
|
360
|
+
}).join('')}</tspan>`;
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Estimates the height of wrapped text blocks.
|
|
365
|
+
*
|
|
366
|
+
* @export
|
|
367
|
+
* @param {UnovisWrappedText[]} blocks - The wrapped text blocks.
|
|
368
|
+
* @returns {number} - The estimated height of the wrapped text blocks.
|
|
369
|
+
*/
|
|
370
|
+
function estimateWrappedTextHeight(blocks) {
|
|
371
|
+
return sum(blocks, b => b._estimatedHeight);
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Renders a text or array of texts to an SVG text element.
|
|
375
|
+
* Calling this function will replace the contents of the specified SVG text element.
|
|
376
|
+
*
|
|
377
|
+
* @export
|
|
378
|
+
* @param {SVGTextElement} textElement - The SVG text element to render the text into.
|
|
379
|
+
* @param {UnovisText | UnovisText[]} text - The text or array of texts to render.
|
|
380
|
+
* @param {UnovisTextOptions} options - The text options.
|
|
381
|
+
*/
|
|
382
|
+
function renderTextToSvgTextElement(textElement, text, options) {
|
|
383
|
+
const wrappedText = getWrappedText(text, options.width, undefined, options.fastMode, options.separator, options.wordBreak);
|
|
384
|
+
const textElementX = +textElement.getAttribute('x');
|
|
385
|
+
const textElementY = +textElement.getAttribute('y');
|
|
386
|
+
const x = textElementX !== null && textElementX !== void 0 ? textElementX : 0;
|
|
387
|
+
let y = textElementY !== null && textElementY !== void 0 ? textElementY : 0;
|
|
388
|
+
if (options.textAlign)
|
|
389
|
+
textElement.setAttribute('text-anchor', getTextAnchorFromTextAlign(options.textAlign));
|
|
390
|
+
if (options.verticalAlign && options.verticalAlign !== VerticalAlign.Top) {
|
|
391
|
+
const height = estimateWrappedTextHeight(wrappedText);
|
|
392
|
+
const dy = options.verticalAlign === VerticalAlign.Middle ? -height / 2
|
|
393
|
+
: options.verticalAlign === VerticalAlign.Bottom ? -height : 0;
|
|
394
|
+
y += dy;
|
|
395
|
+
}
|
|
396
|
+
const parser = new DOMParser();
|
|
397
|
+
textElement.textContent = '';
|
|
398
|
+
wrappedText.forEach(block => {
|
|
399
|
+
const svgCode = renderTextToTspanStrings([block], x, y).join('');
|
|
400
|
+
const parsedSvgCode = parser.parseFromString(svgCode, 'image/svg+xml').firstChild;
|
|
401
|
+
textElement.appendChild(parsedSvgCode);
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Renders a text or array of texts into a frame.
|
|
406
|
+
* Calling this function will replace the contents of the specified SVG group.
|
|
407
|
+
*
|
|
408
|
+
* @export
|
|
409
|
+
* @param {SVGGElement} group - The SVG group element to render the text into.
|
|
410
|
+
* @param {UnovisText | UnovisText[]} text - The text or array of texts to render.
|
|
411
|
+
* @param {UnovisTextFrameOptions} frameOptions - The text frame options.
|
|
412
|
+
*/
|
|
413
|
+
function renderTextIntoFrame(group, text, frameOptions) {
|
|
414
|
+
var _a, _b;
|
|
415
|
+
const wrappedText = getWrappedText(text, frameOptions.width, frameOptions.height, frameOptions.fastMode, frameOptions.separator, frameOptions.wordBreak);
|
|
416
|
+
const x = frameOptions.textAlign === TextAlign.Center ? frameOptions.width / 2
|
|
417
|
+
: frameOptions.textAlign === TextAlign.Right ? frameOptions.width : 0;
|
|
418
|
+
let y = 0;
|
|
419
|
+
const height = estimateWrappedTextHeight(wrappedText);
|
|
420
|
+
if (frameOptions.height && height < frameOptions.height) {
|
|
421
|
+
const height = estimateWrappedTextHeight(wrappedText);
|
|
422
|
+
const dh = frameOptions.height - height;
|
|
423
|
+
y = frameOptions.verticalAlign === VerticalAlign.Middle ? dh / 2
|
|
424
|
+
: frameOptions.verticalAlign === VerticalAlign.Bottom ? dh : 0;
|
|
425
|
+
}
|
|
426
|
+
const translate = (frameOptions.x || frameOptions.y)
|
|
427
|
+
? `transform="translate(${(_a = frameOptions.x) !== null && _a !== void 0 ? _a : 0},${(_b = frameOptions.y) !== null && _b !== void 0 ? _b : 0})"`
|
|
428
|
+
: '';
|
|
429
|
+
const svgCode = `<text
|
|
430
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
431
|
+
text-anchor="${getTextAnchorFromTextAlign(frameOptions.textAlign)}"
|
|
432
|
+
${translate}
|
|
433
|
+
>
|
|
434
|
+
${renderTextToTspanStrings(wrappedText, x, y).join('')}
|
|
435
|
+
</text>`;
|
|
436
|
+
const parser = new DOMParser();
|
|
437
|
+
const parsedSvgCode = parser.parseFromString(svgCode, 'image/svg+xml').firstChild;
|
|
438
|
+
group.textContent = '';
|
|
439
|
+
group.appendChild(parsedSvgCode);
|
|
205
440
|
}
|
|
206
441
|
|
|
207
|
-
export {
|
|
442
|
+
export { estimateStringPixelLength, estimateTextSize, estimateWrappedTextHeight, getPreciseStringLengthPx, getWrappedText, kebabCase, kebabCaseToCamel, renderTextIntoFrame, renderTextToSvgTextElement, splitString, trimSVGText, trimString, trimStringEnd, trimStringMiddle, trimStringStart, wrapSVGText };
|
|
208
443
|
//# sourceMappingURL=text.js.map
|