@dhis2/analytics 25.0.0 → 25.1.1

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
+ ## [25.1.1](https://github.com/dhis2/analytics/compare/v25.1.0...v25.1.1) (2023-05-02)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * address various SV SVG issues after KFMT ([#1456](https://github.com/dhis2/analytics/issues/1456)) ([f0ee1f1](https://github.com/dhis2/analytics/commit/f0ee1f16450e1b6de7c879c8ecdeec7c1d7c89fb))
7
+
8
+ # [25.1.0](https://github.com/dhis2/analytics/compare/v25.0.0...v25.1.0) (2023-04-27)
9
+
10
+
11
+ ### Features
12
+
13
+ * icon in SV visualization DHIS2-10496 ([#1440](https://github.com/dhis2/analytics/issues/1440)) ([e6563ca](https://github.com/dhis2/analytics/commit/e6563cacc5e901a04d5432330b09b685936ddd70))
14
+
1
15
  # [25.0.0](https://github.com/dhis2/analytics/compare/v24.10.1...v25.0.0) (2023-04-24)
2
16
 
3
17
 
@@ -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,95 @@ 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
+ textColor,
30
+ icon,
21
31
  noData,
22
- y
32
+ containerWidth,
33
+ containerHeight
23
34
  } = _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);
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
+ const showIcon = icon && formattedValue !== noData.text;
42
+ let viewBoxWidth = textWidth;
43
+
44
+ if (showIcon) {
45
+ viewBoxWidth += iconSize + iconPadding;
31
46
  }
32
47
 
48
+ const viewBoxHeight = viewBoxWidth * ratio;
49
+ const svgValue = document.createElementNS(svgNS, 'svg');
50
+ svgValue.setAttribute('viewBox', "0 0 ".concat(viewBoxWidth, " ").concat(viewBoxHeight));
51
+ svgValue.setAttribute('width', '95%');
52
+ svgValue.setAttribute('height', '95%');
53
+ svgValue.setAttribute('x', '50%');
54
+ svgValue.setAttribute('y', '50%');
55
+ svgValue.setAttribute('style', 'overflow: visible');
33
56
  let fillColor = _ui.colors.grey900;
34
57
 
35
58
  if (valueColor) {
36
59
  fillColor = valueColor;
37
60
  } else if (formattedValue === noData.text) {
38
61
  fillColor = _ui.colors.grey600;
62
+ } // show icon if configured in maintenance app
63
+
64
+
65
+ if (showIcon) {
66
+ // embed icon to allow changing color
67
+ // (elements with fill need to use "currentColor" for this to work)
68
+ const iconSvgNode = document.createElementNS(svgNS, 'svg');
69
+ iconSvgNode.setAttribute('width', iconSize);
70
+ iconSvgNode.setAttribute('height', iconSize);
71
+ iconSvgNode.setAttribute('viewBox', '0 0 48 48');
72
+ iconSvgNode.setAttribute('y', "-".concat(iconSize / 2));
73
+ iconSvgNode.setAttribute('x', "-".concat((iconSize + iconPadding + textWidth) / 2));
74
+ iconSvgNode.setAttribute('style', "color: ".concat(fillColor));
75
+ const parser = new DOMParser();
76
+ const svgIconDocument = parser.parseFromString(icon, 'image/svg+xml');
77
+ Array.from(svgIconDocument.documentElement.children).forEach(node => iconSvgNode.appendChild(node));
78
+ svgValue.appendChild(iconSvgNode);
39
79
  }
40
80
 
41
81
  const textNode = document.createElementNS(svgNS, 'text');
42
- textNode.setAttribute('text-anchor', 'middle');
43
82
  textNode.setAttribute('font-size', textSize);
44
83
  textNode.setAttribute('font-weight', '300');
45
84
  textNode.setAttribute('letter-spacing', '-5');
46
- textNode.setAttribute('x', '50%');
85
+ textNode.setAttribute('text-anchor', 'middle');
86
+ textNode.setAttribute('x', showIcon ? "".concat((iconSize + iconPadding) / 2) : 0); // vertical align, "alignment-baseline: central" is not supported by Batik
87
+
88
+ textNode.setAttribute('y', '.35em');
47
89
  textNode.setAttribute('fill', fillColor);
