@dhis2/analytics 25.1.8 → 25.1.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/CHANGELOG.md +14 -0
- package/build/cjs/locales/es/translations.json +2 -2
- package/build/cjs/visualizations/config/generators/dhis/singleValue.js +104 -64
- package/build/es/locales/es/translations.json +2 -2
- package/build/es/visualizations/config/generators/dhis/singleValue.js +104 -64
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## [25.1.10](https://github.com/dhis2/analytics/compare/v25.1.9...v25.1.10) (2023-05-30)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* single value size and position issues (DHIS2-15344) ([#1470](https://github.com/dhis2/analytics/issues/1470)) ([d94fe02](https://github.com/dhis2/analytics/commit/d94fe02c7cc85a6b4aca41c85ba60ed37871b645))
|
|
7
|
+
|
|
8
|
+
## [25.1.9](https://github.com/dhis2/analytics/compare/v25.1.8...v25.1.9) (2023-05-30)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **translations:** sync translations from transifex (master) ([9466322](https://github.com/dhis2/analytics/commit/9466322c7cac471ee1d5bd1e451f911968e30a3d))
|
|
14
|
+
|
|
1
15
|
## [25.1.8](https://github.com/dhis2/analytics/compare/v25.1.7...v25.1.8) (2023-05-28)
|
|
2
16
|
|
|
3
17
|
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"About this line list": "",
|
|
21
21
|
"About this visualization": "",
|
|
22
22
|
"This app could not retrieve required data.": "",
|
|
23
|
-
"Network error": "",
|
|
23
|
+
"Network error": "Error de red",
|
|
24
24
|
"Data / Edit calculation": "",
|
|
25
25
|
"Data / New calculation": "",
|
|
26
26
|
"Remove item": "",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"Add a name to save this calculation": "",
|
|
34
34
|
"Save calculation": "",
|
|
35
35
|
"Are you sure you want to delete this calculation? It may be used by other visualizations.": "",
|
|
36
|
-
"Yes, delete": "",
|
|
36
|
+
"Yes, delete": "si, eliminar",
|
|
37
37
|
"Totals only": "",
|
|
38
38
|
"Details only": "",
|
|
39
39
|
"Loading": "Cargando",
|
|
@@ -11,14 +11,59 @@ var _fontStyle = require("../../../../modules/fontStyle.js");
|
|
|
11
11
|
|
|
12
12
|
var _legends = require("../../../../modules/legends.js");
|
|
13
13
|
|
|
14
|
-
const svgNS = 'http://www.w3.org/2000/svg'; //
|
|
14
|
+
const svgNS = 'http://www.w3.org/2000/svg'; // multiply text width with this factor
|
|
15
|
+
// to get very close to actual text width
|
|
16
|
+
// nb: dependent on viewbox etc
|
|
17
|
+
|
|
18
|
+
const ACTUAL_TEXT_WIDTH_FACTOR = 0.9; // multiply value text size with this factor
|
|
19
|
+
// to get very close to the actual number height
|
|
20
|
+
// as numbers don't go below the baseline like e.g. "j" and "g"
|
|
21
|
+
|
|
22
|
+
const ACTUAL_NUMBER_HEIGHT_FACTOR = 0.67; // do not allow text width to exceed this threshold
|
|
23
|
+
// a threshold >1 does not really make sense but text width vs viewbox is complicated
|
|
24
|
+
|
|
25
|
+
const TEXT_WIDTH_CONTAINER_WIDTH_FACTOR = 1.3; // do not allow text size to exceed this
|
|
26
|
+
|
|
27
|
+
const TEXT_SIZE_CONTAINER_HEIGHT_FACTOR = 0.6;
|
|
28
|
+
const TEXT_SIZE_MAX_THRESHOLD = 400; // multiply text size with this factor
|
|
29
|
+
// to get an appropriate letter spacing
|
|
30
|
+
|
|
31
|
+
const LETTER_SPACING_TEXT_SIZE_FACTOR = 1 / 35 * -1;
|
|
32
|
+
const LETTER_SPACING_MIN_THRESHOLD = -6;
|
|
33
|
+
const LETTER_SPACING_MAX_THRESHOLD = -1; // fixed top margin above title/subtitle
|
|
34
|
+
|
|
35
|
+
const TOP_MARGIN_FIXED = 16; // multiply text size with this factor
|
|
36
|
+
// to get an appropriate sub text size
|
|
37
|
+
|
|
38
|
+
const SUB_TEXT_SIZE_FACTOR = 0.5;
|
|
39
|
+
const SUB_TEXT_SIZE_MIN_THRESHOLD = 26;
|
|
40
|
+
const SUB_TEXT_SIZE_MAX_THRESHOLD = 40; // multiply text size with this factor
|
|
41
|
+
// to get an appropriate icon padding
|
|
42
|
+
|
|
43
|
+
const ICON_PADDING_FACTOR = 0.3; // Compute text width before rendering
|
|
15
44
|
// Not exactly precise but close enough
|
|
16
45
|
|
|
17
46
|
const getTextWidth = (text, font) => {
|
|
18
47
|
const canvas = document.createElement('canvas');
|
|
19
48
|
const context = canvas.getContext('2d');
|
|
20
49
|
context.font = font;
|
|
21
|
-
return context.measureText(text).width;
|
|
50
|
+
return Math.round(context.measureText(text).width * ACTUAL_TEXT_WIDTH_FACTOR);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const getTextHeightForNumbers = textSize => textSize * ACTUAL_NUMBER_HEIGHT_FACTOR;
|
|
54
|
+
|
|
55
|
+
const getIconPadding = textSize => Math.round(textSize * ICON_PADDING_FACTOR);
|
|
56
|
+
|
|
57
|
+
const getTextSize = (formattedValue, containerWidth, containerHeight, showIcon) => {
|
|
58
|
+
let size = Math.min(Math.round(containerHeight * TEXT_SIZE_CONTAINER_HEIGHT_FACTOR), TEXT_SIZE_MAX_THRESHOLD);
|
|
59
|
+
const widthThreshold = Math.round(containerWidth * TEXT_WIDTH_CONTAINER_WIDTH_FACTOR);
|
|
60
|
+
const textWidth = getTextWidth(formattedValue, "".concat(size, "px Roboto")) + (showIcon ? getIconPadding(size) : 0);
|
|
61
|
+
|
|
62
|
+
if (textWidth > widthThreshold) {
|
|
63
|
+
size = Math.round(size * (widthThreshold / textWidth));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return size;
|
|
22
67
|
};
|
|
23
68
|
|
|
24
69
|
const generateValueSVG = _ref => {
|
|
@@ -30,26 +75,18 @@ const generateValueSVG = _ref => {
|
|
|
30
75
|
icon,
|
|
31
76
|
noData,
|
|
32
77
|
containerWidth,
|
|
33
|
-
containerHeight
|
|
78
|
+
containerHeight,
|
|
79
|
+
topMargin = 0
|
|
34
80
|
} = _ref;
|
|
35
|
-
const ratio = containerHeight / containerWidth;
|
|
36
|
-
const iconSize = 300;
|
|
37
|
-
const iconPadding = 50;
|
|
38
|
-
const textSize = iconSize * 0.85;
|
|
39
|
-
const textWidth = getTextWidth(formattedValue, "".concat(textSize, "px Roboto"));
|
|
40
|
-
const subTextSize = 40;
|
|
41
81
|
const showIcon = icon && formattedValue !== noData.text;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const viewBoxHeight = viewBoxWidth * ratio;
|
|
82
|
+
const textSize = getTextSize(formattedValue, containerWidth, containerHeight, showIcon);
|
|
83
|
+
const textWidth = getTextWidth(formattedValue, "".concat(textSize, "px Roboto"));
|
|
84
|
+
const iconSize = textSize;
|
|
85
|
+
const subTextSize = textSize * SUB_TEXT_SIZE_FACTOR > SUB_TEXT_SIZE_MAX_THRESHOLD ? SUB_TEXT_SIZE_MAX_THRESHOLD : textSize * SUB_TEXT_SIZE_FACTOR < SUB_TEXT_SIZE_MIN_THRESHOLD ? SUB_TEXT_SIZE_MIN_THRESHOLD : textSize * SUB_TEXT_SIZE_FACTOR;
|
|
49
86
|
const svgValue = document.createElementNS(svgNS, 'svg');
|
|
50
|
-
svgValue.setAttribute('viewBox', "0 0 ".concat(
|
|
51
|
-
svgValue.setAttribute('width', '
|
|
52
|
-
svgValue.setAttribute('height', '
|
|
87
|
+
svgValue.setAttribute('viewBox', "0 0 ".concat(containerWidth, " ").concat(containerHeight));
|
|
88
|
+
svgValue.setAttribute('width', '50%');
|
|
89
|
+
svgValue.setAttribute('height', '50%');
|
|
53
90
|
svgValue.setAttribute('x', '50%');
|
|
54
91
|
svgValue.setAttribute('y', '50%');
|
|
55
92
|
svgValue.setAttribute('style', 'overflow: visible');
|
|
@@ -66,11 +103,11 @@ const generateValueSVG = _ref => {
|
|
|
66
103
|
// embed icon to allow changing color
|
|
67
104
|
// (elements with fill need to use "currentColor" for this to work)
|
|
68
105
|
const iconSvgNode = document.createElementNS(svgNS, 'svg');
|
|
106
|
+
iconSvgNode.setAttribute('viewBox', '0 0 48 48');
|
|
69
107
|
iconSvgNode.setAttribute('width', iconSize);
|
|
70
108
|
iconSvgNode.setAttribute('height', iconSize);
|
|
71
|
-
iconSvgNode.setAttribute('
|
|
72
|
-
iconSvgNode.setAttribute('
|
|
73
|
-
iconSvgNode.setAttribute('x', "-".concat((iconSize + iconPadding + textWidth) / 2));
|
|
109
|
+
iconSvgNode.setAttribute('y', (iconSize / 2 - topMargin / 2) * -1);
|
|
110
|
+
iconSvgNode.setAttribute('x', "-".concat((iconSize + getIconPadding(textSize) + textWidth) / 2));
|
|
74
111
|
iconSvgNode.setAttribute('style', "color: ".concat(fillColor));
|
|
75
112
|
const parser = new DOMParser();
|
|
76
113
|
const svgIconDocument = parser.parseFromString(icon, 'image/svg+xml');
|
|
@@ -78,14 +115,14 @@ const generateValueSVG = _ref => {
|
|
|
78
115
|
svgValue.appendChild(iconSvgNode);
|
|
79
116
|
}
|
|
80
117
|
|
|
118
|
+
const letterSpacing = Math.round(textSize * LETTER_SPACING_TEXT_SIZE_FACTOR);
|
|
81
119
|
const textNode = document.createElementNS(svgNS, 'text');
|
|
82
120
|
textNode.setAttribute('font-size', textSize);
|
|
83
121
|
textNode.setAttribute('font-weight', '300');
|
|
84
|
-
textNode.setAttribute('letter-spacing',
|
|
122
|
+
textNode.setAttribute('letter-spacing', letterSpacing < LETTER_SPACING_MIN_THRESHOLD ? LETTER_SPACING_MIN_THRESHOLD : letterSpacing > LETTER_SPACING_MAX_THRESHOLD ? LETTER_SPACING_MAX_THRESHOLD : letterSpacing);
|
|
85
123
|
textNode.setAttribute('text-anchor', 'middle');
|
|
86
|
-
textNode.setAttribute('x', showIcon ? "".concat((iconSize +
|
|
87
|
-
|
|
88
|
-
textNode.setAttribute('y', '.35em');
|
|
124
|
+
textNode.setAttribute('x', showIcon ? "".concat((iconSize + getIconPadding(textSize)) / 2) : 0);
|
|
125
|
+
textNode.setAttribute('y', topMargin / 2 + getTextHeightForNumbers(textSize) / 2);
|
|
89
126
|
textNode.setAttribute('fill', fillColor);
|
|
90
127
|
textNode.setAttribute('data-test', 'visualization-primary-value');
|
|
91
128
|
textNode.appendChild(document.createTextNode(formattedValue));
|
|
@@ -95,8 +132,8 @@ const generateValueSVG = _ref => {
|
|
|
95
132
|
const subTextNode = document.createElementNS(svgNS, 'text');
|
|
96
133
|
subTextNode.setAttribute('text-anchor', 'middle');
|
|
97
134
|
subTextNode.setAttribute('font-size', subTextSize);
|
|
98
|
-
subTextNode.setAttribute('y', iconSize / 2);
|
|
99
|
-
subTextNode.setAttribute('dy', subTextSize);
|
|
135
|
+
subTextNode.setAttribute('y', iconSize / 2 + topMargin / 2);
|
|
136
|
+
subTextNode.setAttribute('dy', subTextSize * 1.7);
|
|
100
137
|
subTextNode.setAttribute('fill', textColor);
|
|
101
138
|
subTextNode.appendChild(document.createTextNode(subText));
|
|
102
139
|
svgValue.appendChild(subTextNode);
|
|
@@ -198,47 +235,49 @@ const generateDVItem = (config, _ref3) => {
|
|
|
198
235
|
svgContainer.appendChild(background);
|
|
199
236
|
}
|
|
200
237
|
|
|
201
|
-
const svgWrapper = document.createElementNS(svgNS, 'svg');
|
|
238
|
+
const svgWrapper = document.createElementNS(svgNS, 'svg'); // title
|
|
239
|
+
|
|
202
240
|
const title = document.createElementNS(svgNS, 'text');
|
|
203
241
|
const titleFontStyle = (0, _fontStyle.mergeFontStyleWithDefault)(fontStyle && fontStyle[_fontStyle.FONT_STYLE_VISUALIZATION_TITLE], _fontStyle.FONT_STYLE_VISUALIZATION_TITLE);
|
|
204
|
-
const titleYPosition = titleFontStyle[_fontStyle.FONT_STYLE_OPTION_FONT_SIZE];
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
242
|
+
const titleYPosition = TOP_MARGIN_FIXED + parseInt(titleFontStyle[_fontStyle.FONT_STYLE_OPTION_FONT_SIZE]) + 'px';
|
|
243
|
+
const titleAttributes = {
|
|
244
|
+
x: getXFromTextAlign(titleFontStyle[_fontStyle.FONT_STYLE_OPTION_TEXT_ALIGN]),
|
|
245
|
+
y: titleYPosition,
|
|
246
|
+
'text-anchor': getTextAnchorFromTextAlign(titleFontStyle[_fontStyle.FONT_STYLE_OPTION_TEXT_ALIGN]),
|
|
247
|
+
'font-size': "".concat(titleFontStyle[_fontStyle.FONT_STYLE_OPTION_FONT_SIZE], "px"),
|
|
248
|
+
'font-weight': titleFontStyle[_fontStyle.FONT_STYLE_OPTION_BOLD] ? _fontStyle.FONT_STYLE_OPTION_BOLD : 'normal',
|
|
249
|
+
'font-style': titleFontStyle[_fontStyle.FONT_STYLE_OPTION_ITALIC] ? _fontStyle.FONT_STYLE_OPTION_ITALIC : 'normal',
|
|
250
|
+
'data-test': 'visualization-title',
|
|
251
|
+
fill: titleColor && titleFontStyle[_fontStyle.FONT_STYLE_OPTION_TEXT_COLOR] === _fontStyle.defaultFontStyle[_fontStyle.FONT_STYLE_VISUALIZATION_TITLE][_fontStyle.FONT_STYLE_OPTION_TEXT_COLOR] ? titleColor : titleFontStyle[_fontStyle.FONT_STYLE_OPTION_TEXT_COLOR]
|
|
252
|
+
};
|
|
253
|
+
Object.entries(titleAttributes).forEach(_ref4 => {
|
|
254
|
+
let [key, value] = _ref4;
|
|
255
|
+
return title.setAttribute(key, value);
|
|
256
|
+
});
|
|
219
257
|
|
|
220
258
|
if (config.title) {
|
|
221
259
|
title.appendChild(document.createTextNode(config.title));
|
|
222
260
|
svgWrapper.appendChild(title);
|
|
223
|
-
}
|
|
261
|
+
} // subtitle
|
|
224
262
|
|
|
225
|
-
const subtitleFontStyle = (0, _fontStyle.mergeFontStyleWithDefault)(fontStyle && fontStyle[_fontStyle.FONT_STYLE_VISUALIZATION_SUBTITLE], _fontStyle.FONT_STYLE_VISUALIZATION_SUBTITLE);
|
|
226
|
-
const subtitle = document.createElementNS(svgNS, 'text');
|
|
227
|
-
subtitle.setAttribute('x', getXFromTextAlign(subtitleFontStyle[_fontStyle.FONT_STYLE_OPTION_TEXT_ALIGN]));
|
|
228
|
-
subtitle.setAttribute('y', titleYPosition);
|
|
229
|
-
subtitle.setAttribute('dy', "".concat(subtitleFontStyle[_fontStyle.FONT_STYLE_OPTION_FONT_SIZE] + 10));
|
|
230
|
-
subtitle.setAttribute('text-anchor', getTextAnchorFromTextAlign(subtitleFontStyle[_fontStyle.FONT_STYLE_OPTION_TEXT_ALIGN]));
|
|
231
|
-
subtitle.setAttribute('font-size', "".concat(subtitleFontStyle[_fontStyle.FONT_STYLE_OPTION_FONT_SIZE], "px"));
|
|
232
|
-
subtitle.setAttribute('font-weight', subtitleFontStyle[_fontStyle.FONT_STYLE_OPTION_BOLD] ? _fontStyle.FONT_STYLE_OPTION_BOLD : 'normal');
|
|
233
|
-
subtitle.setAttribute('font-style', subtitleFontStyle[_fontStyle.FONT_STYLE_OPTION_ITALIC] ? _fontStyle.FONT_STYLE_OPTION_ITALIC : 'normal');
|
|
234
|
-
|
|
235
|
-
if (titleColor && subtitleFontStyle[_fontStyle.FONT_STYLE_OPTION_TEXT_COLOR] === _fontStyle.defaultFontStyle[_fontStyle.FONT_STYLE_VISUALIZATION_SUBTITLE][_fontStyle.FONT_STYLE_OPTION_TEXT_COLOR]) {
|
|
236
|
-
subtitle.setAttribute('fill', titleColor);
|
|
237
|
-
} else {
|
|
238
|
-
subtitle.setAttribute('fill', subtitleFontStyle[_fontStyle.FONT_STYLE_OPTION_TEXT_COLOR]);
|
|
239
|
-
}
|
|
240
263
|
|
|
241
|
-
subtitle.
|
|
264
|
+
const subtitle = document.createElementNS(svgNS, 'text');
|
|
265
|
+
const subtitleFontStyle = (0, _fontStyle.mergeFontStyleWithDefault)(fontStyle && fontStyle[_fontStyle.FONT_STYLE_VISUALIZATION_SUBTITLE], _fontStyle.FONT_STYLE_VISUALIZATION_SUBTITLE);
|
|
266
|
+
const subtitleAttributes = {
|
|
267
|
+
x: getXFromTextAlign(subtitleFontStyle[_fontStyle.FONT_STYLE_OPTION_TEXT_ALIGN]),
|
|
268
|
+
y: titleYPosition,
|
|
269
|
+
dy: "".concat(subtitleFontStyle[_fontStyle.FONT_STYLE_OPTION_FONT_SIZE] + 10),
|
|
270
|
+
'text-anchor': getTextAnchorFromTextAlign(subtitleFontStyle[_fontStyle.FONT_STYLE_OPTION_TEXT_ALIGN]),
|
|
271
|
+
'font-size': "".concat(subtitleFontStyle[_fontStyle.FONT_STYLE_OPTION_FONT_SIZE], "px"),
|
|
272
|
+
'font-weight': subtitleFontStyle[_fontStyle.FONT_STYLE_OPTION_BOLD] ? _fontStyle.FONT_STYLE_OPTION_BOLD : 'normal',
|
|
273
|
+
'font-style': subtitleFontStyle[_fontStyle.FONT_STYLE_OPTION_ITALIC] ? _fontStyle.FONT_STYLE_OPTION_ITALIC : 'normal',
|
|
274
|
+
fill: titleColor && subtitleFontStyle[_fontStyle.FONT_STYLE_OPTION_TEXT_COLOR] === _fontStyle.defaultFontStyle[_fontStyle.FONT_STYLE_VISUALIZATION_SUBTITLE][_fontStyle.FONT_STYLE_OPTION_TEXT_COLOR] ? titleColor : subtitleFontStyle[_fontStyle.FONT_STYLE_OPTION_TEXT_COLOR],
|
|
275
|
+
'data-test': 'visualization-subtitle'
|
|
276
|
+
};
|
|
277
|
+
Object.entries(subtitleAttributes).forEach(_ref5 => {
|
|
278
|
+
let [key, value] = _ref5;
|
|
279
|
+
return subtitle.setAttribute(key, value);
|
|
280
|
+
});
|
|
242
281
|
|
|
243
282
|
if (config.subtitle) {
|
|
244
283
|
subtitle.appendChild(document.createTextNode(config.subtitle));
|
|
@@ -254,7 +293,8 @@ const generateDVItem = (config, _ref3) => {
|
|
|
254
293
|
noData,
|
|
255
294
|
icon,
|
|
256
295
|
containerWidth: width,
|
|
257
|
-
containerHeight: height
|
|
296
|
+
containerHeight: height,
|
|
297
|
+
topMargin: TOP_MARGIN_FIXED + ((config.title ? parseInt(title.getAttribute('font-size')) : 0) + (config.subtitle ? parseInt(subtitle.getAttribute('font-size')) : 0)) * 2.5
|
|
258
298
|
}));
|
|
259
299
|
return svgContainer;
|
|
260
300
|
};
|
|
@@ -281,7 +321,7 @@ const shouldUseContrastColor = function () {
|
|
|
281
321
|
return L <= 0.179;
|
|
282
322
|
};
|
|
283
323
|
|
|
284
|
-
function _default(config, parentEl,
|
|
324
|
+
function _default(config, parentEl, _ref6) {
|
|
285
325
|
let {
|
|
286
326
|
dashboard,
|
|
287
327
|
legendSets,
|
|
@@ -289,7 +329,7 @@ function _default(config, parentEl, _ref4) {
|
|
|
289
329
|
noData,
|
|
290
330
|
legendOptions,
|
|
291
331
|
icon
|
|
292
|
-
} =
|
|
332
|
+
} = _ref6;
|
|
293
333
|
const legendSet = legendOptions && legendSets[0];
|
|
294
334
|
const legendColor = legendSet && (0, _legends.getColorByValueFromLegendSet)(legendSet, config.value);
|
|
295
335
|
let valueColor, titleColor, backgroundColor;
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"About this line list": "",
|
|
21
21
|
"About this visualization": "",
|
|
22
22
|
"This app could not retrieve required data.": "",
|
|
23
|
-
"Network error": "",
|
|
23
|
+
"Network error": "Error de red",
|
|
24
24
|
"Data / Edit calculation": "",
|
|
25
25
|
"Data / New calculation": "",
|
|
26
26
|
"Remove item": "",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"Add a name to save this calculation": "",
|
|
34
34
|
"Save calculation": "",
|
|
35
35
|
"Are you sure you want to delete this calculation? It may be used by other visualizations.": "",
|
|
36
|
-
"Yes, delete": "",
|
|
36
|
+
"Yes, delete": "si, eliminar",
|
|
37
37
|
"Totals only": "",
|
|
38
38
|
"Details only": "",
|
|
39
39
|
"Loading": "Cargando",
|
|
@@ -1,14 +1,59 @@
|
|
|
1
1
|
import { colors } from '@dhis2/ui';
|
|
2
2
|
import { FONT_STYLE_VISUALIZATION_TITLE, FONT_STYLE_VISUALIZATION_SUBTITLE, FONT_STYLE_OPTION_FONT_SIZE, FONT_STYLE_OPTION_TEXT_COLOR, FONT_STYLE_OPTION_TEXT_ALIGN, FONT_STYLE_OPTION_ITALIC, FONT_STYLE_OPTION_BOLD, TEXT_ALIGN_LEFT, TEXT_ALIGN_RIGHT, TEXT_ALIGN_CENTER, mergeFontStyleWithDefault, defaultFontStyle } from '../../../../modules/fontStyle.js';
|
|
3
3
|
import { getColorByValueFromLegendSet, LEGEND_DISPLAY_STYLE_FILL } from '../../../../modules/legends.js';
|
|
4
|
-
const svgNS = 'http://www.w3.org/2000/svg'; //
|
|
4
|
+
const svgNS = 'http://www.w3.org/2000/svg'; // multiply text width with this factor
|
|
5
|
+
// to get very close to actual text width
|
|
6
|
+
// nb: dependent on viewbox etc
|
|
7
|
+
|
|
8
|
+
const ACTUAL_TEXT_WIDTH_FACTOR = 0.9; // multiply value text size with this factor
|
|
9
|
+
// to get very close to the actual number height
|
|
10
|
+
// as numbers don't go below the baseline like e.g. "j" and "g"
|
|
11
|
+
|
|
12
|
+
const ACTUAL_NUMBER_HEIGHT_FACTOR = 0.67; // do not allow text width to exceed this threshold
|
|
13
|
+
// a threshold >1 does not really make sense but text width vs viewbox is complicated
|
|
14
|
+
|
|
15
|
+
const TEXT_WIDTH_CONTAINER_WIDTH_FACTOR = 1.3; // do not allow text size to exceed this
|
|
16
|
+
|
|
17
|
+
const TEXT_SIZE_CONTAINER_HEIGHT_FACTOR = 0.6;
|
|
18
|
+
const TEXT_SIZE_MAX_THRESHOLD = 400; // multiply text size with this factor
|
|
19
|
+
// to get an appropriate letter spacing
|
|
20
|
+
|
|
21
|
+
const LETTER_SPACING_TEXT_SIZE_FACTOR = 1 / 35 * -1;
|
|
22
|
+
const LETTER_SPACING_MIN_THRESHOLD = -6;
|
|
23
|
+
const LETTER_SPACING_MAX_THRESHOLD = -1; // fixed top margin above title/subtitle
|
|
24
|
+
|
|
25
|
+
const TOP_MARGIN_FIXED = 16; // multiply text size with this factor
|
|
26
|
+
// to get an appropriate sub text size
|
|
27
|
+
|
|
28
|
+
const SUB_TEXT_SIZE_FACTOR = 0.5;
|
|
29
|
+
const SUB_TEXT_SIZE_MIN_THRESHOLD = 26;
|
|
30
|
+
const SUB_TEXT_SIZE_MAX_THRESHOLD = 40; // multiply text size with this factor
|
|
31
|
+
// to get an appropriate icon padding
|
|
32
|
+
|
|
33
|
+
const ICON_PADDING_FACTOR = 0.3; // Compute text width before rendering
|
|
5
34
|
// Not exactly precise but close enough
|
|
6
35
|
|
|
7
36
|
const getTextWidth = (text, font) => {
|
|
8
37
|
const canvas = document.createElement('canvas');
|
|
9
38
|
const context = canvas.getContext('2d');
|
|
10
39
|
context.font = font;
|
|
11
|
-
return context.measureText(text).width;
|
|
40
|
+
return Math.round(context.measureText(text).width * ACTUAL_TEXT_WIDTH_FACTOR);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const getTextHeightForNumbers = textSize => textSize * ACTUAL_NUMBER_HEIGHT_FACTOR;
|
|
44
|
+
|
|
45
|
+
const getIconPadding = textSize => Math.round(textSize * ICON_PADDING_FACTOR);
|
|
46
|
+
|
|
47
|
+
const getTextSize = (formattedValue, containerWidth, containerHeight, showIcon) => {
|
|
48
|
+
let size = Math.min(Math.round(containerHeight * TEXT_SIZE_CONTAINER_HEIGHT_FACTOR), TEXT_SIZE_MAX_THRESHOLD);
|
|
49
|
+
const widthThreshold = Math.round(containerWidth * TEXT_WIDTH_CONTAINER_WIDTH_FACTOR);
|
|
50
|
+
const textWidth = getTextWidth(formattedValue, "".concat(size, "px Roboto")) + (showIcon ? getIconPadding(size) : 0);
|
|
51
|
+
|
|
52
|
+
if (textWidth > widthThreshold) {
|
|
53
|
+
size = Math.round(size * (widthThreshold / textWidth));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return size;
|
|
12
57
|
};
|
|
13
58
|
|
|
14
59
|
const generateValueSVG = _ref => {
|
|
@@ -20,26 +65,18 @@ const generateValueSVG = _ref => {
|
|
|
20
65
|
icon,
|
|
21
66
|
noData,
|
|
22
67
|
containerWidth,
|
|
23
|
-
containerHeight
|
|
68
|
+
containerHeight,
|
|
69
|
+
topMargin = 0
|
|
24
70
|
} = _ref;
|
|
25
|
-
const ratio = containerHeight / containerWidth;
|
|
26
|
-
const iconSize = 300;
|
|
27
|
-
const iconPadding = 50;
|
|
28
|
-
const textSize = iconSize * 0.85;
|
|
29
|
-
const textWidth = getTextWidth(formattedValue, "".concat(textSize, "px Roboto"));
|
|
30
|
-
const subTextSize = 40;
|
|
31
71
|
const showIcon = icon && formattedValue !== noData.text;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const viewBoxHeight = viewBoxWidth * ratio;
|
|
72
|
+
const textSize = getTextSize(formattedValue, containerWidth, containerHeight, showIcon);
|
|
73
|
+
const textWidth = getTextWidth(formattedValue, "".concat(textSize, "px Roboto"));
|
|
74
|
+
const iconSize = textSize;
|
|
75
|
+
const subTextSize = textSize * SUB_TEXT_SIZE_FACTOR > SUB_TEXT_SIZE_MAX_THRESHOLD ? SUB_TEXT_SIZE_MAX_THRESHOLD : textSize * SUB_TEXT_SIZE_FACTOR < SUB_TEXT_SIZE_MIN_THRESHOLD ? SUB_TEXT_SIZE_MIN_THRESHOLD : textSize * SUB_TEXT_SIZE_FACTOR;
|
|
39
76
|
const svgValue = document.createElementNS(svgNS, 'svg');
|
|
40
|
-
svgValue.setAttribute('viewBox', "0 0 ".concat(
|
|
41
|
-
svgValue.setAttribute('width', '
|
|
42
|
-
svgValue.setAttribute('height', '
|
|
77
|
+
svgValue.setAttribute('viewBox', "0 0 ".concat(containerWidth, " ").concat(containerHeight));
|
|
78
|
+
svgValue.setAttribute('width', '50%');
|
|
79
|
+
svgValue.setAttribute('height', '50%');
|
|
43
80
|
svgValue.setAttribute('x', '50%');
|
|
44
81
|
svgValue.setAttribute('y', '50%');
|
|
45
82
|
svgValue.setAttribute('style', 'overflow: visible');
|
|
@@ -56,11 +93,11 @@ const generateValueSVG = _ref => {
|
|
|
56
93
|
// embed icon to allow changing color
|
|
57
94
|
// (elements with fill need to use "currentColor" for this to work)
|
|
58
95
|
const iconSvgNode = document.createElementNS(svgNS, 'svg');
|
|
96
|
+
iconSvgNode.setAttribute('viewBox', '0 0 48 48');
|
|
59
97
|
iconSvgNode.setAttribute('width', iconSize);
|
|
60
98
|
iconSvgNode.setAttribute('height', iconSize);
|
|
61
|
-
iconSvgNode.setAttribute('
|
|
62
|
-
iconSvgNode.setAttribute('
|
|
63
|
-
iconSvgNode.setAttribute('x', "-".concat((iconSize + iconPadding + textWidth) / 2));
|
|
99
|
+
iconSvgNode.setAttribute('y', (iconSize / 2 - topMargin / 2) * -1);
|
|
100
|
+
iconSvgNode.setAttribute('x', "-".concat((iconSize + getIconPadding(textSize) + textWidth) / 2));
|
|
64
101
|
iconSvgNode.setAttribute('style', "color: ".concat(fillColor));
|
|
65
102
|
const parser = new DOMParser();
|
|
66
103
|
const svgIconDocument = parser.parseFromString(icon, 'image/svg+xml');
|
|
@@ -68,14 +105,14 @@ const generateValueSVG = _ref => {
|
|
|
68
105
|
svgValue.appendChild(iconSvgNode);
|
|
69
106
|
}
|
|
70
107
|
|
|
108
|
+
const letterSpacing = Math.round(textSize * LETTER_SPACING_TEXT_SIZE_FACTOR);
|
|
71
109
|
const textNode = document.createElementNS(svgNS, 'text');
|
|
72
110
|
textNode.setAttribute('font-size', textSize);
|
|
73
111
|
textNode.setAttribute('font-weight', '300');
|
|
74
|
-
textNode.setAttribute('letter-spacing',
|
|
112
|
+
textNode.setAttribute('letter-spacing', letterSpacing < LETTER_SPACING_MIN_THRESHOLD ? LETTER_SPACING_MIN_THRESHOLD : letterSpacing > LETTER_SPACING_MAX_THRESHOLD ? LETTER_SPACING_MAX_THRESHOLD : letterSpacing);
|
|
75
113
|
textNode.setAttribute('text-anchor', 'middle');
|
|
76
|
-
textNode.setAttribute('x', showIcon ? "".concat((iconSize +
|
|
77
|
-
|
|
78
|
-
textNode.setAttribute('y', '.35em');
|
|
114
|
+
textNode.setAttribute('x', showIcon ? "".concat((iconSize + getIconPadding(textSize)) / 2) : 0);
|
|
115
|
+
textNode.setAttribute('y', topMargin / 2 + getTextHeightForNumbers(textSize) / 2);
|
|
79
116
|
textNode.setAttribute('fill', fillColor);
|
|
80
117
|
textNode.setAttribute('data-test', 'visualization-primary-value');
|
|
81
118
|
textNode.appendChild(document.createTextNode(formattedValue));
|
|
@@ -85,8 +122,8 @@ const generateValueSVG = _ref => {
|
|
|
85
122
|
const subTextNode = document.createElementNS(svgNS, 'text');
|
|
86
123
|
subTextNode.setAttribute('text-anchor', 'middle');
|
|
87
124
|
subTextNode.setAttribute('font-size', subTextSize);
|
|
88
|
-
subTextNode.setAttribute('y', iconSize / 2);
|
|
89
|
-
subTextNode.setAttribute('dy', subTextSize);
|
|
125
|
+
subTextNode.setAttribute('y', iconSize / 2 + topMargin / 2);
|
|
126
|
+
subTextNode.setAttribute('dy', subTextSize * 1.7);
|
|
90
127
|
subTextNode.setAttribute('fill', textColor);
|
|
91
128
|
subTextNode.appendChild(document.createTextNode(subText));
|
|
92
129
|
svgValue.appendChild(subTextNode);
|
|
@@ -188,47 +225,49 @@ const generateDVItem = (config, _ref3) => {
|
|
|
188
225
|
svgContainer.appendChild(background);
|
|
189
226
|
}
|
|
190
227
|
|
|
191
|
-
const svgWrapper = document.createElementNS(svgNS, 'svg');
|
|
228
|
+
const svgWrapper = document.createElementNS(svgNS, 'svg'); // title
|
|
229
|
+
|
|
192
230
|
const title = document.createElementNS(svgNS, 'text');
|
|
193
231
|
const titleFontStyle = mergeFontStyleWithDefault(fontStyle && fontStyle[FONT_STYLE_VISUALIZATION_TITLE], FONT_STYLE_VISUALIZATION_TITLE);
|
|
194
|
-
const titleYPosition = titleFontStyle[FONT_STYLE_OPTION_FONT_SIZE];
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
232
|
+
const titleYPosition = TOP_MARGIN_FIXED + parseInt(titleFontStyle[FONT_STYLE_OPTION_FONT_SIZE]) + 'px';
|
|
233
|
+
const titleAttributes = {
|
|
234
|
+
x: getXFromTextAlign(titleFontStyle[FONT_STYLE_OPTION_TEXT_ALIGN]),
|
|
235
|
+
y: titleYPosition,
|
|
236
|
+
'text-anchor': getTextAnchorFromTextAlign(titleFontStyle[FONT_STYLE_OPTION_TEXT_ALIGN]),
|
|
237
|
+
'font-size': "".concat(titleFontStyle[FONT_STYLE_OPTION_FONT_SIZE], "px"),
|
|
238
|
+
'font-weight': titleFontStyle[FONT_STYLE_OPTION_BOLD] ? FONT_STYLE_OPTION_BOLD : 'normal',
|
|
239
|
+
'font-style': titleFontStyle[FONT_STYLE_OPTION_ITALIC] ? FONT_STYLE_OPTION_ITALIC : 'normal',
|
|
240
|
+
'data-test': 'visualization-title',
|
|
241
|
+
fill: titleColor && titleFontStyle[FONT_STYLE_OPTION_TEXT_COLOR] === defaultFontStyle[FONT_STYLE_VISUALIZATION_TITLE][FONT_STYLE_OPTION_TEXT_COLOR] ? titleColor : titleFontStyle[FONT_STYLE_OPTION_TEXT_COLOR]
|
|
242
|
+
};
|
|
243
|
+
Object.entries(titleAttributes).forEach(_ref4 => {
|
|
244
|
+
let [key, value] = _ref4;
|
|
245
|
+
return title.setAttribute(key, value);
|
|
246
|
+
});
|
|
209
247
|
|
|
210
248
|
if (config.title) {
|
|
211
249
|
title.appendChild(document.createTextNode(config.title));
|
|
212
250
|
svgWrapper.appendChild(title);
|
|
213
|
-
}
|
|
251
|
+
} // subtitle
|
|
214
252
|
|
|
215
|
-
const subtitleFontStyle = mergeFontStyleWithDefault(fontStyle && fontStyle[FONT_STYLE_VISUALIZATION_SUBTITLE], FONT_STYLE_VISUALIZATION_SUBTITLE);
|
|
216
|
-
const subtitle = document.createElementNS(svgNS, 'text');
|
|
217
|
-
subtitle.setAttribute('x', getXFromTextAlign(subtitleFontStyle[FONT_STYLE_OPTION_TEXT_ALIGN]));
|
|
218
|
-
subtitle.setAttribute('y', titleYPosition);
|
|
219
|
-
subtitle.setAttribute('dy', "".concat(subtitleFontStyle[FONT_STYLE_OPTION_FONT_SIZE] + 10));
|
|
220
|
-
subtitle.setAttribute('text-anchor', getTextAnchorFromTextAlign(subtitleFontStyle[FONT_STYLE_OPTION_TEXT_ALIGN]));
|
|
221
|
-
subtitle.setAttribute('font-size', "".concat(subtitleFontStyle[FONT_STYLE_OPTION_FONT_SIZE], "px"));
|
|
222
|
-
subtitle.setAttribute('font-weight', subtitleFontStyle[FONT_STYLE_OPTION_BOLD] ? FONT_STYLE_OPTION_BOLD : 'normal');
|
|
223
|
-
subtitle.setAttribute('font-style', subtitleFontStyle[FONT_STYLE_OPTION_ITALIC] ? FONT_STYLE_OPTION_ITALIC : 'normal');
|
|
224
|
-
|
|
225
|
-
if (titleColor && subtitleFontStyle[FONT_STYLE_OPTION_TEXT_COLOR] === defaultFontStyle[FONT_STYLE_VISUALIZATION_SUBTITLE][FONT_STYLE_OPTION_TEXT_COLOR]) {
|
|
226
|
-
subtitle.setAttribute('fill', titleColor);
|
|
227
|
-
} else {
|
|
228
|
-
subtitle.setAttribute('fill', subtitleFontStyle[FONT_STYLE_OPTION_TEXT_COLOR]);
|
|
229
|
-
}
|
|
230
253
|
|
|
231
|
-
subtitle.
|
|
254
|
+
const subtitle = document.createElementNS(svgNS, 'text');
|
|
255
|
+
const subtitleFontStyle = mergeFontStyleWithDefault(fontStyle && fontStyle[FONT_STYLE_VISUALIZATION_SUBTITLE], FONT_STYLE_VISUALIZATION_SUBTITLE);
|
|
256
|
+
const subtitleAttributes = {
|
|
257
|
+
x: getXFromTextAlign(subtitleFontStyle[FONT_STYLE_OPTION_TEXT_ALIGN]),
|
|
258
|
+
y: titleYPosition,
|
|
259
|
+
dy: "".concat(subtitleFontStyle[FONT_STYLE_OPTION_FONT_SIZE] + 10),
|
|
260
|
+
'text-anchor': getTextAnchorFromTextAlign(subtitleFontStyle[FONT_STYLE_OPTION_TEXT_ALIGN]),
|
|
261
|
+
'font-size': "".concat(subtitleFontStyle[FONT_STYLE_OPTION_FONT_SIZE], "px"),
|
|
262
|
+
'font-weight': subtitleFontStyle[FONT_STYLE_OPTION_BOLD] ? FONT_STYLE_OPTION_BOLD : 'normal',
|
|
263
|
+
'font-style': subtitleFontStyle[FONT_STYLE_OPTION_ITALIC] ? FONT_STYLE_OPTION_ITALIC : 'normal',
|
|
264
|
+
fill: titleColor && subtitleFontStyle[FONT_STYLE_OPTION_TEXT_COLOR] === defaultFontStyle[FONT_STYLE_VISUALIZATION_SUBTITLE][FONT_STYLE_OPTION_TEXT_COLOR] ? titleColor : subtitleFontStyle[FONT_STYLE_OPTION_TEXT_COLOR],
|
|
265
|
+
'data-test': 'visualization-subtitle'
|
|
266
|
+
};
|
|
267
|
+
Object.entries(subtitleAttributes).forEach(_ref5 => {
|
|
268
|
+
let [key, value] = _ref5;
|
|
269
|
+
return subtitle.setAttribute(key, value);
|
|
270
|
+
});
|
|
232
271
|
|
|
233
272
|
if (config.subtitle) {
|
|
234
273
|
subtitle.appendChild(document.createTextNode(config.subtitle));
|
|
@@ -244,7 +283,8 @@ const generateDVItem = (config, _ref3) => {
|
|
|
244
283
|
noData,
|
|
245
284
|
icon,
|
|
246
285
|
containerWidth: width,
|
|
247
|
-
containerHeight: height
|
|
286
|
+
containerHeight: height,
|
|
287
|
+
topMargin: TOP_MARGIN_FIXED + ((config.title ? parseInt(title.getAttribute('font-size')) : 0) + (config.subtitle ? parseInt(subtitle.getAttribute('font-size')) : 0)) * 2.5
|
|
248
288
|
}));
|
|
249
289
|
return svgContainer;
|
|
250
290
|
};
|
|
@@ -271,7 +311,7 @@ const shouldUseContrastColor = function () {
|
|
|
271
311
|
return L <= 0.179;
|
|
272
312
|
};
|
|
273
313
|
|
|
274
|
-
export default function (config, parentEl,
|
|
314
|
+
export default function (config, parentEl, _ref6) {
|
|
275
315
|
let {
|
|
276
316
|
dashboard,
|
|
277
317
|
legendSets,
|
|
@@ -279,7 +319,7 @@ export default function (config, parentEl, _ref4) {
|
|
|
279
319
|
noData,
|
|
280
320
|
legendOptions,
|
|
281
321
|
icon
|
|
282
|
-
} =
|
|
322
|
+
} = _ref6;
|
|
283
323
|
const legendSet = legendOptions && legendSets[0];
|
|
284
324
|
const legendColor = legendSet && getColorByValueFromLegendSet(legendSet, config.value);
|
|
285
325
|
let valueColor, titleColor, backgroundColor;
|