@dhis2/analytics 24.10.7 → 24.10.9

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 CHANGED
@@ -1,3 +1,17 @@
1
+ ## [24.10.9](https://github.com/dhis2/analytics/compare/v24.10.8...v24.10.9) (2024-06-04)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * numbers are misaligned in pivot table (DHIS2-16900) ([#1671](https://github.com/dhis2/analytics/issues/1671)) ([7fc5321](https://github.com/dhis2/analytics/commit/7fc532122d7bd0f12b2dba1ce005fd7490d47137))
7
+
8
+ ## [24.10.8](https://github.com/dhis2/analytics/compare/v24.10.7...v24.10.8) (2024-04-25)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * single value size and position issues (DHIS2-15344) (DHIS2-13077) [24.x] ([#1646](https://github.com/dhis2/analytics/issues/1646)) ([f052741](https://github.com/dhis2/analytics/commit/f052741ca0f5eafc93094cf53b9bf89f7c7244c3))
14
+
1
15
  ## [24.10.7](https://github.com/dhis2/analytics/compare/v24.10.6...v24.10.7) (2024-04-22)
2
16
 
3
17
 
@@ -12,9 +12,9 @@ var _pivotTableConstants = require("../../../modules/pivotTable/pivotTableConsta
12
12
  const table = ["div.pivot-table-container{font-family:'Roboto',Arial,sans-serif;overflow:auto;color:".concat(_ui.colors.grey900, ";}"), "table{border-spacing:0;white-space:nowrap;box-sizing:border-box;text-align:center;border:1px solid ".concat(_pivotTableConstants.BORDER_COLOR, ";border-width:1px 1px 0 0;}"), "table.fixed-headers{border-width:0 0 0 1px;}", "table.fixed-headers tr th,table.fixed-headers tr td{border-width:0 1px 1px 0;}", "table.fixed-column-headers{border-width:0 1px 0 0;}", "table.fixed-column-headers tr th,table.fixed-column-headers tr td{border-width:0 0 1px 1px;}", "table.fixed-headers thead tr:first-of-type th,table.fixed-column-headers thead tr:first-of-type th{border-top:1px solid ".concat(_pivotTableConstants.BORDER_COLOR, ";}"), "table.fixed-row-headers{border-width:0 0 1px 1px;}", "table.fixed-row-headers tr th,table.fixed-row-headers tr td{border-width:1px 1px 0 0;}"];
13
13
  exports.table = table;
14
14
  table.__hash = "712241344";
15
- const cell = ["td.jsx-1789008308,th.jsx-1789008308{box-sizing:border-box;font-weight:normal;overflow:hidden;text-overflow:ellipsis;border:1px solid ".concat(_pivotTableConstants.BORDER_COLOR, ";border-width:0 0 1px 1px;cursor:default;}"), "th.fixed-header.jsx-1789008308{position:-webkit-sticky;position:sticky;z-index:1;top:0;left:0;}", ".fontsize-SMALL.jsx-1789008308{font-size:".concat(_pivotTableConstants.FONT_SIZE_SMALL, "px;line-height:").concat(_pivotTableConstants.FONT_SIZE_SMALL, "px;}"), ".fontsize-NORMAL.jsx-1789008308{font-size:".concat(_pivotTableConstants.FONT_SIZE_NORMAL, "px;line-height:").concat(_pivotTableConstants.FONT_SIZE_NORMAL, "px;}"), ".fontsize-LARGE.jsx-1789008308{font-size:".concat(_pivotTableConstants.FONT_SIZE_LARGE, "px;line-height:").concat(_pivotTableConstants.FONT_SIZE_LARGE, "px;}"), ".displaydensity-COMPACT.jsx-1789008308{padding:".concat(_pivotTableConstants.DISPLAY_DENSITY_PADDING_COMPACT, "px;}"), ".displaydensity-NORMAL.jsx-1789008308{padding:".concat(_pivotTableConstants.DISPLAY_DENSITY_PADDING_NORMAL, "px;}"), ".displaydensity-COMFORTABLE.jsx-1789008308{padding:".concat(_pivotTableConstants.DISPLAY_DENSITY_PADDING_COMFORTABLE, "px;}"), ".column-header.jsx-1789008308{background-color:#dae6f8;}", "div.column-header-inner.jsx-1789008308{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;}", ".title.jsx-1789008308{font-weight:bold;background-color:#cddaed;}", ".row-header.jsx-1789008308{background-color:#dae6f8;}", ".row-header-hierarchy.jsx-1789008308{text-align:left;}", ".empty-header.jsx-1789008308{background-color:#cddaed;}", ".total-header.jsx-1789008308{background-color:#bac6d8;}", ".value.jsx-1789008308{background-color:#ffffff;}", ".TEXT.jsx-1789008308{text-align:left;}", ".NUMBER.jsx-1789008308{text-align:right;}", ".clickable.jsx-1789008308{cursor:pointer;}", ".value.jsx-1789008308:hover{background-color:#f3f3f3;}", ".subtotal.jsx-1789008308{background-color:#f4f4f4;}", ".total.jsx-1789008308{background-color:#d8d8d8;}", ".column-header-label.jsx-1789008308{overflow:hidden;text-overflow:ellipsis;}"];
15
+ const cell = ["td.jsx-2757248239,th.jsx-2757248239{box-sizing:border-box;font-weight:normal;overflow:hidden;text-overflow:ellipsis;border:1px solid ".concat(_pivotTableConstants.BORDER_COLOR, ";border-width:0 0 1px 1px;cursor:default;}"), "th.fixed-header.jsx-2757248239{position:-webkit-sticky;position:sticky;z-index:1;top:0;left:0;}", ".fontsize-SMALL.jsx-2757248239{font-size:".concat(_pivotTableConstants.FONT_SIZE_SMALL, "px;line-height:").concat(_pivotTableConstants.FONT_SIZE_SMALL, "px;}"), ".fontsize-NORMAL.jsx-2757248239{font-size:".concat(_pivotTableConstants.FONT_SIZE_NORMAL, "px;line-height:").concat(_pivotTableConstants.FONT_SIZE_NORMAL, "px;}"), ".fontsize-LARGE.jsx-2757248239{font-size:".concat(_pivotTableConstants.FONT_SIZE_LARGE, "px;line-height:").concat(_pivotTableConstants.FONT_SIZE_LARGE, "px;}"), ".displaydensity-COMPACT.jsx-2757248239{padding:".concat(_pivotTableConstants.DISPLAY_DENSITY_PADDING_COMPACT, "px;}"), ".displaydensity-NORMAL.jsx-2757248239{padding:".concat(_pivotTableConstants.DISPLAY_DENSITY_PADDING_NORMAL, "px;}"), ".displaydensity-COMFORTABLE.jsx-2757248239{padding:".concat(_pivotTableConstants.DISPLAY_DENSITY_PADDING_COMFORTABLE, "px;}"), ".column-header.jsx-2757248239{background-color:#dae6f8;}", "div.column-header-inner.jsx-2757248239{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;}", ".title.jsx-2757248239{font-weight:bold;background-color:#cddaed;}", ".row-header.jsx-2757248239{background-color:#dae6f8;}", ".row-header-hierarchy.jsx-2757248239{text-align:left;}", ".empty-header.jsx-2757248239{background-color:#cddaed;}", ".total-header.jsx-2757248239{background-color:#bac6d8;}", ".value.jsx-2757248239{background-color:#ffffff;text-align:left;}", ".NUMBER.jsx-2757248239,.INTEGER.jsx-2757248239,.INTEGER_POSITIVE.jsx-2757248239,.INTEGER_NEGATIVE.jsx-2757248239,.INTEGER_ZERO_OR_POSITIVE.jsx-2757248239,.UNIT_INTERVAL.jsx-2757248239,.PERCENTAGE.jsx-2757248239,.BOOLEAN.jsx-2757248239,.TRUE_ONLY.jsx-2757248239{text-align:right;}", ".clickable.jsx-2757248239{cursor:pointer;}", ".value.jsx-2757248239:hover{background-color:#f3f3f3;}", ".subtotal.jsx-2757248239{background-color:#f4f4f4;}", ".total.jsx-2757248239{background-color:#d8d8d8;}", ".column-header-label.jsx-2757248239{overflow:hidden;text-overflow:ellipsis;}"];
16
16
  exports.cell = cell;
17
- cell.__hash = "1789008308";
17
+ cell.__hash = "2757248239";
18
18
  const sortIcon = [".fontsize-SMALL.jsx-2877616992{height:".concat(_pivotTableConstants.FONT_SIZE_SMALL, "px;margin-bottom:1px;margin-left:5px;}"), ".fontsize-NORMAL.jsx-2877616992{height:".concat(_pivotTableConstants.FONT_SIZE_NORMAL, "px;max-height:11px;margin-bottom:2px;margin-left:6px;}"), ".fontsize-LARGE.jsx-2877616992{height:".concat(_pivotTableConstants.FONT_SIZE_LARGE, "px;margin-bottom:2px;margin-left:7px;}")];
19
19
  exports.sortIcon = sortIcon;
20
20
  sortIcon.__hash = "2877616992";
@@ -11,62 +11,133 @@ 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
44
+ // Not exactly precise but close enough
45
+
46
+ const getTextWidth = (text, font) => {
47
+ const canvas = document.createElement('canvas');
48
+ const context = canvas.getContext('2d');
49
+ context.font = font;
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;
67
+ };
15
68
 
16
69
  const generateValueSVG = _ref => {
17
70
  let {
18
71
  formattedValue,
19
72
  subText,
20
73
  valueColor,
74
+ textColor,
75
+ icon,
21
76
  noData,
22
- y
77
+ containerWidth,
78
+ containerHeight,
79
+ topMargin = 0
23
80
  } = _ref;
24
- const textSize = 300;
81
+ console.log('jj SV generateValueSVG', formattedValue, icon, subText);
82
+ const showIcon = icon && formattedValue !== noData.text;
83
+ const textSize = getTextSize(formattedValue, containerWidth, containerHeight, showIcon);
84
+ const textWidth = getTextWidth(formattedValue, "".concat(textSize, "px Roboto"));
85
+ const iconSize = textSize;
86
+ 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;
25
87
  const svgValue = document.createElementNS(svgNS, 'svg');
26
- svgValue.setAttribute('xmlns', svgNS);
27
- svgValue.setAttribute('viewBox', "0 -".concat(textSize + 50, " ").concat(textSize * 0.75 * formattedValue.length, " ").concat(textSize + 200));
28
-
29
- if (y) {
30
- svgValue.setAttribute('y', y);
31
- }
32
-
88
+ svgValue.setAttribute('viewBox', "0 0 ".concat(containerWidth, " ").concat(containerHeight));
89
+ svgValue.setAttribute('width', '50%');
90
+ svgValue.setAttribute('height', '50%');
91
+ svgValue.setAttribute('x', '50%');
92
+ svgValue.setAttribute('y', '50%');
93
+ svgValue.setAttribute('style', 'overflow: visible');
33
94
  let fillColor = _ui.colors.grey900;
34
95
 
35
96
  if (valueColor) {
36
97
  fillColor = valueColor;
37
98
  } else if (formattedValue === noData.text) {
38
99
  fillColor = _ui.colors.grey600;
100
+ } // show icon if configured in maintenance app
101
+
102
+
103
+ if (showIcon) {
104
+ // embed icon to allow changing color
105
+ // (elements with fill need to use "currentColor" for this to work)
106
+ const iconSvgNode = document.createElementNS(svgNS, 'svg');
107
+ iconSvgNode.setAttribute('viewBox', '0 0 48 48');
108
+ iconSvgNode.setAttribute('width', iconSize);
109
+ iconSvgNode.setAttribute('height', iconSize);
110
+ iconSvgNode.setAttribute('y', (iconSize / 2 - topMargin / 2) * -1);
111
+ iconSvgNode.setAttribute('x', "-".concat((iconSize + getIconPadding(textSize) + textWidth) / 2));
112
+ iconSvgNode.setAttribute('style', "color: ".concat(fillColor));
113
+ const parser = new DOMParser();
114
+ const svgIconDocument = parser.parseFromString(icon, 'image/svg+xml');
115
+ Array.from(svgIconDocument.documentElement.children).forEach(node => iconSvgNode.appendChild(node));
116
+ svgValue.appendChild(iconSvgNode);
39
117
  }
40
118
 
119
+ const letterSpacing = Math.round(textSize * LETTER_SPACING_TEXT_SIZE_FACTOR);
41
120
  const textNode = document.createElementNS(svgNS, 'text');
42
- textNode.setAttribute('text-anchor', 'middle');
43
121
  textNode.setAttribute('font-size', textSize);
44
122
  textNode.setAttribute('font-weight', '300');
45
- textNode.setAttribute('letter-spacing', '-5');
46
- textNode.setAttribute('x', '50%');
123
+ textNode.setAttribute('letter-spacing', letterSpacing < LETTER_SPACING_MIN_THRESHOLD ? LETTER_SPACING_MIN_THRESHOLD : letterSpacing > LETTER_SPACING_MAX_THRESHOLD ? LETTER_SPACING_MAX_THRESHOLD : letterSpacing);
124
+ textNode.setAttribute('text-anchor', 'middle');
125
+ textNode.setAttribute('x', showIcon ? "".concat((iconSize + getIconPadding(textSize)) / 2) : 0);
126
+ textNode.setAttribute('y', topMargin / 2 + getTextHeightForNumbers(textSize) / 2);
47
127
  textNode.setAttribute('fill', fillColor);
48
128
  textNode.setAttribute('data-test', 'visualization-primary-value');
49
129
  textNode.appendChild(document.createTextNode(formattedValue));
50
130
  svgValue.appendChild(textNode);
51
131
 
52
132
  if (subText) {
53
- const svgSubText = document.createElementNS(svgNS, 'svg');
54
- const subTextSize = 40;
55
- svgSubText.setAttribute('viewBox', "0 -50 ".concat(textSize * 0.75 * formattedValue.length, " ").concat(textSize + 200));
56
-
57
- if (y) {
58
- svgSubText.setAttribute('y', y);
59
- }
60
-
61
133
  const subTextNode = document.createElementNS(svgNS, 'text');
62
134
  subTextNode.setAttribute('text-anchor', 'middle');
63
135
  subTextNode.setAttribute('font-size', subTextSize);
64
- subTextNode.setAttribute('x', '50%');
65
- subTextNode.setAttribute('x', '50%');
66
- subTextNode.setAttribute('fill', _ui.colors.grey600);
136
+ subTextNode.setAttribute('y', iconSize / 2 + topMargin / 2);
137
+ subTextNode.setAttribute('dy', subTextSize * 1.7);
138
+ subTextNode.setAttribute('fill', textColor);
67
139
  subTextNode.appendChild(document.createTextNode(subText));
68
- svgSubText.appendChild(subTextNode);
69
- svgValue.appendChild(svgSubText);
140
+ svgValue.appendChild(subTextNode);
70
141
  }
71
142
 
72
143
  return svgValue;
@@ -74,14 +145,28 @@ const generateValueSVG = _ref => {
74
145
 
75
146
  const generateDashboardItem = (config, _ref2) => {
76
147
  let {
148
+ svgContainer,
149
+ width,
150
+ height,
77
151
  valueColor,
78
152
  titleColor,
79
153
  backgroundColor,
80
- noData
154
+ noData,
155
+ icon
81
156
  } = _ref2;
157
+ svgContainer.appendChild(generateValueSVG({
158
+ formattedValue: config.formattedValue,
159
+ subText: config.subText,
160
+ valueColor,
161
+ textColor: titleColor,
162
+ noData,
163
+ icon,
164
+ containerWidth: width,
165
+ containerHeight: height
166
+ }));
82
167
  const container = document.createElement('div');
83
- container.setAttribute('style', "display: flex; flex-direction: column; align-items: center; justify-content: center; width: 100%; height: 100%; background-color:".concat(backgroundColor, ";"));
84
- const titleStyle = "font-size: 12px; color: ".concat(titleColor || '#666', ";");
168
+ container.setAttribute('style', "display: flex; flex-direction: column; align-items: center; justify-content: center; width: 100%; height: 100%; padding-top: 8px; ".concat(backgroundColor ? "background-color:".concat(backgroundColor, ";") : ''));
169
+ const titleStyle = "padding: 0 8px; text-align: center; font-size: 12px; color: ".concat(titleColor || '#666', ";");
85
170
  const title = document.createElement('span');
86
171
  title.setAttribute('style', titleStyle);
87
172
 
@@ -92,18 +177,12 @@ const generateDashboardItem = (config, _ref2) => {
92
177
 
93
178
  if (config.subtitle) {
94
179
  const subtitle = document.createElement('span');
95
- subtitle.setAttribute('style', titleStyle + ' margin-top: 4px; padding: 0 8px');
180
+ subtitle.setAttribute('style', titleStyle + ' margin-top: 4px;');
96
181
  subtitle.appendChild(document.createTextNode(config.subtitle));
97
182
  container.appendChild(subtitle);
98
183
  }
99
184
 
100
- container.appendChild(generateValueSVG({
101
- formattedValue: config.formattedValue,
102
- subText: config.subText,
103
- valueColor,
104
- noData,
105
- y: 40
106
- }));
185
+ container.appendChild(svgContainer);
107
186
  return container;
108
187
  };
109
188
 
@@ -137,89 +216,92 @@ const getXFromTextAlign = textAlign => {
137
216
 
138
217
  const generateDVItem = (config, _ref3) => {
139
218
  let {
219
+ svgContainer,
220
+ width,
221
+ height,
140
222
  valueColor,
223
+ noData,
141
224
  backgroundColor,
142
225
  titleColor,
143
- parentEl,
144
226
  fontStyle,
145
- noData
227
+ icon
146
228
  } = _ref3;
147
- const parentElBBox = parentEl.getBoundingClientRect();
148
- const width = parentElBBox.width;
149
- const height = parentElBBox.height;
150
- const svgNS = 'http://www.w3.org/2000/svg';
151
- const svg = document.createElementNS(svgNS, 'svg');
152
- svg.setAttribute('xmlns', svgNS);
153
- svg.setAttribute('viewBox', "0 0 ".concat(width, " ").concat(height));
154
- svg.setAttribute('width', width);
155
- svg.setAttribute('height', height);
156
- svg.setAttribute('data-test', 'visualization-container');
157
229
 
158
230
  if (backgroundColor) {
159
- svg.setAttribute('style', "background-color: ".concat(backgroundColor, ";"));
231
+ svgContainer.setAttribute('style', "background-color: ".concat(backgroundColor, ";"));
160
232
  const background = document.createElementNS(svgNS, 'rect');
161
233
  background.setAttribute('width', '100%');
162
234
  background.setAttribute('height', '100%');
163
235
  background.setAttribute('fill', backgroundColor);
164
- svg.appendChild(background);
236
+ svgContainer.appendChild(background);
165
237
  }
166
238
 
239
+ const svgWrapper = document.createElementNS(svgNS, 'svg'); // title
240
+
167
241
  const title = document.createElementNS(svgNS, 'text');
168
242
  const titleFontStyle = (0, _fontStyle.mergeFontStyleWithDefault)(fontStyle && fontStyle[_fontStyle.FONT_STYLE_VISUALIZATION_TITLE], _fontStyle.FONT_STYLE_VISUALIZATION_TITLE);
169
- title.setAttribute('x', getXFromTextAlign(titleFontStyle[_fontStyle.FONT_STYLE_OPTION_TEXT_ALIGN]));
170
- title.setAttribute('y', 28);
171
- title.setAttribute('text-anchor', getTextAnchorFromTextAlign(titleFontStyle[_fontStyle.FONT_STYLE_OPTION_TEXT_ALIGN]));
172
- title.setAttribute('font-size', "".concat(titleFontStyle[_fontStyle.FONT_STYLE_OPTION_FONT_SIZE], "px"));
173
- title.setAttribute('font-weight', titleFontStyle[_fontStyle.FONT_STYLE_OPTION_BOLD] ? _fontStyle.FONT_STYLE_OPTION_BOLD : 'normal');
174
- title.setAttribute('font-style', titleFontStyle[_fontStyle.FONT_STYLE_OPTION_ITALIC] ? _fontStyle.FONT_STYLE_OPTION_ITALIC : 'normal');
175
-
176
- if (titleColor && titleFontStyle[_fontStyle.FONT_STYLE_OPTION_TEXT_COLOR] === _fontStyle.defaultFontStyle[_fontStyle.FONT_STYLE_VISUALIZATION_TITLE][_fontStyle.FONT_STYLE_OPTION_TEXT_COLOR]) {
177
- title.setAttribute('fill', titleColor);
178
- } else {
179
- title.setAttribute('fill', titleFontStyle[_fontStyle.FONT_STYLE_OPTION_TEXT_COLOR]);
180
- }
181
-
182
- title.setAttribute('data-test', 'visualization-title');
243
+ const titleYPosition = TOP_MARGIN_FIXED + parseInt(titleFontStyle[_fontStyle.FONT_STYLE_OPTION_FONT_SIZE]) + 'px';
244
+ const titleAttributes = {
245
+ x: getXFromTextAlign(titleFontStyle[_fontStyle.FONT_STYLE_OPTION_TEXT_ALIGN]),
246
+ y: titleYPosition,
247
+ 'text-anchor': getTextAnchorFromTextAlign(titleFontStyle[_fontStyle.FONT_STYLE_OPTION_TEXT_ALIGN]),
248
+ 'font-size': "".concat(titleFontStyle[_fontStyle.FONT_STYLE_OPTION_FONT_SIZE], "px"),
249
+ 'font-weight': titleFontStyle[_fontStyle.FONT_STYLE_OPTION_BOLD] ? _fontStyle.FONT_STYLE_OPTION_BOLD : 'normal',
250
+ 'font-style': titleFontStyle[_fontStyle.FONT_STYLE_OPTION_ITALIC] ? _fontStyle.FONT_STYLE_OPTION_ITALIC : 'normal',
251
+ 'data-test': 'visualization-title',
252
+ 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]
253
+ };
254
+ Object.entries(titleAttributes).forEach(_ref4 => {
255
+ let [key, value] = _ref4;
256
+ return title.setAttribute(key, value);
257
+ });
183
258
 
184
259
  if (config.title) {
185
260
  title.appendChild(document.createTextNode(config.title));
186
- svg.appendChild(title);
187
- }
261
+ svgWrapper.appendChild(title);
262
+ } // subtitle
188
263
 
189
- const subtitleFontStyle = (0, _fontStyle.mergeFontStyleWithDefault)(fontStyle && fontStyle[_fontStyle.FONT_STYLE_VISUALIZATION_SUBTITLE], _fontStyle.FONT_STYLE_VISUALIZATION_SUBTITLE);
190
- const subtitle = document.createElementNS(svgNS, 'text');
191
- subtitle.setAttribute('x', getXFromTextAlign(subtitleFontStyle[_fontStyle.FONT_STYLE_OPTION_TEXT_ALIGN]));
192
- subtitle.setAttribute('y', 28);
193
- subtitle.setAttribute('dy', 22);
194
- subtitle.setAttribute('text-anchor', getTextAnchorFromTextAlign(subtitleFontStyle[_fontStyle.FONT_STYLE_OPTION_TEXT_ALIGN]));
195
- subtitle.setAttribute('font-size', "".concat(subtitleFontStyle[_fontStyle.FONT_STYLE_OPTION_FONT_SIZE], "px"));
196
- subtitle.setAttribute('font-weight', subtitleFontStyle[_fontStyle.FONT_STYLE_OPTION_BOLD] ? _fontStyle.FONT_STYLE_OPTION_BOLD : 'normal');
197
- subtitle.setAttribute('font-style', subtitleFontStyle[_fontStyle.FONT_STYLE_OPTION_ITALIC] ? _fontStyle.FONT_STYLE_OPTION_ITALIC : 'normal');
198
-
199
- if (titleColor && subtitleFontStyle[_fontStyle.FONT_STYLE_OPTION_TEXT_COLOR] === _fontStyle.defaultFontStyle[_fontStyle.FONT_STYLE_VISUALIZATION_SUBTITLE][_fontStyle.FONT_STYLE_OPTION_TEXT_COLOR]) {
200
- subtitle.setAttribute('fill', titleColor);
201
- } else {
202
- subtitle.setAttribute('fill', subtitleFontStyle[_fontStyle.FONT_STYLE_OPTION_TEXT_COLOR]);
203
- }
204
264
 
205
- subtitle.setAttribute('data-test', 'visualization-subtitle');
265
+ const subtitle = document.createElementNS(svgNS, 'text');
266
+ const subtitleFontStyle = (0, _fontStyle.mergeFontStyleWithDefault)(fontStyle && fontStyle[_fontStyle.FONT_STYLE_VISUALIZATION_SUBTITLE], _fontStyle.FONT_STYLE_VISUALIZATION_SUBTITLE);
267
+ const subtitleAttributes = {
268
+ x: getXFromTextAlign(subtitleFontStyle[_fontStyle.FONT_STYLE_OPTION_TEXT_ALIGN]),
269
+ y: titleYPosition,
270
+ dy: "".concat(subtitleFontStyle[_fontStyle.FONT_STYLE_OPTION_FONT_SIZE] + 10),
271
+ 'text-anchor': getTextAnchorFromTextAlign(subtitleFontStyle[_fontStyle.FONT_STYLE_OPTION_TEXT_ALIGN]),
272
+ 'font-size': "".concat(subtitleFontStyle[_fontStyle.FONT_STYLE_OPTION_FONT_SIZE], "px"),
273
+ 'font-weight': subtitleFontStyle[_fontStyle.FONT_STYLE_OPTION_BOLD] ? _fontStyle.FONT_STYLE_OPTION_BOLD : 'normal',
274
+ 'font-style': subtitleFontStyle[_fontStyle.FONT_STYLE_OPTION_ITALIC] ? _fontStyle.FONT_STYLE_OPTION_ITALIC : 'normal',
275
+ 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],
276
+ 'data-test': 'visualization-subtitle'
277
+ };
278
+ Object.entries(subtitleAttributes).forEach(_ref5 => {
279
+ let [key, value] = _ref5;
280
+ return subtitle.setAttribute(key, value);
281
+ });
206
282
 
207
283
  if (config.subtitle) {
208
284
  subtitle.appendChild(document.createTextNode(config.subtitle));
209
- svg.appendChild(subtitle);
285
+ svgWrapper.appendChild(subtitle);
210
286
  }
211
287
 
212
- svg.appendChild(generateValueSVG({
288
+ svgContainer.appendChild(svgWrapper);
289
+ svgContainer.appendChild(generateValueSVG({
213
290
  formattedValue: config.formattedValue,
214
291
  subText: config.subText,
215
292
  valueColor,
293
+ textColor: titleColor,
216
294
  noData,
217
- y: 20
295
+ icon,
296
+ containerWidth: width,
297
+ containerHeight: height,
298
+ topMargin: TOP_MARGIN_FIXED + ((config.title ? parseInt(title.getAttribute('font-size')) : 0) + (config.subtitle ? parseInt(subtitle.getAttribute('font-size')) : 0)) * 2.5
218
299
  }));
219
- return svg;
300
+ return svgContainer;
220
301
  };
221
302
 
222
- const shouldUseContrastColor = inputColor => {
303
+ const shouldUseContrastColor = function () {
304
+ let inputColor = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
223
305
  // based on https://stackoverflow.com/questions/3942878/how-to-decide-font-color-in-white-or-black-depending-on-background-color
224
306
  var color = inputColor.charAt(0) === '#' ? inputColor.substring(1, 7) : inputColor;
225
307
  var r = parseInt(color.substring(0, 2), 16); // hexToR
@@ -240,14 +322,15 @@ const shouldUseContrastColor = inputColor => {
240
322
  return L <= 0.179;
241
323
  };
242
324
 
243
- function _default(config, parentEl, _ref4) {
325
+ function _default(config, parentEl, _ref6) {
244
326
  let {
245
327
  dashboard,
246
328
  legendSets,
247
329
  fontStyle,
248
330
  noData,
249
- legendOptions
250
- } = _ref4;
331
+ legendOptions,
332
+ icon
333
+ } = _ref6;
251
334
  const legendSet = legendOptions && legendSets[0];
252
335
  const legendColor = legendSet && (0, _legends.getColorByValueFromLegendSet)(legendSet, config.value);
253
336
  let valueColor, titleColor, backgroundColor;
@@ -264,26 +347,42 @@ function _default(config, parentEl, _ref4) {
264
347
  parentEl.style.overflow = 'hidden';
265
348
  parentEl.style.display = 'flex';
266
349
  parentEl.style.justifyContent = 'center';
350
+ const parentElBBox = parentEl.getBoundingClientRect();
351
+ const width = parentElBBox.width;
352
+ const height = parentElBBox.height;
353
+ const svgContainer = document.createElementNS(svgNS, 'svg');
354
+ svgContainer.setAttribute('xmlns', svgNS);
355
+ svgContainer.setAttribute('viewBox', "0 0 ".concat(width, " ").concat(height));
356
+ svgContainer.setAttribute('width', dashboard ? '100%' : width);
357
+ svgContainer.setAttribute('height', dashboard ? '100%' : height);
358
+ svgContainer.setAttribute('data-test', 'visualization-container');
267
359
 
268
360
  if (dashboard) {
269
- parentEl.style.borderRadius = _ui.spacers.dp8;
361
+ parentEl.style.borderRadius = '3px';
270
362
  return generateDashboardItem(config, {
363
+ svgContainer,
364
+ width,
365
+ height,
271
366
  valueColor,
272
367
  backgroundColor,
273
368
  noData,
274
- ...(shouldUseContrastColor(legendColor) ? {
369
+ icon,
370
+ ...(legendOptions.style === _legends.LEGEND_DISPLAY_STYLE_FILL && legendColor && shouldUseContrastColor(legendColor) ? {
275
371
  titleColor: _ui.colors.white
276
372
  } : {})
277
373
  });
278
374
  } else {
279
375
  parentEl.style.height = "100%";
280
376
  return generateDVItem(config, {
377
+ svgContainer,
378
+ width,
379
+ height,
281
380
  valueColor,
282
381
  backgroundColor,
283
382
  titleColor,
284
- parentEl,
285
- fontStyle,
286
- noData
383
+ noData,
384
+ icon,
385
+ fontStyle
287
386
  });
288
387
  }
289
388
  }
@@ -2,7 +2,7 @@ import { colors } from '@dhis2/ui';
2
2
  import { BORDER_COLOR, DISPLAY_DENSITY_PADDING_COMPACT, DISPLAY_DENSITY_PADDING_NORMAL, DISPLAY_DENSITY_PADDING_COMFORTABLE, FONT_SIZE_SMALL, FONT_SIZE_NORMAL, FONT_SIZE_LARGE } from '../../../modules/pivotTable/pivotTableConstants.js';
3
3
  export const table = ["div.pivot-table-container{font-family:'Roboto',Arial,sans-serif;overflow:auto;color:".concat(colors.grey900, ";}"), "table{border-spacing:0;white-space:nowrap;box-sizing:border-box;text-align:center;border:1px solid ".concat(BORDER_COLOR, ";border-width:1px 1px 0 0;}"), "table.fixed-headers{border-width:0 0 0 1px;}", "table.fixed-headers tr th,table.fixed-headers tr td{border-width:0 1px 1px 0;}", "table.fixed-column-headers{border-width:0 1px 0 0;}", "table.fixed-column-headers tr th,table.fixed-column-headers tr td{border-width:0 0 1px 1px;}", "table.fixed-headers thead tr:first-of-type th,table.fixed-column-headers thead tr:first-of-type th{border-top:1px solid ".concat(BORDER_COLOR, ";}"), "table.fixed-row-headers{border-width:0 0 1px 1px;}", "table.fixed-row-headers tr th,table.fixed-row-headers tr td{border-width:1px 1px 0 0;}"];
4
4
  table.__hash = "712241344";
5
- export const cell = ["td.jsx-1789008308,th.jsx-1789008308{box-sizing:border-box;font-weight:normal;overflow:hidden;text-overflow:ellipsis;border:1px solid ".concat(BORDER_COLOR, ";border-width:0 0 1px 1px;cursor:default;}"), "th.fixed-header.jsx-1789008308{position:-webkit-sticky;position:sticky;z-index:1;top:0;left:0;}", ".fontsize-SMALL.jsx-1789008308{font-size:".concat(FONT_SIZE_SMALL, "px;line-height:").concat(FONT_SIZE_SMALL, "px;}"), ".fontsize-NORMAL.jsx-1789008308{font-size:".concat(FONT_SIZE_NORMAL, "px;line-height:").concat(FONT_SIZE_NORMAL, "px;}"), ".fontsize-LARGE.jsx-1789008308{font-size:".concat(FONT_SIZE_LARGE, "px;line-height:").concat(FONT_SIZE_LARGE, "px;}"), ".displaydensity-COMPACT.jsx-1789008308{padding:".concat(DISPLAY_DENSITY_PADDING_COMPACT, "px;}"), ".displaydensity-NORMAL.jsx-1789008308{padding:".concat(DISPLAY_DENSITY_PADDING_NORMAL, "px;}"), ".displaydensity-COMFORTABLE.jsx-1789008308{padding:".concat(DISPLAY_DENSITY_PADDING_COMFORTABLE, "px;}"), ".column-header.jsx-1789008308{background-color:#dae6f8;}", "div.column-header-inner.jsx-1789008308{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;}", ".title.jsx-1789008308{font-weight:bold;background-color:#cddaed;}", ".row-header.jsx-1789008308{background-color:#dae6f8;}", ".row-header-hierarchy.jsx-1789008308{text-align:left;}", ".empty-header.jsx-1789008308{background-color:#cddaed;}", ".total-header.jsx-1789008308{background-color:#bac6d8;}", ".value.jsx-1789008308{background-color:#ffffff;}", ".TEXT.jsx-1789008308{text-align:left;}", ".NUMBER.jsx-1789008308{text-align:right;}", ".clickable.jsx-1789008308{cursor:pointer;}", ".value.jsx-1789008308:hover{background-color:#f3f3f3;}", ".subtotal.jsx-1789008308{background-color:#f4f4f4;}", ".total.jsx-1789008308{background-color:#d8d8d8;}", ".column-header-label.jsx-1789008308{overflow:hidden;text-overflow:ellipsis;}"];
6
- cell.__hash = "1789008308";
5
+ export const cell = ["td.jsx-2757248239,th.jsx-2757248239{box-sizing:border-box;font-weight:normal;overflow:hidden;text-overflow:ellipsis;border:1px solid ".concat(BORDER_COLOR, ";border-width:0 0 1px 1px;cursor:default;}"), "th.fixed-header.jsx-2757248239{position:-webkit-sticky;position:sticky;z-index:1;top:0;left:0;}", ".fontsize-SMALL.jsx-2757248239{font-size:".concat(FONT_SIZE_SMALL, "px;line-height:").concat(FONT_SIZE_SMALL, "px;}"), ".fontsize-NORMAL.jsx-2757248239{font-size:".concat(FONT_SIZE_NORMAL, "px;line-height:").concat(FONT_SIZE_NORMAL, "px;}"), ".fontsize-LARGE.jsx-2757248239{font-size:".concat(FONT_SIZE_LARGE, "px;line-height:").concat(FONT_SIZE_LARGE, "px;}"), ".displaydensity-COMPACT.jsx-2757248239{padding:".concat(DISPLAY_DENSITY_PADDING_COMPACT, "px;}"), ".displaydensity-NORMAL.jsx-2757248239{padding:".concat(DISPLAY_DENSITY_PADDING_NORMAL, "px;}"), ".displaydensity-COMFORTABLE.jsx-2757248239{padding:".concat(DISPLAY_DENSITY_PADDING_COMFORTABLE, "px;}"), ".column-header.jsx-2757248239{background-color:#dae6f8;}", "div.column-header-inner.jsx-2757248239{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;}", ".title.jsx-2757248239{font-weight:bold;background-color:#cddaed;}", ".row-header.jsx-2757248239{background-color:#dae6f8;}", ".row-header-hierarchy.jsx-2757248239{text-align:left;}", ".empty-header.jsx-2757248239{background-color:#cddaed;}", ".total-header.jsx-2757248239{background-color:#bac6d8;}", ".value.jsx-2757248239{background-color:#ffffff;text-align:left;}", ".NUMBER.jsx-2757248239,.INTEGER.jsx-2757248239,.INTEGER_POSITIVE.jsx-2757248239,.INTEGER_NEGATIVE.jsx-2757248239,.INTEGER_ZERO_OR_POSITIVE.jsx-2757248239,.UNIT_INTERVAL.jsx-2757248239,.PERCENTAGE.jsx-2757248239,.BOOLEAN.jsx-2757248239,.TRUE_ONLY.jsx-2757248239{text-align:right;}", ".clickable.jsx-2757248239{cursor:pointer;}", ".value.jsx-2757248239:hover{background-color:#f3f3f3;}", ".subtotal.jsx-2757248239{background-color:#f4f4f4;}", ".total.jsx-2757248239{background-color:#d8d8d8;}", ".column-header-label.jsx-2757248239{overflow:hidden;text-overflow:ellipsis;}"];
6
+ cell.__hash = "2757248239";
7
7
  export const sortIcon = [".fontsize-SMALL.jsx-2877616992{height:".concat(FONT_SIZE_SMALL, "px;margin-bottom:1px;margin-left:5px;}"), ".fontsize-NORMAL.jsx-2877616992{height:".concat(FONT_SIZE_NORMAL, "px;max-height:11px;margin-bottom:2px;margin-left:6px;}"), ".fontsize-LARGE.jsx-2877616992{height:".concat(FONT_SIZE_LARGE, "px;margin-bottom:2px;margin-left:7px;}")];
8
8
  sortIcon.__hash = "2877616992";
@@ -1,62 +1,133 @@
1
- import { colors, spacers } from '@dhis2/ui';
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
34
+ // Not exactly precise but close enough
35
+
36
+ const getTextWidth = (text, font) => {
37
+ const canvas = document.createElement('canvas');
38
+ const context = canvas.getContext('2d');
39
+ context.font = font;
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;
57
+ };
5
58
 
6
59
  const generateValueSVG = _ref => {
7
60
  let {
8
61
  formattedValue,
9
62
  subText,
10
63
  valueColor,
64
+ textColor,
65
+ icon,
11
66
  noData,
12
- y
67
+ containerWidth,
68
+ containerHeight,
69
+ topMargin = 0
13
70
  } = _ref;
14
- const textSize = 300;
71
+ console.log('jj SV generateValueSVG', formattedValue, icon, subText);
72
+ const showIcon = icon && formattedValue !== noData.text;
73
+ const textSize = getTextSize(formattedValue, containerWidth, containerHeight, showIcon);
74
+ const textWidth = getTextWidth(formattedValue, "".concat(textSize, "px Roboto"));
75
+ const iconSize = textSize;
76
+ 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;
15
77
  const svgValue = document.createElementNS(svgNS, 'svg');
16
- svgValue.setAttribute('xmlns', svgNS);
17
- svgValue.setAttribute('viewBox', "0 -".concat(textSize + 50, " ").concat(textSize * 0.75 * formattedValue.length, " ").concat(textSize + 200));
18
-
19
- if (y) {
20
- svgValue.setAttribute('y', y);
21
- }
22
-
78
+ svgValue.setAttribute('viewBox', "0 0 ".concat(containerWidth, " ").concat(containerHeight));
79
+ svgValue.setAttribute('width', '50%');
80
+ svgValue.setAttribute('height', '50%');
81
+ svgValue.setAttribute('x', '50%');
82
+ svgValue.setAttribute('y', '50%');
83
+ svgValue.setAttribute('style', 'overflow: visible');
23
84
  let fillColor = colors.grey900;
24
85
 
25
86
  if (valueColor) {
26
87
  fillColor = valueColor;
27
88
  } else if (formattedValue === noData.text) {
28
89
  fillColor = colors.grey600;
90
+ } // show icon if configured in maintenance app
91
+
92
+
93
+ if (showIcon) {
94
+ // embed icon to allow changing color
95
+ // (elements with fill need to use "currentColor" for this to work)
96
+ const iconSvgNode = document.createElementNS(svgNS, 'svg');
97
+ iconSvgNode.setAttribute('viewBox', '0 0 48 48');
98
+ iconSvgNode.setAttribute('width', iconSize);
99
+ iconSvgNode.setAttribute('height', iconSize);
100
+ iconSvgNode.setAttribute('y', (iconSize / 2 - topMargin / 2) * -1);
101
+ iconSvgNode.setAttribute('x', "-".concat((iconSize + getIconPadding(textSize) + textWidth) / 2));
102
+ iconSvgNode.setAttribute('style', "color: ".concat(fillColor));
103
+ const parser = new DOMParser();
104
+ const svgIconDocument = parser.parseFromString(icon, 'image/svg+xml');
105
+ Array.from(svgIconDocument.documentElement.children).forEach(node => iconSvgNode.appendChild(node));
106
+ svgValue.appendChild(iconSvgNode);
29
107
  }
30
108
 
109
+ const letterSpacing = Math.round(textSize * LETTER_SPACING_TEXT_SIZE_FACTOR);
31
110
  const textNode = document.createElementNS(svgNS, 'text');
32
- textNode.setAttribute('text-anchor', 'middle');
33
111
  textNode.setAttribute('font-size', textSize);
34
112
  textNode.setAttribute('font-weight', '300');
35
- textNode.setAttribute('letter-spacing', '-5');
36
- textNode.setAttribute('x', '50%');
113
+ textNode.setAttribute('letter-spacing', letterSpacing < LETTER_SPACING_MIN_THRESHOLD ? LETTER_SPACING_MIN_THRESHOLD : letterSpacing > LETTER_SPACING_MAX_THRESHOLD ? LETTER_SPACING_MAX_THRESHOLD : letterSpacing);
114
+ textNode.setAttribute('text-anchor', 'middle');
115
+ textNode.setAttribute('x', showIcon ? "".concat((iconSize + getIconPadding(textSize)) / 2) : 0);
116
+ textNode.setAttribute('y', topMargin / 2 + getTextHeightForNumbers(textSize) / 2);
37
117
  textNode.setAttribute('fill', fillColor);
38
118
  textNode.setAttribute('data-test', 'visualization-primary-value');
39
119
  textNode.appendChild(document.createTextNode(formattedValue));
40
120
  svgValue.appendChild(textNode);
41
121
 
42
122
  if (subText) {
43
- const svgSubText = document.createElementNS(svgNS, 'svg');
44
- const subTextSize = 40;
45
- svgSubText.setAttribute('viewBox', "0 -50 ".concat(textSize * 0.75 * formattedValue.length, " ").concat(textSize + 200));
46
-
47
- if (y) {
48
- svgSubText.setAttribute('y', y);
49
- }
50
-
51
123
  const subTextNode = document.createElementNS(svgNS, 'text');
52
124
  subTextNode.setAttribute('text-anchor', 'middle');
53
125
  subTextNode.setAttribute('font-size', subTextSize);
54
- subTextNode.setAttribute('x', '50%');
55
- subTextNode.setAttribute('x', '50%');
56
- subTextNode.setAttribute('fill', colors.grey600);
126
+ subTextNode.setAttribute('y', iconSize / 2 + topMargin / 2);
127
+ subTextNode.setAttribute('dy', subTextSize * 1.7);
128
+ subTextNode.setAttribute('fill', textColor);
57
129
  subTextNode.appendChild(document.createTextNode(subText));
58
- svgSubText.appendChild(subTextNode);
59
- svgValue.appendChild(svgSubText);
130
+ svgValue.appendChild(subTextNode);
60
131
  }
61
132
 
62
133
  return svgValue;
@@ -64,14 +135,28 @@ const generateValueSVG = _ref => {
64
135
 
65
136
  const generateDashboardItem = (config, _ref2) => {
66
137
  let {
138
+ svgContainer,
139
+ width,
140
+ height,
67
141
  valueColor,
68
142
  titleColor,
69
143
  backgroundColor,
70
- noData
144
+ noData,
145
+ icon
71
146
  } = _ref2;
147
+ svgContainer.appendChild(generateValueSVG({
148
+ formattedValue: config.formattedValue,
149
+ subText: config.subText,
150
+ valueColor,
151
+ textColor: titleColor,
152
+ noData,
153
+ icon,
154
+ containerWidth: width,
155
+ containerHeight: height
156
+ }));
72
157
  const container = document.createElement('div');
73
- container.setAttribute('style', "display: flex; flex-direction: column; align-items: center; justify-content: center; width: 100%; height: 100%; background-color:".concat(backgroundColor, ";"));
74
- const titleStyle = "font-size: 12px; color: ".concat(titleColor || '#666', ";");
158
+ container.setAttribute('style', "display: flex; flex-direction: column; align-items: center; justify-content: center; width: 100%; height: 100%; padding-top: 8px; ".concat(backgroundColor ? "background-color:".concat(backgroundColor, ";") : ''));
159
+ const titleStyle = "padding: 0 8px; text-align: center; font-size: 12px; color: ".concat(titleColor || '#666', ";");
75
160
  const title = document.createElement('span');
76
161
  title.setAttribute('style', titleStyle);
77
162
 
@@ -82,18 +167,12 @@ const generateDashboardItem = (config, _ref2) => {
82
167
 
83
168
  if (config.subtitle) {
84
169
  const subtitle = document.createElement('span');
85
- subtitle.setAttribute('style', titleStyle + ' margin-top: 4px; padding: 0 8px');
170
+ subtitle.setAttribute('style', titleStyle + ' margin-top: 4px;');
86
171
  subtitle.appendChild(document.createTextNode(config.subtitle));
87
172
  container.appendChild(subtitle);
88
173
  }
89
174
 
90
- container.appendChild(generateValueSVG({
91
- formattedValue: config.formattedValue,
92
- subText: config.subText,
93
- valueColor,
94
- noData,
95
- y: 40
96
- }));
175
+ container.appendChild(svgContainer);
97
176
  return container;
98
177
  };
99
178
 
@@ -127,89 +206,92 @@ const getXFromTextAlign = textAlign => {
127
206
 
128
207
  const generateDVItem = (config, _ref3) => {
129
208
  let {
209
+ svgContainer,
210
+ width,
211
+ height,
130
212
  valueColor,
213
+ noData,
131
214
  backgroundColor,
132
215
  titleColor,
133
- parentEl,
134
216
  fontStyle,
135
- noData
217
+ icon
136
218
  } = _ref3;
137
- const parentElBBox = parentEl.getBoundingClientRect();
138
- const width = parentElBBox.width;
139
- const height = parentElBBox.height;
140
- const svgNS = 'http://www.w3.org/2000/svg';
141
- const svg = document.createElementNS(svgNS, 'svg');
142
- svg.setAttribute('xmlns', svgNS);
143
- svg.setAttribute('viewBox', "0 0 ".concat(width, " ").concat(height));
144
- svg.setAttribute('width', width);
145
- svg.setAttribute('height', height);
146
- svg.setAttribute('data-test', 'visualization-container');
147
219
 
148
220
  if (backgroundColor) {
149
- svg.setAttribute('style', "background-color: ".concat(backgroundColor, ";"));
221
+ svgContainer.setAttribute('style', "background-color: ".concat(backgroundColor, ";"));
150
222
  const background = document.createElementNS(svgNS, 'rect');
151
223
  background.setAttribute('width', '100%');
152
224
  background.setAttribute('height', '100%');
153
225
  background.setAttribute('fill', backgroundColor);
154
- svg.appendChild(background);
226
+ svgContainer.appendChild(background);
155
227
  }
156
228
 
229
+ const svgWrapper = document.createElementNS(svgNS, 'svg'); // title
230
+
157
231
  const title = document.createElementNS(svgNS, 'text');
158
232
  const titleFontStyle = mergeFontStyleWithDefault(fontStyle && fontStyle[FONT_STYLE_VISUALIZATION_TITLE], FONT_STYLE_VISUALIZATION_TITLE);
159
- title.setAttribute('x', getXFromTextAlign(titleFontStyle[FONT_STYLE_OPTION_TEXT_ALIGN]));
160
- title.setAttribute('y', 28);
161
- title.setAttribute('text-anchor', getTextAnchorFromTextAlign(titleFontStyle[FONT_STYLE_OPTION_TEXT_ALIGN]));
162
- title.setAttribute('font-size', "".concat(titleFontStyle[FONT_STYLE_OPTION_FONT_SIZE], "px"));
163
- title.setAttribute('font-weight', titleFontStyle[FONT_STYLE_OPTION_BOLD] ? FONT_STYLE_OPTION_BOLD : 'normal');
164
- title.setAttribute('font-style', titleFontStyle[FONT_STYLE_OPTION_ITALIC] ? FONT_STYLE_OPTION_ITALIC : 'normal');
165
-
166
- if (titleColor && titleFontStyle[FONT_STYLE_OPTION_TEXT_COLOR] === defaultFontStyle[FONT_STYLE_VISUALIZATION_TITLE][FONT_STYLE_OPTION_TEXT_COLOR]) {
167
- title.setAttribute('fill', titleColor);
168
- } else {
169
- title.setAttribute('fill', titleFontStyle[FONT_STYLE_OPTION_TEXT_COLOR]);
170
- }
171
-
172
- title.setAttribute('data-test', 'visualization-title');
233
+ const titleYPosition = TOP_MARGIN_FIXED + parseInt(titleFontStyle[FONT_STYLE_OPTION_FONT_SIZE]) + 'px';
234
+ const titleAttributes = {
235
+ x: getXFromTextAlign(titleFontStyle[FONT_STYLE_OPTION_TEXT_ALIGN]),
236
+ y: titleYPosition,
237
+ 'text-anchor': getTextAnchorFromTextAlign(titleFontStyle[FONT_STYLE_OPTION_TEXT_ALIGN]),
238
+ 'font-size': "".concat(titleFontStyle[FONT_STYLE_OPTION_FONT_SIZE], "px"),
239
+ 'font-weight': titleFontStyle[FONT_STYLE_OPTION_BOLD] ? FONT_STYLE_OPTION_BOLD : 'normal',
240
+ 'font-style': titleFontStyle[FONT_STYLE_OPTION_ITALIC] ? FONT_STYLE_OPTION_ITALIC : 'normal',
241
+ 'data-test': 'visualization-title',
242
+ 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]
243
+ };
244
+ Object.entries(titleAttributes).forEach(_ref4 => {
245
+ let [key, value] = _ref4;
246
+ return title.setAttribute(key, value);
247
+ });
173
248
 
174
249
  if (config.title) {
175
250
  title.appendChild(document.createTextNode(config.title));
176
- svg.appendChild(title);
177
- }
251
+ svgWrapper.appendChild(title);
252
+ } // subtitle
178
253
 
179
- const subtitleFontStyle = mergeFontStyleWithDefault(fontStyle && fontStyle[FONT_STYLE_VISUALIZATION_SUBTITLE], FONT_STYLE_VISUALIZATION_SUBTITLE);
180
- const subtitle = document.createElementNS(svgNS, 'text');
181
- subtitle.setAttribute('x', getXFromTextAlign(subtitleFontStyle[FONT_STYLE_OPTION_TEXT_ALIGN]));
182
- subtitle.setAttribute('y', 28);
183
- subtitle.setAttribute('dy', 22);
184
- subtitle.setAttribute('text-anchor', getTextAnchorFromTextAlign(subtitleFontStyle[FONT_STYLE_OPTION_TEXT_ALIGN]));
185
- subtitle.setAttribute('font-size', "".concat(subtitleFontStyle[FONT_STYLE_OPTION_FONT_SIZE], "px"));
186
- subtitle.setAttribute('font-weight', subtitleFontStyle[FONT_STYLE_OPTION_BOLD] ? FONT_STYLE_OPTION_BOLD : 'normal');
187
- subtitle.setAttribute('font-style', subtitleFontStyle[FONT_STYLE_OPTION_ITALIC] ? FONT_STYLE_OPTION_ITALIC : 'normal');
188
-
189
- if (titleColor && subtitleFontStyle[FONT_STYLE_OPTION_TEXT_COLOR] === defaultFontStyle[FONT_STYLE_VISUALIZATION_SUBTITLE][FONT_STYLE_OPTION_TEXT_COLOR]) {
190
- subtitle.setAttribute('fill', titleColor);
191
- } else {
192
- subtitle.setAttribute('fill', subtitleFontStyle[FONT_STYLE_OPTION_TEXT_COLOR]);
193
- }
194
254
 
195
- subtitle.setAttribute('data-test', 'visualization-subtitle');
255
+ const subtitle = document.createElementNS(svgNS, 'text');
256
+ const subtitleFontStyle = mergeFontStyleWithDefault(fontStyle && fontStyle[FONT_STYLE_VISUALIZATION_SUBTITLE], FONT_STYLE_VISUALIZATION_SUBTITLE);
257
+ const subtitleAttributes = {
258
+ x: getXFromTextAlign(subtitleFontStyle[FONT_STYLE_OPTION_TEXT_ALIGN]),
259
+ y: titleYPosition,
260
+ dy: "".concat(subtitleFontStyle[FONT_STYLE_OPTION_FONT_SIZE] + 10),
261
+ 'text-anchor': getTextAnchorFromTextAlign(subtitleFontStyle[FONT_STYLE_OPTION_TEXT_ALIGN]),
262
+ 'font-size': "".concat(subtitleFontStyle[FONT_STYLE_OPTION_FONT_SIZE], "px"),
263
+ 'font-weight': subtitleFontStyle[FONT_STYLE_OPTION_BOLD] ? FONT_STYLE_OPTION_BOLD : 'normal',
264
+ 'font-style': subtitleFontStyle[FONT_STYLE_OPTION_ITALIC] ? FONT_STYLE_OPTION_ITALIC : 'normal',
265
+ 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],
266
+ 'data-test': 'visualization-subtitle'
267
+ };
268
+ Object.entries(subtitleAttributes).forEach(_ref5 => {
269
+ let [key, value] = _ref5;
270
+ return subtitle.setAttribute(key, value);
271
+ });
196
272
 
197
273
  if (config.subtitle) {
198
274
  subtitle.appendChild(document.createTextNode(config.subtitle));
199
- svg.appendChild(subtitle);
275
+ svgWrapper.appendChild(subtitle);
200
276
  }
201
277
 
202
- svg.appendChild(generateValueSVG({
278
+ svgContainer.appendChild(svgWrapper);
279
+ svgContainer.appendChild(generateValueSVG({
203
280
  formattedValue: config.formattedValue,
204
281
  subText: config.subText,
205
282
  valueColor,
283
+ textColor: titleColor,
206
284
  noData,
207
- y: 20
285
+ icon,
286
+ containerWidth: width,
287
+ containerHeight: height,
288
+ topMargin: TOP_MARGIN_FIXED + ((config.title ? parseInt(title.getAttribute('font-size')) : 0) + (config.subtitle ? parseInt(subtitle.getAttribute('font-size')) : 0)) * 2.5
208
289
  }));
209
- return svg;
290
+ return svgContainer;
210
291
  };
211
292
 
212
- const shouldUseContrastColor = inputColor => {
293
+ const shouldUseContrastColor = function () {
294
+ let inputColor = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
213
295
  // based on https://stackoverflow.com/questions/3942878/how-to-decide-font-color-in-white-or-black-depending-on-background-color
214
296
  var color = inputColor.charAt(0) === '#' ? inputColor.substring(1, 7) : inputColor;
215
297
  var r = parseInt(color.substring(0, 2), 16); // hexToR
@@ -230,14 +312,15 @@ const shouldUseContrastColor = inputColor => {
230
312
  return L <= 0.179;
231
313
  };
232
314
 
233
- export default function (config, parentEl, _ref4) {
315
+ export default function (config, parentEl, _ref6) {
234
316
  let {
235
317
  dashboard,
236
318
  legendSets,
237
319
  fontStyle,
238
320
  noData,
239
- legendOptions
240
- } = _ref4;
321
+ legendOptions,
322
+ icon
323
+ } = _ref6;
241
324
  const legendSet = legendOptions && legendSets[0];
242
325
  const legendColor = legendSet && getColorByValueFromLegendSet(legendSet, config.value);
243
326
  let valueColor, titleColor, backgroundColor;
@@ -254,26 +337,42 @@ export default function (config, parentEl, _ref4) {
254
337
  parentEl.style.overflow = 'hidden';
255
338
  parentEl.style.display = 'flex';
256
339
  parentEl.style.justifyContent = 'center';
340
+ const parentElBBox = parentEl.getBoundingClientRect();
341
+ const width = parentElBBox.width;
342
+ const height = parentElBBox.height;
343
+ const svgContainer = document.createElementNS(svgNS, 'svg');
344
+ svgContainer.setAttribute('xmlns', svgNS);
345
+ svgContainer.setAttribute('viewBox', "0 0 ".concat(width, " ").concat(height));
346
+ svgContainer.setAttribute('width', dashboard ? '100%' : width);
347
+ svgContainer.setAttribute('height', dashboard ? '100%' : height);
348
+ svgContainer.setAttribute('data-test', 'visualization-container');
257
349
 
258
350
  if (dashboard) {
259
- parentEl.style.borderRadius = spacers.dp8;
351
+ parentEl.style.borderRadius = '3px';
260
352
  return generateDashboardItem(config, {
353
+ svgContainer,
354
+ width,
355
+ height,
261
356
  valueColor,
262
357
  backgroundColor,
263
358
  noData,
264
- ...(shouldUseContrastColor(legendColor) ? {
359
+ icon,
360
+ ...(legendOptions.style === LEGEND_DISPLAY_STYLE_FILL && legendColor && shouldUseContrastColor(legendColor) ? {
265
361
  titleColor: colors.white
266
362
  } : {})
267
363
  });
268
364
  } else {
269
365
  parentEl.style.height = "100%";
270
366
  return generateDVItem(config, {
367
+ svgContainer,
368
+ width,
369
+ height,
271
370
  valueColor,
272
371
  backgroundColor,
273
372
  titleColor,
274
- parentEl,
275
- fontStyle,
276
- noData
373
+ noData,
374
+ icon,
375
+ fontStyle
277
376
  });
278
377
  }
279
378
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dhis2/analytics",
3
- "version": "24.10.7",
3
+ "version": "24.10.9",
4
4
  "main": "./build/cjs/index.js",
5
5
  "module": "./build/es/index.js",
6
6
  "exports": {