@dhis2/analytics 25.0.0 → 25.1.0

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,10 @@
1
+ # [25.1.0](https://github.com/dhis2/analytics/compare/v25.0.0...v25.1.0) (2023-04-27)
2
+
3
+
4
+ ### Features
5
+
6
+ * icon in SV visualization DHIS2-10496 ([#1440](https://github.com/dhis2/analytics/issues/1440)) ([e6563ca](https://github.com/dhis2/analytics/commit/e6563cacc5e901a04d5432330b09b685936ddd70))
7
+
1
8
  # [25.0.0](https://github.com/dhis2/analytics/compare/v24.10.1...v25.0.0) (2023-04-24)
2
9
 
3
10
 
@@ -415,7 +415,8 @@ const CalculationModal = _ref => {
415
415
  loading: isCreatingCalculation || isUpdatingCalculation || isSavingCalculation,
416
416
  dataTest: "save-button"
417
417
  }, _index.default.t('Save calculation')))))), showDeletePrompt && /*#__PURE__*/_react.default.createElement(_ui.Modal, {
418
- small: true
418
+ small: true,
419
+ dataTest: "calculation-delete-modal"
419
420
  }, /*#__PURE__*/_react.default.createElement(_ui.ModalTitle, null, _index.default.t('Delete calculation')), /*#__PURE__*/_react.default.createElement(_ui.ModalContent, null, _index.default.t('Are you sure you want to delete this calculation? It may be used by other visualizations.')), /*#__PURE__*/_react.default.createElement(_ui.ModalActions, null, /*#__PURE__*/_react.default.createElement(_ui.ButtonStrip, {
420
421
  end: true
421
422
  }, /*#__PURE__*/_react.default.createElement(_ui.Button, {
@@ -58,6 +58,7 @@ const DataElementOption = _ref => {
58
58
  className: "jsx-".concat(_DataElementOptionStyle.default.__hash) + " " + (listeners && listeners.className != null && listeners.className || attributes && attributes.className != null && attributes.className || "draggable-item")
59
59
  }), /*#__PURE__*/_react.default.createElement("div", {
60
60
  onDoubleClick: () => onDoubleClick(data),
61
+ "data-test": "data-element-option",
61
62
  className: "jsx-".concat(_DataElementOptionStyle.default.__hash) + " " + "chip"
62
63
  }, /*#__PURE__*/_react.default.createElement("span", {
63
64
  className: "jsx-".concat(_DataElementOptionStyle.default.__hash) + " " + "icon"
@@ -76,11 +76,13 @@ const GroupSelector = _ref => {
76
76
  }, defaultGroup ? /*#__PURE__*/_react.default.createElement(_ui.SingleSelectOption, {
77
77
  value: defaultGroup.id,
78
78
  key: defaultGroup.id,
79
- label: defaultGroup.getName()
79
+ label: defaultGroup.getName(),
80
+ dataTest: "data-element-group-select-option-".concat(defaultGroup.id)
80
81
  }) : null, !loading ? groups.map(group => /*#__PURE__*/_react.default.createElement(_ui.SingleSelectOption, {
81
82
  value: group.id,
82
83
  key: group.id,
83
- label: group.name
84
+ label: group.name,
85
+ dataTest: "data-element-group-select-option-".concat(group.id)
84
86
  })) : null), /*#__PURE__*/_react.default.createElement(_style.default, {
85
87
  id: _DataElementSelectorStyle.default.__hash
86
88
  }, _DataElementSelectorStyle.default));
@@ -108,7 +110,8 @@ const DisaggregationSelector = _ref2 => {
108
110
  }, Object.entries(options).map(option => /*#__PURE__*/_react.default.createElement(_ui.SingleSelectOption, {
109
111
  value: option[0],
110
112
  key: option[0],
111
- label: option[1]
113
+ label: option[1],
114
+ dataTest: "data-element-disaggregation-select-option-".concat(option[0])
112
115
  }))), /*#__PURE__*/_react.default.createElement(_style.default, {
113
116
  id: _DataElementSelectorStyle.default.__hash
114
117
  }, _DataElementSelectorStyle.default));
@@ -35,7 +35,7 @@ const FORMULA_BOX_ID = 'formulabox';
35
35
  exports.FORMULA_BOX_ID = FORMULA_BOX_ID;
36
36
 
37
37
  const Placeholder = () => /*#__PURE__*/_react.default.createElement("div", {
38
- "data-test": 'placeholder',
38
+ "data-test": "placeholder",
39
39
  className: "jsx-".concat(_FormulaFieldStyle.default.__hash) + " " + "placeholder"
40
40
  }, /*#__PURE__*/_react.default.createElement(_FormulaIcon.default, null), /*#__PURE__*/_react.default.createElement("span", {
41
41
  className: "jsx-".concat(_FormulaFieldStyle.default.__hash) + " " + "help-text"
@@ -67,7 +67,7 @@ const FormulaField = _ref => {
67
67
  className: "jsx-".concat(_FormulaFieldStyle.default.__hash) + " " + "border"
68
68
  }), /*#__PURE__*/_react.default.createElement("div", {
69
69
  ref: setLastDropzoneRef,
70
- "data-test": 'formula-field',
70
+ "data-test": "formula-field",
71
71
  className: "jsx-".concat(_FormulaFieldStyle.default.__hash) + " " + "formula-field"
72
72
  }, loading && /*#__PURE__*/_react.default.createElement(_ui.Center, null, /*#__PURE__*/_react.default.createElement(_ui.CircularLoader, {
73
73
  small: true
@@ -30,6 +30,7 @@ const MathOperatorSelector = _ref => {
30
30
  }, /*#__PURE__*/_react.default.createElement("h4", {
31
31
  className: "jsx-".concat(_MathOperatorSelectorStyle.default.__hash) + " " + "sub-header"
32
32
  }, _index.default.t('Math operators')), /*#__PURE__*/_react.default.createElement("div", {
33
+ "data-test": "operators-list",
33
34
  className: "jsx-".concat(_MathOperatorSelectorStyle.default.__hash) + " " + "operators"
34
35
  }, (0, _expressions.getOperators)().map((_ref2, index) => {
35
36
  let {
@@ -22,7 +22,7 @@ const LegendKey = _ref => {
22
22
  legendSets
23
23
  } = _ref;
24
24
  return legendSets.length ? /*#__PURE__*/_react.default.createElement("div", {
25
- "data-test": 'legend-key-container',
25
+ "data-test": "legend-key-container",
26
26
  className: "jsx-".concat(_LegendKeyStyle.default.__hash) + " " + "container"
27
27
  }, legendSets.map((legendSet, index) => /*#__PURE__*/_react.default.createElement("div", {
28
28
  key: legendSet.id,
@@ -11,62 +11,94 @@ 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'; // Compute text width before rendering
15
+ // Not exactly precise but close enough
16
+
17
+ const getTextWidth = (text, font) => {
18
+ const canvas = document.createElement('canvas');
19
+ const context = canvas.getContext('2d');
20
+ context.font = font;
21
+ return context.measureText(text).width;
22
+ };
15
23
 
16
24
  const generateValueSVG = _ref => {
17
25
  let {
18
26
  formattedValue,
19
27
  subText,
20
28
  valueColor,
29
+ icon,
21
30
  noData,
22
- y
31
+ containerWidth,
32
+ containerHeight
23
33
  } = _ref;
24
- const textSize = 300;
25
- 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);
34
+ const ratio = containerHeight / containerWidth;
35
+ const iconSize = 300;
36
+ const iconPadding = 50;
37
+ const textSize = iconSize * 0.85;
38
+ const textWidth = getTextWidth(formattedValue, "".concat(textSize, "px Roboto"));
39
+ const subTextSize = 40;
40
+ const showIcon = icon && formattedValue !== noData.text;
41
+ let viewBoxWidth = textWidth;
42
+
43
+ if (showIcon) {
44
+ viewBoxWidth += iconSize + iconPadding;
31
45
  }
32
46
 
47
+ const viewBoxHeight = viewBoxWidth * ratio;
48
+ const svgValue = document.createElementNS(svgNS, 'svg');
49
+ svgValue.setAttribute('viewBox', "0 0 ".concat(viewBoxWidth, " ").concat(viewBoxHeight));
50
+ svgValue.setAttribute('width', '95%');
51
+ svgValue.setAttribute('height', '95%');
52
+ svgValue.setAttribute('x', '50%');
53
+ svgValue.setAttribute('y', '50%');
54
+ svgValue.setAttribute('style', 'overflow: visible');
33
55
  let fillColor = _ui.colors.grey900;
34
56
 
35
57
  if (valueColor) {
36
58
  fillColor = valueColor;
37
59
  } else if (formattedValue === noData.text) {
38
60
  fillColor = _ui.colors.grey600;
61
+ } // show icon if configured in maintenance app
62
+
63
+
64
+ if (showIcon) {
65
+ // embed icon to allow changing color
66
+ // (elements with fill need to use "currentColor" for this to work)
67
+ const iconSvgNode = document.createElementNS(svgNS, 'svg');
68
+ iconSvgNode.setAttribute('width', iconSize);
69
+ iconSvgNode.setAttribute('height', iconSize);
70
+ iconSvgNode.setAttribute('viewBox', '0 0 48 48');
71
+ iconSvgNode.setAttribute('y', "-".concat(iconSize / 2));
72
+ iconSvgNode.setAttribute('x', "-".concat((iconSize + iconPadding + textWidth) / 2));
73
+ iconSvgNode.setAttribute('style', "color: ".concat(fillColor));
74
+ const parser = new DOMParser();
75
+ const svgIconDocument = parser.parseFromString(icon, 'image/svg+xml');
76
+ Array.from(svgIconDocument.documentElement.children).forEach(node => iconSvgNode.appendChild(node));
77
+ svgValue.appendChild(iconSvgNode);
39
78
  }
40
79
 
41
80
  const textNode = document.createElementNS(svgNS, 'text');
42
- textNode.setAttribute('text-anchor', 'middle');
43
81
  textNode.setAttribute('font-size', textSize);
44
82
  textNode.setAttribute('font-weight', '300');
45
83
  textNode.setAttribute('letter-spacing', '-5');
46
- textNode.setAttribute('x', '50%');
84
+ textNode.setAttribute('text-anchor', 'middle');
85
+ textNode.setAttribute('x', showIcon ? "".concat((iconSize + iconPadding) / 2) : 0); // vertical align, "alignment-baseline: central" is not supported by Batik
86
+
87
+ textNode.setAttribute('y', '.35em');
47
88
  textNode.setAttribute('fill', fillColor);
48
89
  textNode.setAttribute('data-test', 'visualization-primary-value');
49
90
  textNode.appendChild(document.createTextNode(formattedValue));
50
91
  svgValue.appendChild(textNode);
51
92
 
52
93
  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
94
  const subTextNode = document.createElementNS(svgNS, 'text');
62
95
  subTextNode.setAttribute('text-anchor', 'middle');
63
96
  subTextNode.setAttribute('font-size', subTextSize);
64
- subTextNode.setAttribute('x', '50%');
65
- subTextNode.setAttribute('x', '50%');
97
+ subTextNode.setAttribute('y', iconSize / 2);
98
+ subTextNode.setAttribute('dy', subTextSize);
66
99
  subTextNode.setAttribute('fill', _ui.colors.grey600);
67
100
  subTextNode.appendChild(document.createTextNode(subText));
68
- svgSubText.appendChild(subTextNode);
69
- svgValue.appendChild(svgSubText);
101
+ svgValue.appendChild(subTextNode);
70
102
  }
71
103
 
72
104
  return svgValue;
@@ -74,14 +106,27 @@ const generateValueSVG = _ref => {
74
106
 
75
107
  const generateDashboardItem = (config, _ref2) => {
76
108
  let {
109
+ svgContainer,
110
+ width,
111
+ height,
77
112
  valueColor,
78
113
  titleColor,
79
114
  backgroundColor,
80
- noData
115
+ noData,
116
+ icon
81
117
  } = _ref2;
118
+ svgContainer.appendChild(generateValueSVG({
119
+ formattedValue: config.formattedValue,
120
+ subText: config.subText,
121
+ valueColor,
122
+ noData,
123
+ icon,
124
+ containerWidth: width,
125
+ containerHeight: height
126
+ }));
82
127
  const container = document.createElement('div');
83
128
  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', ";");
129
+ const titleStyle = "padding: 0 8px; text-align: center; font-size: 12px; color: ".concat(titleColor || '#666', ";");
85
130
  const title = document.createElement('span');
86
131
  title.setAttribute('style', titleStyle);
87
132
 
@@ -92,18 +137,12 @@ const generateDashboardItem = (config, _ref2) => {
92
137
 
93
138
  if (config.subtitle) {
94
139
  const subtitle = document.createElement('span');
95
- subtitle.setAttribute('style', titleStyle + ' margin-top: 4px; padding: 0 8px');
140
+ subtitle.setAttribute('style', titleStyle + ' margin-top: 4px;');
96
141
  subtitle.appendChild(document.createTextNode(config.subtitle));
97
142
  container.appendChild(subtitle);
98
143
  }
99
144
 
100
- container.appendChild(generateValueSVG({
101
- formattedValue: config.formattedValue,
102
- subText: config.subText,
103
- valueColor,
104
- noData,
105
- y: 40
106
- }));
145
+ container.appendChild(svgContainer);
107
146
  return container;
108
147
  };
109
148
 
@@ -137,33 +176,27 @@ const getXFromTextAlign = textAlign => {
137
176
 
138
177
  const generateDVItem = (config, _ref3) => {
139
178
  let {
179
+ svgContainer,
180
+ width,
181
+ height,
140
182
  valueColor,
183
+ noData,
141
184
  backgroundColor,
142
185
  titleColor,
143
- parentEl,
144
186
  fontStyle,
145
- noData
187
+ icon
146
188
  } = _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
189
 
158
190
  if (backgroundColor) {
159
- svg.setAttribute('style', "background-color: ".concat(backgroundColor, ";"));
191
+ svgContainer.setAttribute('style', "background-color: ".concat(backgroundColor, ";"));
160
192
  const background = document.createElementNS(svgNS, 'rect');
161
193
  background.setAttribute('width', '100%');
162
194
  background.setAttribute('height', '100%');
163
195
  background.setAttribute('fill', backgroundColor);
164
- svg.appendChild(background);
196
+ svgContainer.appendChild(background);
165
197
  }
166
198
 
199
+ const svgWrapper = document.createElementNS(svgNS, 'svg');
167
200
  const title = document.createElementNS(svgNS, 'text');
168
201
  const titleFontStyle = (0, _fontStyle.mergeFontStyleWithDefault)(fontStyle && fontStyle[_fontStyle.FONT_STYLE_VISUALIZATION_TITLE], _fontStyle.FONT_STYLE_VISUALIZATION_TITLE);
169
202
  title.setAttribute('x', getXFromTextAlign(titleFontStyle[_fontStyle.FONT_STYLE_OPTION_TEXT_ALIGN]));
@@ -183,7 +216,7 @@ const generateDVItem = (config, _ref3) => {
183
216
 
184
217
  if (config.title) {
185
218
  title.appendChild(document.createTextNode(config.title));
186
- svg.appendChild(title);
219
+ svgWrapper.appendChild(title);
187
220
  }
188
221
 
189
222
  const subtitleFontStyle = (0, _fontStyle.mergeFontStyleWithDefault)(fontStyle && fontStyle[_fontStyle.FONT_STYLE_VISUALIZATION_SUBTITLE], _fontStyle.FONT_STYLE_VISUALIZATION_SUBTITLE);
@@ -206,20 +239,24 @@ const generateDVItem = (config, _ref3) => {
206
239
 
207
240
  if (config.subtitle) {
208
241
  subtitle.appendChild(document.createTextNode(config.subtitle));
209
- svg.appendChild(subtitle);
242
+ svgWrapper.appendChild(subtitle);
210
243
  }
211
244
 
212
- svg.appendChild(generateValueSVG({
245
+ svgContainer.appendChild(svgWrapper);
246
+ svgContainer.appendChild(generateValueSVG({
213
247
  formattedValue: config.formattedValue,
214
248
  subText: config.subText,
215
249
  valueColor,
216
250
  noData,
217
- y: 20
251
+ icon,
252
+ containerWidth: width,
253
+ containerHeight: height
218
254
  }));
219
- return svg;
255
+ return svgContainer;
220
256
  };
221
257
 
222
- const shouldUseContrastColor = inputColor => {
258
+ const shouldUseContrastColor = function () {
259
+ let inputColor = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
223
260
  // based on https://stackoverflow.com/questions/3942878/how-to-decide-font-color-in-white-or-black-depending-on-background-color
224
261
  var color = inputColor.charAt(0) === '#' ? inputColor.substring(1, 7) : inputColor;
225
262
  var r = parseInt(color.substring(0, 2), 16); // hexToR
@@ -246,7 +283,8 @@ function _default(config, parentEl, _ref4) {
246
283
  legendSets,
247
284
  fontStyle,
248
285
  noData,
249
- legendOptions
286
+ legendOptions,
287
+ icon
250
288
  } = _ref4;
251
289
  const legendSet = legendOptions && legendSets[0];
252
290
  const legendColor = legendSet && (0, _legends.getColorByValueFromLegendSet)(legendSet, config.value);
@@ -264,26 +302,42 @@ function _default(config, parentEl, _ref4) {
264
302
  parentEl.style.overflow = 'hidden';
265
303
  parentEl.style.display = 'flex';
266
304
  parentEl.style.justifyContent = 'center';
305
+ const parentElBBox = parentEl.getBoundingClientRect();
306
+ const width = parentElBBox.width;
307
+ const height = parentElBBox.height;
308
+ const svgContainer = document.createElementNS(svgNS, 'svg');
309
+ svgContainer.setAttribute('xmlns', svgNS);
310
+ svgContainer.setAttribute('viewBox', "0 0 ".concat(width, " ").concat(height));
311
+ svgContainer.setAttribute('width', dashboard ? '100%' : width);
312
+ svgContainer.setAttribute('height', dashboard ? '100%' : height);
313
+ svgContainer.setAttribute('data-test', 'visualization-container');
267
314
 
268
315
  if (dashboard) {
269
316
  parentEl.style.borderRadius = _ui.spacers.dp8;
270
317
  return generateDashboardItem(config, {
318
+ svgContainer,
319
+ width,
320
+ height,
271
321
  valueColor,
272
322
  backgroundColor,
273
323
  noData,
274
- ...(shouldUseContrastColor(legendColor) ? {
324
+ icon,
325
+ ...(legendColor && shouldUseContrastColor(legendColor) ? {
275
326
  titleColor: _ui.colors.white
276
327
  } : {})
277
328
  });
278
329
  } else {
279
330
  parentEl.style.height = "100%";
280
331
  return generateDVItem(config, {
332
+ svgContainer,
333
+ width,
334
+ height,
281
335
  valueColor,
282
336
  backgroundColor,
283
337
  titleColor,
284
- parentEl,
285
- fontStyle,
286
- noData
338
+ noData,
339
+ icon,
340
+ fontStyle
287
341
  });
288
342
  }
289
343
  }
@@ -387,7 +387,8 @@ const CalculationModal = _ref => {
387
387
  loading: isCreatingCalculation || isUpdatingCalculation || isSavingCalculation,
388
388
  dataTest: "save-button"
389
389
  }, i18n.t('Save calculation')))))), showDeletePrompt && /*#__PURE__*/React.createElement(Modal, {
390
- small: true
390
+ small: true,
391
+ dataTest: "calculation-delete-modal"
391
392
  }, /*#__PURE__*/React.createElement(ModalTitle, null, i18n.t('Delete calculation')), /*#__PURE__*/React.createElement(ModalContent, null, i18n.t('Are you sure you want to delete this calculation? It may be used by other visualizations.')), /*#__PURE__*/React.createElement(ModalActions, null, /*#__PURE__*/React.createElement(ButtonStrip, {
392
393
  end: true
393
394
  }, /*#__PURE__*/React.createElement(Button, {
@@ -42,6 +42,7 @@ const DataElementOption = _ref => {
42
42
  className: "jsx-".concat(styles.__hash) + " " + (listeners && listeners.className != null && listeners.className || attributes && attributes.className != null && attributes.className || "draggable-item")
43
43
  }), /*#__PURE__*/React.createElement("div", {
44
44
  onDoubleClick: () => onDoubleClick(data),
45
+ "data-test": "data-element-option",
45
46
  className: "jsx-".concat(styles.__hash) + " " + "chip"
46
47
  }, /*#__PURE__*/React.createElement("span", {
47
48
  className: "jsx-".concat(styles.__hash) + " " + "icon"
@@ -51,11 +51,13 @@ const GroupSelector = _ref => {
51
51
  }, defaultGroup ? /*#__PURE__*/React.createElement(SingleSelectOption, {
52
52
  value: defaultGroup.id,
53
53
  key: defaultGroup.id,
54
- label: defaultGroup.getName()
54
+ label: defaultGroup.getName(),
55
+ dataTest: "data-element-group-select-option-".concat(defaultGroup.id)
55
56
  }) : null, !loading ? groups.map(group => /*#__PURE__*/React.createElement(SingleSelectOption, {
56
57
  value: group.id,
57
58
  key: group.id,
58
- label: group.name
59
+ label: group.name,
60
+ dataTest: "data-element-group-select-option-".concat(group.id)
59
61
  })) : null), /*#__PURE__*/React.createElement(_JSXStyle, {
60
62
  id: styles.__hash
61
63
  }, styles));
@@ -83,7 +85,8 @@ const DisaggregationSelector = _ref2 => {
83
85
  }, Object.entries(options).map(option => /*#__PURE__*/React.createElement(SingleSelectOption, {
84
86
  value: option[0],
85
87
  key: option[0],
86
- label: option[1]
88
+ label: option[1],
89
+ dataTest: "data-element-disaggregation-select-option-".concat(option[0])
87
90
  }))), /*#__PURE__*/React.createElement(_JSXStyle, {
88
91
  id: styles.__hash
89
92
  }, styles));
@@ -13,7 +13,7 @@ export const LAST_DROPZONE_ID = 'lastdropzone';
13
13
  export const FORMULA_BOX_ID = 'formulabox';
14
14
 
15
15
  const Placeholder = () => /*#__PURE__*/React.createElement("div", {
16
- "data-test": 'placeholder',
16
+ "data-test": "placeholder",
17
17
  className: "jsx-".concat(styles.__hash) + " " + "placeholder"
18
18
  }, /*#__PURE__*/React.createElement(FormulaIcon, null), /*#__PURE__*/React.createElement("span", {
19
19
  className: "jsx-".concat(styles.__hash) + " " + "help-text"
@@ -45,7 +45,7 @@ const FormulaField = _ref => {
45
45
  className: "jsx-".concat(styles.__hash) + " " + "border"
46
46
  }), /*#__PURE__*/React.createElement("div", {
47
47
  ref: setLastDropzoneRef,
48
- "data-test": 'formula-field',
48
+ "data-test": "formula-field",
49
49
  className: "jsx-".concat(styles.__hash) + " " + "formula-field"
50
50
  }, loading && /*#__PURE__*/React.createElement(Center, null, /*#__PURE__*/React.createElement(CircularLoader, {
51
51
  small: true
@@ -15,6 +15,7 @@ const MathOperatorSelector = _ref => {
15
15
  }, /*#__PURE__*/React.createElement("h4", {
16
16
  className: "jsx-".concat(styles.__hash) + " " + "sub-header"
17
17
  }, i18n.t('Math operators')), /*#__PURE__*/React.createElement("div", {
18
+ "data-test": "operators-list",
18
19
  className: "jsx-".concat(styles.__hash) + " " + "operators"
19
20
  }, getOperators().map((_ref2, index) => {
20
21
  let {
@@ -9,7 +9,7 @@ const LegendKey = _ref => {
9
9
  legendSets
10
10
  } = _ref;
11
11
  return legendSets.length ? /*#__PURE__*/React.createElement("div", {
12
- "data-test": 'legend-key-container',
12
+ "data-test": "legend-key-container",
13
13
  className: "jsx-".concat(styles.__hash) + " " + "container"
14
14
  }, legendSets.map((legendSet, index) => /*#__PURE__*/React.createElement("div", {
15
15
  key: legendSet.id,
@@ -1,62 +1,94 @@
1
1
  import { colors, spacers } 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'; // Compute text width before rendering
5
+ // Not exactly precise but close enough
6
+
7
+ const getTextWidth = (text, font) => {
8
+ const canvas = document.createElement('canvas');
9
+ const context = canvas.getContext('2d');
10
+ context.font = font;
11
+ return context.measureText(text).width;
12
+ };
5
13
 
6
14
  const generateValueSVG = _ref => {
7
15
  let {
8
16
  formattedValue,
9
17
  subText,
10
18
  valueColor,
19
+ icon,
11
20
  noData,
12
- y
21
+ containerWidth,
22
+ containerHeight
13
23
  } = _ref;
14
- const textSize = 300;
15
- 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);
24
+ const ratio = containerHeight / containerWidth;
25
+ const iconSize = 300;
26
+ const iconPadding = 50;
27
+ const textSize = iconSize * 0.85;
28
+ const textWidth = getTextWidth(formattedValue, "".concat(textSize, "px Roboto"));
29
+ const subTextSize = 40;
30
+ const showIcon = icon && formattedValue !== noData.text;
31
+ let viewBoxWidth = textWidth;
32
+
33
+ if (showIcon) {
34
+ viewBoxWidth += iconSize + iconPadding;
21
35
  }
22
36
 
37
+ const viewBoxHeight = viewBoxWidth * ratio;
38
+ const svgValue = document.createElementNS(svgNS, 'svg');
39
+ svgValue.setAttribute('viewBox', "0 0 ".concat(viewBoxWidth, " ").concat(viewBoxHeight));
40
+ svgValue.setAttribute('width', '95%');
41
+ svgValue.setAttribute('height', '95%');
42
+ svgValue.setAttribute('x', '50%');
43
+ svgValue.setAttribute('y', '50%');
44
+ svgValue.setAttribute('style', 'overflow: visible');
23
45
  let fillColor = colors.grey900;
24
46
 
25
47
  if (valueColor) {
26
48
  fillColor = valueColor;
27
49
  } else if (formattedValue === noData.text) {
28
50
  fillColor = colors.grey600;
51
+ } // show icon if configured in maintenance app
52
+
53
+
54
+ if (showIcon) {
55
+ // embed icon to allow changing color
56
+ // (elements with fill need to use "currentColor" for this to work)
57
+ const iconSvgNode = document.createElementNS(svgNS, 'svg');
58
+ iconSvgNode.setAttribute('width', iconSize);
59
+ iconSvgNode.setAttribute('height', iconSize);
60
+ iconSvgNode.setAttribute('viewBox', '0 0 48 48');
61
+ iconSvgNode.setAttribute('y', "-".concat(iconSize / 2));
62
+ iconSvgNode.setAttribute('x', "-".concat((iconSize + iconPadding + textWidth) / 2));
63
+ iconSvgNode.setAttribute('style', "color: ".concat(fillColor));
64
+ const parser = new DOMParser();
65
+ const svgIconDocument = parser.parseFromString(icon, 'image/svg+xml');
66
+ Array.from(svgIconDocument.documentElement.children).forEach(node => iconSvgNode.appendChild(node));
67
+ svgValue.appendChild(iconSvgNode);
29
68
  }
30
69
 
31
70
  const textNode = document.createElementNS(svgNS, 'text');
32
- textNode.setAttribute('text-anchor', 'middle');
33
71
  textNode.setAttribute('font-size', textSize);
34
72
  textNode.setAttribute('font-weight', '300');
35
73
  textNode.setAttribute('letter-spacing', '-5');
36
- textNode.setAttribute('x', '50%');
74
+ textNode.setAttribute('text-anchor', 'middle');
75
+ textNode.setAttribute('x', showIcon ? "".concat((iconSize + iconPadding) / 2) : 0); // vertical align, "alignment-baseline: central" is not supported by Batik
76
+
77
+ textNode.setAttribute('y', '.35em');
37
78
  textNode.setAttribute('fill', fillColor);
38
79
  textNode.setAttribute('data-test', 'visualization-primary-value');
39
80
  textNode.appendChild(document.createTextNode(formattedValue));
40
81
  svgValue.appendChild(textNode);
41
82
 
42
83
  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
84
  const subTextNode = document.createElementNS(svgNS, 'text');
52
85
  subTextNode.setAttribute('text-anchor', 'middle');
53
86
  subTextNode.setAttribute('font-size', subTextSize);
54
- subTextNode.setAttribute('x', '50%');
55
- subTextNode.setAttribute('x', '50%');
87
+ subTextNode.setAttribute('y', iconSize / 2);
88
+ subTextNode.setAttribute('dy', subTextSize);
56
89
  subTextNode.setAttribute('fill', colors.grey600);
57
90
  subTextNode.appendChild(document.createTextNode(subText));
58
- svgSubText.appendChild(subTextNode);
59
- svgValue.appendChild(svgSubText);
91
+ svgValue.appendChild(subTextNode);
60
92
  }
61
93
 
62
94
  return svgValue;
@@ -64,14 +96,27 @@ const generateValueSVG = _ref => {
64
96
 
65
97
  const generateDashboardItem = (config, _ref2) => {
66
98
  let {
99
+ svgContainer,
100
+ width,
101
+ height,
67
102
  valueColor,
68
103
  titleColor,
69
104
  backgroundColor,
70
- noData
105
+ noData,
106
+ icon
71
107
  } = _ref2;
108
+ svgContainer.appendChild(generateValueSVG({
109
+ formattedValue: config.formattedValue,
110
+ subText: config.subText,
111
+ valueColor,
112
+ noData,
113
+ icon,
114
+ containerWidth: width,
115
+ containerHeight: height
116
+ }));
72
117
  const container = document.createElement('div');
73
118
  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', ";");
119
+ const titleStyle = "padding: 0 8px; text-align: center; font-size: 12px; color: ".concat(titleColor || '#666', ";");
75
120
  const title = document.createElement('span');
76
121
  title.setAttribute('style', titleStyle);
77
122
 
@@ -82,18 +127,12 @@ const generateDashboardItem = (config, _ref2) => {
82
127
 
83
128
  if (config.subtitle) {
84
129
  const subtitle = document.createElement('span');
85
- subtitle.setAttribute('style', titleStyle + ' margin-top: 4px; padding: 0 8px');
130
+ subtitle.setAttribute('style', titleStyle + ' margin-top: 4px;');
86
131
  subtitle.appendChild(document.createTextNode(config.subtitle));
87
132
  container.appendChild(subtitle);
88
133
  }
89
134
 
90
- container.appendChild(generateValueSVG({
91
- formattedValue: config.formattedValue,
92
- subText: config.subText,
93
- valueColor,
94
- noData,
95
- y: 40
96
- }));
135
+ container.appendChild(svgContainer);
97
136
  return container;
98
137
  };
99
138
 
@@ -127,33 +166,27 @@ const getXFromTextAlign = textAlign => {
127
166
 
128
167
  const generateDVItem = (config, _ref3) => {
129
168
  let {
169
+ svgContainer,
170
+ width,
171
+ height,
130
172
  valueColor,
173
+ noData,
131
174
  backgroundColor,
132
175
  titleColor,
133
- parentEl,
134
176
  fontStyle,
135
- noData
177
+ icon
136
178
  } = _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
179
 
148
180
  if (backgroundColor) {
149
- svg.setAttribute('style', "background-color: ".concat(backgroundColor, ";"));
181
+ svgContainer.setAttribute('style', "background-color: ".concat(backgroundColor, ";"));
150
182
  const background = document.createElementNS(svgNS, 'rect');
151
183
  background.setAttribute('width', '100%');
152
184
  background.setAttribute('height', '100%');
153
185
  background.setAttribute('fill', backgroundColor);
154
- svg.appendChild(background);
186
+ svgContainer.appendChild(background);
155
187
  }
156
188
 
189
+ const svgWrapper = document.createElementNS(svgNS, 'svg');
157
190
  const title = document.createElementNS(svgNS, 'text');
158
191
  const titleFontStyle = mergeFontStyleWithDefault(fontStyle && fontStyle[FONT_STYLE_VISUALIZATION_TITLE], FONT_STYLE_VISUALIZATION_TITLE);
159
192
  title.setAttribute('x', getXFromTextAlign(titleFontStyle[FONT_STYLE_OPTION_TEXT_ALIGN]));
@@ -173,7 +206,7 @@ const generateDVItem = (config, _ref3) => {
173
206
 
174
207
  if (config.title) {
175
208
  title.appendChild(document.createTextNode(config.title));
176
- svg.appendChild(title);
209
+ svgWrapper.appendChild(title);
177
210
  }
178
211
 
179
212
  const subtitleFontStyle = mergeFontStyleWithDefault(fontStyle && fontStyle[FONT_STYLE_VISUALIZATION_SUBTITLE], FONT_STYLE_VISUALIZATION_SUBTITLE);
@@ -196,20 +229,24 @@ const generateDVItem = (config, _ref3) => {
196
229
 
197
230
  if (config.subtitle) {
198
231
  subtitle.appendChild(document.createTextNode(config.subtitle));
199
- svg.appendChild(subtitle);
232
+ svgWrapper.appendChild(subtitle);
200
233
  }
201
234
 
202
- svg.appendChild(generateValueSVG({
235
+ svgContainer.appendChild(svgWrapper);
236
+ svgContainer.appendChild(generateValueSVG({
203
237
  formattedValue: config.formattedValue,
204
238
  subText: config.subText,
205
239
  valueColor,
206
240
  noData,
207
- y: 20
241
+ icon,
242
+ containerWidth: width,
243
+ containerHeight: height
208
244
  }));
209
- return svg;
245
+ return svgContainer;
210
246
  };
211
247
 
212
- const shouldUseContrastColor = inputColor => {
248
+ const shouldUseContrastColor = function () {
249
+ let inputColor = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
213
250
  // based on https://stackoverflow.com/questions/3942878/how-to-decide-font-color-in-white-or-black-depending-on-background-color
214
251
  var color = inputColor.charAt(0) === '#' ? inputColor.substring(1, 7) : inputColor;
215
252
  var r = parseInt(color.substring(0, 2), 16); // hexToR
@@ -236,7 +273,8 @@ export default function (config, parentEl, _ref4) {
236
273
  legendSets,
237
274
  fontStyle,
238
275
  noData,
239
- legendOptions
276
+ legendOptions,
277
+ icon
240
278
  } = _ref4;
241
279
  const legendSet = legendOptions && legendSets[0];
242
280
  const legendColor = legendSet && getColorByValueFromLegendSet(legendSet, config.value);
@@ -254,26 +292,42 @@ export default function (config, parentEl, _ref4) {
254
292
  parentEl.style.overflow = 'hidden';
255
293
  parentEl.style.display = 'flex';
256
294
  parentEl.style.justifyContent = 'center';
295
+ const parentElBBox = parentEl.getBoundingClientRect();
296
+ const width = parentElBBox.width;
297
+ const height = parentElBBox.height;
298
+ const svgContainer = document.createElementNS(svgNS, 'svg');
299
+ svgContainer.setAttribute('xmlns', svgNS);
300
+ svgContainer.setAttribute('viewBox', "0 0 ".concat(width, " ").concat(height));
301
+ svgContainer.setAttribute('width', dashboard ? '100%' : width);
302
+ svgContainer.setAttribute('height', dashboard ? '100%' : height);
303
+ svgContainer.setAttribute('data-test', 'visualization-container');
257
304
 
258
305
  if (dashboard) {
259
306
  parentEl.style.borderRadius = spacers.dp8;
260
307
  return generateDashboardItem(config, {
308
+ svgContainer,
309
+ width,
310
+ height,
261
311
  valueColor,
262
312
  backgroundColor,
263
313
  noData,
264
- ...(shouldUseContrastColor(legendColor) ? {
314
+ icon,
315
+ ...(legendColor && shouldUseContrastColor(legendColor) ? {
265
316
  titleColor: colors.white
266
317
  } : {})
267
318
  });
268
319
  } else {
269
320
  parentEl.style.height = "100%";
270
321
  return generateDVItem(config, {
322
+ svgContainer,
323
+ width,
324
+ height,
271
325
  valueColor,
272
326
  backgroundColor,
273
327
  titleColor,
274
- parentEl,
275
- fontStyle,
276
- noData
328
+ noData,
329
+ icon,
330
+ fontStyle
277
331
  });
278
332
  }
279
333
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dhis2/analytics",
3
- "version": "25.0.0",
3
+ "version": "25.1.0",
4
4
  "main": "./build/cjs/index.js",
5
5
  "module": "./build/es/index.js",
6
6
  "exports": {