48
90
  textNode.setAttribute('data-test', 'visualization-primary-value');
49
91
  textNode.appendChild(document.createTextNode(formattedValue));
50
92
  svgValue.appendChild(textNode);
51
93
 
52
94
  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
95
  const subTextNode = document.createElementNS(svgNS, 'text');
62
96
  subTextNode.setAttribute('text-anchor', 'middle');
63
97
  subTextNode.setAttribute('font-size', subTextSize);
64
- subTextNode.setAttribute('x', '50%');
65
- subTextNode.setAttribute('x', '50%');
66
- subTextNode.setAttribute('fill', _ui.colors.grey600);
98
+ subTextNode.setAttribute('y', iconSize / 2);
99
+ subTextNode.setAttribute('dy', subTextSize);
100
+ subTextNode.setAttribute('fill', textColor);
67
101
  subTextNode.appendChild(document.createTextNode(subText));
68
- svgSubText.appendChild(subTextNode);
69
- svgValue.appendChild(svgSubText);
102
+ svgValue.appendChild(subTextNode);
70
103
  }
71
104
 
72
105
  return svgValue;
@@ -74,14 +107,28 @@ const generateValueSVG = _ref => {
74
107
 
75
108
  const generateDashboardItem = (config, _ref2) => {
76
109
  let {
110
+ svgContainer,
111
+ width,
112
+ height,
77
113
  valueColor,
78
114
  titleColor,
79
115
  backgroundColor,
80
- noData
116
+ noData,
117
+ icon
81
118
  } = _ref2;
119
+ svgContainer.appendChild(generateValueSVG({
120
+ formattedValue: config.formattedValue,
121
+ subText: config.subText,
122
+ valueColor,
123
+ textColor: titleColor,
124
+ noData,
125
+ icon,
126
+ containerWidth: width,
127
+ containerHeight: height
128
+ }));
82
129
  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', ";");
130
+ 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, ";") : ''));
131
+ const titleStyle = "padding: 0 8px; text-align: center; font-size: 12px; color: ".concat(titleColor || '#666', ";");
85
132
  const title = document.createElement('span');
86
133
  title.setAttribute('style', titleStyle);
87
134
 
@@ -92,18 +139,12 @@ const generateDashboardItem = (config, _ref2) => {
92
139
 
93
140
  if (config.subtitle) {
94
141
  const subtitle = document.createElement('span');
95
- subtitle.setAttribute('style', titleStyle + ' margin-top: 4px; padding: 0 8px');
142
+ subtitle.setAttribute('style', titleStyle + ' margin-top: 4px;');
96
143
  subtitle.appendChild(document.createTextNode(config.subtitle));
97
144
  container.appendChild(subtitle);
98
145
  }
99
146
 
100
- container.appendChild(generateValueSVG({
101
- formattedValue: config.formattedValue,
102
- subText: config.subText,
103
- valueColor,
104
- noData,
105
- y: 40
106
- }));
147
+ container.appendChild(svgContainer);
107
148
  return container;
108
149
  };
109
150
 
@@ -137,37 +178,32 @@ const getXFromTextAlign = textAlign => {
137
178
 
138
179
  const generateDVItem = (config, _ref3) => {
139
180
  let {
181
+ svgContainer,
182
+ width,
183
+ height,
140
184
  valueColor,
185
+ noData,
141
186
  backgroundColor,
142
187
  titleColor,
143
- parentEl,
144
188
  fontStyle,
145
- noData
189
+ icon
146
190
  } = _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
191
 
158
192
  if (backgroundColor) {
159
- svg.setAttribute('style', "background-color: ".concat(backgroundColor, ";"));
193
+ svgContainer.setAttribute('style', "background-color: ".concat(backgroundColor, ";"));
160
194
  const background = document.createElementNS(svgNS, 'rect');
161
195
  background.setAttribute('width', '100%');
162
196
  background.setAttribute('height', '100%');
163
197
  background.setAttribute('fill', backgroundColor);
164
- svg.appendChild(background);
198
+ svgContainer.appendChild(background);
165
199
  }
166
200
 
201
+ const svgWrapper = document.createElementNS(svgNS, 'svg');
167
202
  const title = document.createElementNS(svgNS, 'text');
168
203
  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];
169
205
  title.setAttribute('x', getXFromTextAlign(titleFontStyle[_fontStyle.FONT_STYLE_OPTION_TEXT_ALIGN]));
170
- title.setAttribute('y', 28);
206
+ title.setAttribute('y', titleYPosition);
171
207
  title.setAttribute('text-anchor', getTextAnchorFromTextAlign(titleFontStyle[_fontStyle.FONT_STYLE_OPTION_TEXT_ALIGN]));
172
208
  title.setAttribute('font-size', "".concat(titleFontStyle[_fontStyle.FONT_STYLE_OPTION_FONT_SIZE], "px"));
173
209
  title.setAttribute('font-weight', titleFontStyle[_fontStyle.FONT_STYLE_OPTION_BOLD] ? _fontStyle.FONT_STYLE_OPTION_BOLD : 'normal');
@@ -183,14 +219,14 @@ const generateDVItem = (config, _ref3) => {
183
219
 
184
220
  if (config.title) {
185
221
  title.appendChild(document.createTextNode(config.title));
186
- svg.appendChild(title);
222
+ svgWrapper.appendChild(title);
187
223
  }
188
224
 
189
225
  const subtitleFontStyle = (0, _fontStyle.mergeFontStyleWithDefault)(fontStyle && fontStyle[_fontStyle.FONT_STYLE_VISUALIZATION_SUBTITLE], _fontStyle.FONT_STYLE_VISUALIZATION_SUBTITLE);
190
226
  const subtitle = document.createElementNS(svgNS, 'text');
191
227
  subtitle.setAttribute('x', getXFromTextAlign(subtitleFontStyle[_fontStyle.FONT_STYLE_OPTION_TEXT_ALIGN]));
192
- subtitle.setAttribute('y', 28);
193
- subtitle.setAttribute('dy', 22);
228
+ subtitle.setAttribute('y', titleYPosition);
229
+ subtitle.setAttribute('dy', "".concat(subtitleFontStyle[_fontStyle.FONT_STYLE_OPTION_FONT_SIZE] + 4));
194
230
  subtitle.setAttribute('text-anchor', getTextAnchorFromTextAlign(subtitleFontStyle[_fontStyle.FONT_STYLE_OPTION_TEXT_ALIGN]));
195
231
  subtitle.setAttribute('font-size', "".concat(subtitleFontStyle[_fontStyle.FONT_STYLE_OPTION_FONT_SIZE], "px"));
196
232
  subtitle.setAttribute('font-weight', subtitleFontStyle[_fontStyle.FONT_STYLE_OPTION_BOLD] ? _fontStyle.FONT_STYLE_OPTION_BOLD : 'normal');
@@ -206,20 +242,25 @@ const generateDVItem = (config, _ref3) => {
206
242
 
207
243
  if (config.subtitle) {
208
244
  subtitle.appendChild(document.createTextNode(config.subtitle));
209
- svg.appendChild(subtitle);
245
+ svgWrapper.appendChild(subtitle);
210
246
  }
211
247
 
212
- svg.appendChild(generateValueSVG({
248
+ svgContainer.appendChild(svgWrapper);
249
+ svgContainer.appendChild(generateValueSVG({
213
250
  formattedValue: config.formattedValue,
214
251
  subText: config.subText,
215
252
  valueColor,
253
+ textColor: titleColor,
216
254
  noData,
217
- y: 20
255
+ icon,
256
+ containerWidth: width,
257
+ containerHeight: height
218
258
  }));
219
- return svg;
259
+ return svgContainer;
220
260
  };
221
261
 
222
- const shouldUseContrastColor = inputColor => {
262
+ const shouldUseContrastColor = function () {
263
+ let inputColor = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
223
264
  // based on https://stackoverflow.com/questions/3942878/how-to-decide-font-color-in-white-or-black-depending-on-background-color
224
265
  var color = inputColor.charAt(0) === '#' ? inputColor.substring(1, 7) : inputColor;
225
266
  var r = parseInt(color.substring(0, 2), 16); // hexToR
@@ -246,7 +287,8 @@ function _default(config, parentEl, _ref4) {
246
287
  legendSets,
247
288
  fontStyle,
248
289
  noData,
249
- legendOptions
290
+ legendOptions,
291
+ icon
250
292
  } = _ref4;
251
293
  const legendSet = legendOptions && legendSets[0];
252
294
  const legendColor = legendSet && (0, _legends.getColorByValueFromLegendSet)(legendSet, config.value);
@@ -264,26 +306,42 @@ function _default(config, parentEl, _ref4) {
264
306
  parentEl.style.overflow = 'hidden';
265
307
  parentEl.style.display = 'flex';
266
308
  parentEl.style.justifyContent = 'center';
309
+ const parentElBBox = parentEl.getBoundingClientRect();
310
+ const width = parentElBBox.width;
311
+ const height = parentElBBox.height;
312
+ const svgContainer = document.createElementNS(svgNS, 'svg');
313
+ svgContainer.setAttribute('xmlns', svgNS);
314
+ svgContainer.setAttribute('viewBox', "0 0 ".concat(width, " ").concat(height));
315
+ svgContainer.setAttribute('width', dashboard ? '100%' : width);
316
+ svgContainer.setAttribute('height', dashboard ? '100%' : height);
317
+ svgContainer.setAttribute('data-test', 'visualization-container');
267
318
 
268
319
  if (dashboard) {
269
320
  parentEl.style.borderRadius = _ui.spacers.dp8;
270
321
  return generateDashboardItem(config, {
322
+ svgContainer,
323
+ width,
324
+ height,
271
325
  valueColor,
272
326
  backgroundColor,
273
327
  noData,
274
- ...(shouldUseContrastColor(legendColor) ? {
328
+ icon,
329
+ ...(legendOptions.style === _legends.LEGEND_DISPLAY_STYLE_FILL && legendColor && shouldUseContrastColor(legendColor) ? {
275
330
  titleColor: _ui.colors.white
276
331
  } : {})
277
332
  });
278
333
  } else {
279
334
  parentEl.style.height = "100%";
280
335
  return generateDVItem(config, {
336
+ svgContainer,
337
+ width,
338
+ height,
281
339
  valueColor,
282
340
  backgroundColor,
283
341
  titleColor,
284
- parentEl,
285
- fontStyle,
286
- noData
342
+ noData,
343
+ icon,
344
+ fontStyle
287
345
  });
288
346
  }
289
347
  }
@@ -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,95 @@
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
+ textColor,
20
+ icon,
11
21
  noData,
12
- y
22
+ containerWidth,
23
+ containerHeight
13
24
  } = _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);
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
+ const showIcon = icon && formattedValue !== noData.text;
32
+ let viewBoxWidth = textWidth;
33
+
34
+ if (showIcon) {
35
+ viewBoxWidth += iconSize + iconPadding;
21
36
  }
22
37
 
38
+ const viewBoxHeight = viewBoxWidth * ratio;
39
+ const svgValue = document.createElementNS(svgNS, 'svg');
40
+ svgValue.setAttribute('viewBox', "0 0 ".concat(viewBoxWidth, " ").concat(viewBoxHeight));
41
+ svgValue.setAttribute('width', '95%');
42
+ svgValue.setAttribute('height', '95%');
43
+ svgValue.setAttribute('x', '50%');
44
+ svgValue.setAttribute('y', '50%');
45
+ svgValue.setAttribute('style', 'overflow: visible');
23
46
  let fillColor = colors.grey900;
24
47
 
25
48
  if (valueColor) {
26
49
  fillColor = valueColor;
27
50
  } else if (formattedValue === noData.text) {
28
51
  fillColor = colors.grey600;
52
+ } // show icon if configured in maintenance app
53
+
54
+
55
+ if (showIcon) {
56
+ // embed icon to allow changing color
57
+ // (elements with fill need to use "currentColor" for this to work)
58
+ const iconSvgNode = document.createElementNS(svgNS, 'svg');
59
+ iconSvgNode.setAttribute('width', iconSize);
60
+ iconSvgNode.setAttribute('height', iconSize);
61
+ iconSvgNode.setAttribute('viewBox', '0 0 48 48');
62
+ iconSvgNode.setAttribute('y', "-".concat(iconSize / 2));
63
+ iconSvgNode.setAttribute('x', "-".concat((iconSize + iconPadding + textWidth) / 2));
64
+ iconSvgNode.setAttribute('style', "color: ".concat(fillColor));
65
+ const parser = new DOMParser();
66
+ const svgIconDocument = parser.parseFromString(icon, 'image/svg+xml');
67
+ Array.from(svgIconDocument.documentElement.children).forEach(node => iconSvgNode.appendChild(node));
68
+ svgValue.appendChild(iconSvgNode);
29
69
  }
30
70
 
31
71
  const textNode = document.createElementNS(svgNS, 'text');
32
- textNode.setAttribute('text-anchor', 'middle');
33
72
  textNode.setAttribute('font-size', textSize);
34
73
  textNode.setAttribute('font-weight', '300');
35
74
  textNode.setAttribute('letter-spacing', '-5');
36
- textNode.setAttribute('x', '50%');
75
+ textNode.setAttribute('text-anchor', 'middle');
76
+ textNode.setAttribute('x', showIcon ? "".concat((iconSize + iconPadding) / 2) : 0); // vertical align, "alignment-baseline: central" is not supported by Batik
77
+
78
+ textNode.setAttribute('y', '.35em');
37
79
  textNode.setAttribute('fill', fillColor);
38
80
  textNode.setAttribute('data-test', 'visualization-primary-value');
39
81
  textNode.appendChild(document.createTextNode(formattedValue));
40
82
  svgValue.appendChild(textNode);
41
83
 
42
84
  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
85
  const subTextNode = document.createElementNS(svgNS, 'text');
52
86
  subTextNode.setAttribute('text-anchor', 'middle');
53
87
  subTextNode.setAttribute('font-size', subTextSize);
54
- subTextNode.setAttribute('x', '50%');
55
- subTextNode.setAttribute('x', '50%');
56
- subTextNode.setAttribute('fill', colors.grey600);
88
+ subTextNode.setAttribute('y', iconSize / 2);
89
+ subTextNode.setAttribute('dy', subTextSize);
90
+ subTextNode.setAttribute('fill', textColor);
57
91
  subTextNode.appendChild(document.createTextNode(subText));
58
- svgSubText.appendChild(subTextNode);
59
- svgValue.appendChild(svgSubText);
92
+ svgValue.appendChild(subTextNode);
60
93
  }
61
94
 
62
95
  return svgValue;
@@ -64,14 +97,28 @@ const generateValueSVG = _ref => {
64
97
 
65
98
  const generateDashboardItem = (config, _ref2) => {
66
99
  let {
100
+ svgContainer,
101
+ width,
102
+ height,
67
103
  valueColor,
68
104
  titleColor,
69
105
  backgroundColor,
70
- noData
106
+ noData,
107
+ icon
71
108
  } = _ref2;
109
+ svgContainer.appendChild(generateValueSVG({
110
+ formattedValue: config.formattedValue,
111
+ subText: config.subText,
112
+ valueColor,
113
+ textColor: titleColor,
114
+ noData,
115
+ icon,
116
+ containerWidth: width,
117
+ containerHeight: height
118
+ }));
72
119
  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', ";");
120
+ 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, ";") : ''));
121
+ const titleStyle = "padding: 0 8px; text-align: center; font-size: 12px; color: ".concat(titleColor || '#666', ";");
75
122
  const title = document.createElement('span');
76
123
  title.setAttribute('style', titleStyle);
77
124
 
@@ -82,18 +129,12 @@ const generateDashboardItem = (config, _ref2) => {
82
129
 
83
130
  if (config.subtitle) {
84
131
  const subtitle = document.createElement('span');
85
- subtitle.setAttribute('style', titleStyle + ' margin-top: 4px; padding: 0 8px');
132
+ subtitle.setAttribute('style', titleStyle + ' margin-top: 4px;');
86
133
  subtitle.appendChild(document.createTextNode(config.subtitle));
87
134
  container.appendChild(subtitle);
88
135
  }
89
136
 
90
- container.appendChild(generateValueSVG({
91
- formattedValue: config.formattedValue,
92
- subText: config.subText,
93
- valueColor,
94
- noData,
95
- y: 40
96
- }));
137
+ container.appendChild(svgContainer);
97
138
  return container;
98
139
  };
99
140
 
@@ -127,37 +168,32 @@ const getXFromTextAlign = textAlign => {
127
168
 
128
169
  const generateDVItem = (config, _ref3) => {
129
170
  let {
171
+ svgContainer,
172
+ width,
173
+ height,
130
174
  valueColor,
175
+ noData,
131
176
  backgroundColor,
132
177
  titleColor,
133
- parentEl,
134
178
  fontStyle,
135
- noData
179
+ icon
136
180
  } = _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
181
 
148
182
  if (backgroundColor) {
149
- svg.setAttribute('style', "background-color: ".concat(backgroundColor, ";"));
183
+ svgContainer.setAttribute('style', "background-color: ".concat(backgroundColor, ";"));
150
184
  const background = document.createElementNS(svgNS, 'rect');
151
185
  background.setAttribute('width', '100%');
152
186
  background.setAttribute('height', '100%');
153
187
  background.setAttribute('fill', backgroundColor);
154
- svg.appendChild(background);
188
+ svgContainer.appendChild(background);
155
189
  }
156
190
 
191
+ const svgWrapper = document.createElementNS(svgNS, 'svg');
157
192
  const title = document.createElementNS(svgNS, 'text');
158
193
  const titleFontStyle = mergeFontStyleWithDefault(fontStyle && fontStyle[FONT_STYLE_VISUALIZATION_TITLE], FONT_STYLE_VISUALIZATION_TITLE);
194
+ const titleYPosition = titleFontStyle[FONT_STYLE_OPTION_FONT_SIZE];
159
195
  title.setAttribute('x', getXFromTextAlign(titleFontStyle[FONT_STYLE_OPTION_TEXT_ALIGN]));
160
- title.setAttribute('y', 28);
196
+ title.setAttribute('y', titleYPosition);
161
197
  title.setAttribute('text-anchor', getTextAnchorFromTextAlign(titleFontStyle[FONT_STYLE_OPTION_TEXT_ALIGN]));
162
198
  title.setAttribute('font-size', "".concat(titleFontStyle[FONT_STYLE_OPTION_FONT_SIZE], "px"));
163
199
  title.setAttribute('font-weight', titleFontStyle[FONT_STYLE_OPTION_BOLD] ? FONT_STYLE_OPTION_BOLD : 'normal');
@@ -173,14 +209,14 @@ const generateDVItem = (config, _ref3) => {
173
209
 
174
210
  if (config.title) {
175
211
  title.appendChild(document.createTextNode(config.title));
176
- svg.appendChild(title);
212
+ svgWrapper.appendChild(title);
177
213
  }
178
214
 
179
215
  const subtitleFontStyle = mergeFontStyleWithDefault(fontStyle && fontStyle[FONT_STYLE_VISUALIZATION_SUBTITLE], FONT_STYLE_VISUALIZATION_SUBTITLE);
180
216
  const subtitle = document.createElementNS(svgNS, 'text');
181
217
  subtitle.setAttribute('x', getXFromTextAlign(subtitleFontStyle[FONT_STYLE_OPTION_TEXT_ALIGN]));
182
- subtitle.setAttribute('y', 28);
183
- subtitle.setAttribute('dy', 22);
218
+ subtitle.setAttribute('y', titleYPosition);
219
+ subtitle.setAttribute('dy', "".concat(subtitleFontStyle[FONT_STYLE_OPTION_FONT_SIZE] + 4));
184
220
  subtitle.setAttribute('text-anchor', getTextAnchorFromTextAlign(subtitleFontStyle[FONT_STYLE_OPTION_TEXT_ALIGN]));
185
221
  subtitle.setAttribute('font-size', "".concat(subtitleFontStyle[FONT_STYLE_OPTION_FONT_SIZE], "px"));
186
222
  subtitle.setAttribute('font-weight', subtitleFontStyle[FONT_STYLE_OPTION_BOLD] ? FONT_STYLE_OPTION_BOLD : 'normal');
@@ -196,20 +232,25 @@ const generateDVItem = (config, _ref3) => {
196
232
 
197
233
  if (config.subtitle) {
198
234
  subtitle.appendChild(document.createTextNode(config.subtitle));
199
- svg.appendChild(subtitle);
235
+ svgWrapper.appendChild(subtitle);
200
236
  }
201
237
 
202
- svg.appendChild(generateValueSVG({
238
+ svgContainer.appendChild(svgWrapper);
239
+ svgContainer.appendChild(generateValueSVG({
203
240
  formattedValue: config.formattedValue,
204
241
  subText: config.subText,
205
242
  valueColor,
243
+ textColor: titleColor,
206
244
  noData,
207
- y: 20
245
+ icon,
246
+ containerWidth: width,
247
+ containerHeight: height
208
248
  }));
209
- return svg;
249
+ return svgContainer;
210
250
  };
211
251
 
212
- const shouldUseContrastColor = inputColor => {
252
+ const shouldUseContrastColor = function () {
253
+ let inputColor = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
213
254
  // based on https://stackoverflow.com/questions/3942878/how-to-decide-font-color-in-white-or-black-depending-on-background-color
214
255
  var color = inputColor.charAt(0) === '#' ? inputColor.substring(1, 7) : inputColor;
215
256
  var r = parseInt(color.substring(0, 2), 16); // hexToR
@@ -236,7 +277,8 @@ export default function (config, parentEl, _ref4) {
236
277
  legendSets,
237
278
  fontStyle,
238
279
  noData,
239
- legendOptions
280
+ legendOptions,
281
+ icon
240
282
  } = _ref4;
241
283
  const legendSet = legendOptions && legendSets[0];
242
284
  const legendColor = legendSet && getColorByValueFromLegendSet(legendSet, config.value);
@@ -254,26 +296,42 @@ export default function (config, parentEl, _ref4) {
254
296
  parentEl.style.overflow = 'hidden';
255
297
  parentEl.style.display = 'flex';
256
298
  parentEl.style.justifyContent = 'center';
299
+ const parentElBBox = parentEl.getBoundingClientRect();
300
+ const width = parentElBBox.width;
301
+ const height = parentElBBox.height;
302
+ const svgContainer = document.createElementNS(svgNS, 'svg');
303
+ svgContainer.setAttribute('xmlns', svgNS);
304
+ svgContainer.setAttribute('viewBox', "0 0 ".concat(width, " ").concat(height));
305
+ svgContainer.setAttribute('width', dashboard ? '100%' : width);
306
+ svgContainer.setAttribute('height', dashboard ? '100%' : height);
307
+ svgContainer.setAttribute('data-test', 'visualization-container');
257
308
 
258
309
  if (dashboard) {
259
310
  parentEl.style.borderRadius = spacers.dp8;
260
311
  return generateDashboardItem(config, {
312
+ svgContainer,
313
+ width,
314
+ height,
261
315
  valueColor,
262
316
  backgroundColor,
263
317
  noData,
264
- ...(shouldUseContrastColor(legendColor) ? {
318
+ icon,
319
+ ...(legendOptions.style === LEGEND_DISPLAY_STYLE_FILL && legendColor && shouldUseContrastColor(legendColor) ? {
265
320
  titleColor: colors.white
266
321
  } : {})
267
322
  });
268
323
  } else {
269
324
  parentEl.style.height = "100%";
270
325
  return generateDVItem(config, {
326
+ svgContainer,
327
+ width,
328
+ height,
271
329
  valueColor,
272
330
  backgroundColor,
273
331
  titleColor,
274
- parentEl,
275
- fontStyle,
276
- noData
332
+ noData,
333
+ icon,
334
+ fontStyle
277
335
  });
278
336
  }
279
337
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dhis2/analytics",
3
- "version": "25.0.0",
3
+ "version": "25.1.1",
4
4
  "main": "./build/cjs/index.js",
5
5
  "module": "./build/es/index.js",
6
6
  "exports": {