@dhis2/analytics 24.10.1 → 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.
Files changed (88) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/build/cjs/__demo__/CalculationModal.stories.js +448 -0
  3. package/build/cjs/api/analytics/AnalyticsRequest.js +12 -1
  4. package/build/cjs/api/dimensions.js +1 -1
  5. package/build/cjs/api/expression.js +67 -0
  6. package/build/cjs/assets/DimensionItemIcons/CalculationIcon.js +25 -0
  7. package/build/cjs/assets/FormulaIcon.js +40 -0
  8. package/build/cjs/components/DataDimension/Calculation/CalculationModal.js +448 -0
  9. package/build/cjs/components/DataDimension/Calculation/DataElementOption.js +78 -0
  10. package/build/cjs/components/DataDimension/Calculation/DataElementSelector.js +309 -0
  11. package/build/cjs/components/DataDimension/Calculation/DndContext.js +213 -0
  12. package/build/cjs/components/DataDimension/Calculation/DragHandleIcon.js +23 -0
  13. package/build/cjs/components/DataDimension/Calculation/DraggingItem.js +58 -0
  14. package/build/cjs/components/DataDimension/Calculation/DropZone.js +58 -0
  15. package/build/cjs/components/DataDimension/Calculation/FormulaField.js +121 -0
  16. package/build/cjs/components/DataDimension/Calculation/FormulaItem.js +232 -0
  17. package/build/cjs/components/DataDimension/Calculation/MathOperatorSelector.js +58 -0
  18. package/build/cjs/components/DataDimension/Calculation/Operator.js +81 -0
  19. package/build/cjs/components/DataDimension/Calculation/styles/CalculationModal.style.js +13 -0
  20. package/build/cjs/components/DataDimension/Calculation/styles/DataElementOption.style.js +13 -0
  21. package/build/cjs/components/DataDimension/Calculation/styles/DataElementSelector.style.js +13 -0
  22. package/build/cjs/components/DataDimension/Calculation/styles/DraggingItem.style.js +13 -0
  23. package/build/cjs/components/DataDimension/Calculation/styles/DropZone.style.js +13 -0
  24. package/build/cjs/components/DataDimension/Calculation/styles/FormulaField.style.js +13 -0
  25. package/build/cjs/components/DataDimension/Calculation/styles/FormulaItem.style.js +13 -0
  26. package/build/cjs/components/DataDimension/Calculation/styles/MathOperatorSelector.style.js +13 -0
  27. package/build/cjs/components/DataDimension/Calculation/styles/Operator.style.js +13 -0
  28. package/build/cjs/components/DataDimension/DataDimension.js +22 -6
  29. package/build/cjs/components/DataDimension/DataTypeSelector.js +5 -3
  30. package/build/cjs/components/DataDimension/ItemSelector.js +111 -73
  31. package/build/cjs/components/LegendKey/LegendKey.js +1 -1
  32. package/build/cjs/components/TransferOption.js +13 -4
  33. package/build/cjs/components/styles/DimensionSelector.style.js +2 -2
  34. package/build/cjs/components/styles/TransferOption.style.js +2 -2
  35. package/build/cjs/index.js +6 -0
  36. package/build/cjs/locales/en/translations.json +32 -7
  37. package/build/cjs/modules/__tests__/expressions.spec.js +139 -0
  38. package/build/cjs/modules/__tests__/hash.spec.js +92 -0
  39. package/build/cjs/modules/__tests__/parseExpression.spec.js +46 -0
  40. package/build/cjs/modules/dataTypes.js +8 -1
  41. package/build/cjs/modules/dimensionListItem.js +82 -0
  42. package/build/cjs/modules/expressions.js +164 -0
  43. package/build/cjs/modules/hash.js +28 -0
  44. package/build/cjs/visualizations/config/generators/dhis/singleValue.js +112 -58
  45. package/build/es/__demo__/CalculationModal.stories.js +440 -0
  46. package/build/es/api/analytics/AnalyticsRequest.js +11 -1
  47. package/build/es/api/dimensions.js +1 -1
  48. package/build/es/api/expression.js +57 -0
  49. package/build/es/assets/DimensionItemIcons/CalculationIcon.js +13 -0
  50. package/build/es/assets/FormulaIcon.js +30 -0
  51. package/build/es/components/DataDimension/Calculation/CalculationModal.js +419 -0
  52. package/build/es/components/DataDimension/Calculation/DataElementOption.js +61 -0
  53. package/build/es/components/DataDimension/Calculation/DataElementSelector.js +283 -0
  54. package/build/es/components/DataDimension/Calculation/DndContext.js +194 -0
  55. package/build/es/components/DataDimension/Calculation/DragHandleIcon.js +11 -0
  56. package/build/es/components/DataDimension/Calculation/DraggingItem.js +40 -0
  57. package/build/es/components/DataDimension/Calculation/DropZone.js +43 -0
  58. package/build/es/components/DataDimension/Calculation/FormulaField.js +98 -0
  59. package/build/es/components/DataDimension/Calculation/FormulaItem.js +207 -0
  60. package/build/es/components/DataDimension/Calculation/MathOperatorSelector.js +42 -0
  61. package/build/es/components/DataDimension/Calculation/Operator.js +64 -0
  62. package/build/es/components/DataDimension/Calculation/styles/CalculationModal.style.js +4 -0
  63. package/build/es/components/DataDimension/Calculation/styles/DataElementOption.style.js +4 -0
  64. package/build/es/components/DataDimension/Calculation/styles/DataElementSelector.style.js +4 -0
  65. package/build/es/components/DataDimension/Calculation/styles/DraggingItem.style.js +4 -0
  66. package/build/es/components/DataDimension/Calculation/styles/DropZone.style.js +4 -0
  67. package/build/es/components/DataDimension/Calculation/styles/FormulaField.style.js +4 -0
  68. package/build/es/components/DataDimension/Calculation/styles/FormulaItem.style.js +4 -0
  69. package/build/es/components/DataDimension/Calculation/styles/MathOperatorSelector.style.js +4 -0
  70. package/build/es/components/DataDimension/Calculation/styles/Operator.style.js +4 -0
  71. package/build/es/components/DataDimension/DataDimension.js +21 -6
  72. package/build/es/components/DataDimension/DataTypeSelector.js +6 -4
  73. package/build/es/components/DataDimension/ItemSelector.js +111 -73
  74. package/build/es/components/LegendKey/LegendKey.js +1 -1
  75. package/build/es/components/TransferOption.js +14 -5
  76. package/build/es/components/styles/DimensionSelector.style.js +2 -2
  77. package/build/es/components/styles/TransferOption.style.js +2 -2
  78. package/build/es/index.js +1 -1
  79. package/build/es/locales/en/translations.json +32 -7
  80. package/build/es/modules/__tests__/expressions.spec.js +136 -0
  81. package/build/es/modules/__tests__/hash.spec.js +88 -0
  82. package/build/es/modules/__tests__/parseExpression.spec.js +43 -0
  83. package/build/es/modules/dataTypes.js +6 -0
  84. package/build/es/modules/dimensionListItem.js +61 -0
  85. package/build/es/modules/expressions.js +131 -0
  86. package/build/es/modules/hash.js +12 -0
  87. package/build/es/visualizations/config/generators/dhis/singleValue.js +112 -58
  88. package/package.json +6 -1
@@ -0,0 +1,88 @@
1
+ import { DIMENSION_TYPE_EXPRESSION_DIMENSION_ITEM } from '../dataTypes.js';
2
+ import { getExpressionHashFromVisualization, getHash } from '../hash.js';
3
+ describe('getHash', () => {
4
+ const textInput = 'Raymond Luxury Yacht';
5
+ it('accepts a string and returns a hash', () => {
6
+ const hash = getHash(textInput);
7
+ expect(typeof hash).toBe('string');
8
+ expect(hash).not.toBe(textInput);
9
+ });
10
+ it('is deterministic', () => {
11
+ expect(getHash(textInput)).toBe(getHash(textInput));
12
+ });
13
+ it('returns undefined for invalid input', () => {
14
+ const unsupportedTypes = ['', 1, true, null, undefined, {}, []];
15
+ unsupportedTypes.forEach(type => expect(getHash(type)).toBe(undefined));
16
+ });
17
+ });
18
+ describe('getExpressionHashFromVisualization', () => {
19
+ const edi1 = {
20
+ id: 'OdiHJayrsKo',
21
+ dimensionItemType: DIMENSION_TYPE_EXPRESSION_DIMENSION_ITEM,
22
+ expression: '#{abc} * 10'
23
+ };
24
+ const edi2 = {
25
+ id: 'Uvn6LCg7dVU',
26
+ dimensionItemType: DIMENSION_TYPE_EXPRESSION_DIMENSION_ITEM,
27
+ expression: '#{abc} * 20'
28
+ };
29
+ const dxWithEdi = {
30
+ dimension: 'dx',
31
+ items: [edi1, edi2]
32
+ };
33
+ it('generates a hash (columns)', () => {
34
+ expect(typeof getExpressionHashFromVisualization({
35
+ columns: [dxWithEdi],
36
+ rows: [],
37
+ filters: []
38
+ })).toBe('string');
39
+ });
40
+ it('generates a hash (rows)', () => {
41
+ expect(typeof getExpressionHashFromVisualization({
42
+ columns: [],
43
+ rows: [dxWithEdi],
44
+ filters: []
45
+ })).toBe('string');
46
+ });
47
+ it('generates a hash (filters)', () => {
48
+ expect(typeof getExpressionHashFromVisualization({
49
+ columns: [],
50
+ rows: [],
51
+ filters: [dxWithEdi]
52
+ })).toBe('string');
53
+ });
54
+ it('does not generate a hash when there are no dimensions', () => {
55
+ expect(getExpressionHashFromVisualization({
56
+ columns: [],
57
+ rows: [],
58
+ filters: []
59
+ })).toBe(undefined);
60
+ });
61
+ it('does not generate a hash when there are no EDI dimensions', () => {
62
+ expect(getExpressionHashFromVisualization({
63
+ columns: [{
64
+ id: 'OdiHJayrsKo',
65
+ dimensionItemType: 'INDICATOR'
66
+ }],
67
+ rows: [],
68
+ filters: []
69
+ })).toBe(undefined);
70
+ });
71
+ it('sorts the edi objects by id before generating the hash to optimize caching', () => {
72
+ expect(getExpressionHashFromVisualization({
73
+ columns: [{
74
+ dimension: 'dx',
75
+ items: [edi1, edi2]
76
+ }],
77
+ rows: [],
78
+ filters: []
79
+ })).toBe(getExpressionHashFromVisualization({
80
+ columns: [{
81
+ dimension: 'dx',
82
+ items: [edi2, edi1]
83
+ }],
84
+ rows: [],
85
+ filters: []
86
+ }));
87
+ });
88
+ });
@@ -0,0 +1,43 @@
1
+ import { parseExpression } from '../expressions.js';
2
+ test('matches numbers and operators', () => {
3
+ expect(parseExpression('1+2-3*4/5')).toEqual(['1', '+', '2', '-', '3', '*', '4', '/', '5']);
4
+ });
5
+ test('matches #{} with letters, numbers and operators', () => {
6
+ expect(parseExpression('#{abc123}+100-200*300/400')).toEqual(['#{abc123}', '+', '100', '-', '200', '*', '300', '/', '400']);
7
+ });
8
+ test('matches numbers and operators with brackets', () => {
9
+ expect(parseExpression('1+(2-3)*4/5')).toEqual(['1', '+', '(', '2', '-', '3', ')', '*', '4', '/', '5']);
10
+ });
11
+ test('matches #{} with letters, numbers and operators with brackets', () => {
12
+ expect(parseExpression('(100-200)+#{abc123}*300/400')).toEqual(['(', '100', '-', '200', ')', '+', '#{abc123}', '*', '300', '/', '400']);
13
+ });
14
+ test('matches #{} with numbers and operators with brackets', () => {
15
+ expect(parseExpression('#{123}+(10-200)*3000/40000')).toEqual(['#{123}', '+', '(', '10', '-', '200', ')', '*', '3000', '/', '40000']);
16
+ });
17
+ test('matches #{} with letters and numbers only', () => {
18
+ expect(parseExpression('#{abc123}')).toEqual(['#{abc123}']);
19
+ });
20
+ test('matches #{} with numbers only', () => {
21
+ expect(parseExpression('#{123}')).toEqual(['#{123}']);
22
+ });
23
+ test('matches #{} with letters only', () => {
24
+ expect(parseExpression('#{abc}')).toEqual(['#{abc}']);
25
+ });
26
+ test('matches #{} with no input', () => {
27
+ expect(parseExpression('')).toEqual([]);
28
+ });
29
+ test('matches multiple #{} with operators', () => {
30
+ expect(parseExpression('#{abc123}+#{def456}')).toEqual(['#{abc123}', '+', '#{def456}']);
31
+ });
32
+ test('matches multiple #{} containing dots with operators', () => {
33
+ expect(parseExpression('#{abc123.xyz999}+#{def456.xyz999}')).toEqual(['#{abc123.xyz999}', '+', '#{def456.xyz999}']);
34
+ });
35
+ test('matches multiple #{} with operators with brackets', () => {
36
+ expect(parseExpression('(#{abc123}/#{def456})*#{ghi789}')).toEqual(['(', '#{abc123}', '/', '#{def456}', ')', '*', '#{ghi789}']);
37
+ });
38
+ test('matches on decimal numbers', () => {
39
+ expect(parseExpression('#{abc123}*1.2/#{def456}')).toEqual(['#{abc123}', '*', '1.2', '/', '#{def456}']);
40
+ });
41
+ test('matches on decimal numbers with #{} containing dots', () => {
42
+ expect(parseExpression('1.2+#{abc123.xyz999}')).toEqual(['1.2', '+', '#{abc123.xyz999}']);
43
+ });
@@ -15,6 +15,7 @@ export const DIMENSION_TYPE_DATA = 'DATA_X';
15
15
  export const DIMENSION_TYPE_PERIOD = 'PERIOD';
16
16
  export const DIMENSION_TYPE_ORGANISATION_UNIT = 'ORGANISATION_UNIT';
17
17
  export const DIMENSION_TYPE_ORGANISATION_UNIT_GROUP_SET = 'ORGANISATION_UNIT_GROUP_SET';
18
+ export const DIMENSION_TYPE_EXPRESSION_DIMENSION_ITEM = 'EXPRESSION_DIMENSION_ITEM';
18
19
  export const TOTALS = 'totals';
19
20
  export const DETAIL = 'detail';
20
21
  export const SUB_GROUP_DETAIL = 'DETAIL';
@@ -88,6 +89,11 @@ export const dataTypeMap = {
88
89
  getItemName: () => i18n.t('Program indicator'),
89
90
  getGroupEmptyLabel: () => i18n.t('No programs found'),
90
91
  getGroupLoadingLabel: () => i18n.t('Loading programs')
92
+ },
93
+ [DIMENSION_TYPE_EXPRESSION_DIMENSION_ITEM]: {
94
+ id: DIMENSION_TYPE_EXPRESSION_DIMENSION_ITEM,
95
+ getName: () => i18n.t('Calculations'),
96
+ getItemName: () => i18n.t('Calculation')
91
97
  }
92
98
  };
93
99
  export function defaultGroupId(dataType) {
@@ -0,0 +1,61 @@
1
+ import { IconDimensionDataSet16, IconDimensionIndicator16, IconDimensionEventDataItem16, IconDimensionProgramIndicator16 } from '@dhis2/ui';
2
+ import React from 'react';
3
+ import DataElementIcon from '../assets/DimensionItemIcons/DataElementIcon.js';
4
+ import GenericIcon from '../assets/DimensionItemIcons/GenericIcon.js';
5
+ import CalculationIcon from './../assets/DimensionItemIcons/CalculationIcon.js';
6
+ import { REPORTING_RATE } from './dataSets.js';
7
+ import { DIMENSION_TYPE_DATA_ELEMENT, DIMENSION_TYPE_DATA_ELEMENT_OPERAND, DIMENSION_TYPE_DATA_SET, DIMENSION_TYPE_EVENT_DATA_ITEM, DIMENSION_TYPE_EXPRESSION_DIMENSION_ITEM, DIMENSION_TYPE_PROGRAM_ATTRIBUTE, DIMENSION_TYPE_PROGRAM_DATA_ELEMENT, dataTypeMap as dataTypes, DIMENSION_TYPE_INDICATOR, DIMENSION_TYPE_PROGRAM_INDICATOR } from './dataTypes.js';
8
+ export const getTooltipText = _ref => {
9
+ var _dataTypes$type;
10
+
11
+ let {
12
+ type,
13
+ expression
14
+ } = _ref;
15
+
16
+ if (type === DIMENSION_TYPE_EXPRESSION_DIMENSION_ITEM && expression) {
17
+ return dataTypes[DIMENSION_TYPE_EXPRESSION_DIMENSION_ITEM].getItemName();
18
+ }
19
+
20
+ switch (type) {
21
+ case DIMENSION_TYPE_DATA_ELEMENT_OPERAND:
22
+ return dataTypes[DIMENSION_TYPE_DATA_ELEMENT].getItemName();
23
+
24
+ case REPORTING_RATE:
25
+ return dataTypes[DIMENSION_TYPE_DATA_SET].getItemName();
26
+
27
+ case DIMENSION_TYPE_PROGRAM_DATA_ELEMENT:
28
+ case DIMENSION_TYPE_PROGRAM_ATTRIBUTE:
29
+ return dataTypes[DIMENSION_TYPE_EVENT_DATA_ITEM].getItemName();
30
+
31
+ default:
32
+ return (_dataTypes$type = dataTypes[type]) === null || _dataTypes$type === void 0 ? void 0 : _dataTypes$type.getItemName();
33
+ }
34
+ };
35
+ export const getIcon = type => {
36
+ switch (type) {
37
+ case DIMENSION_TYPE_INDICATOR:
38
+ return /*#__PURE__*/React.createElement(IconDimensionIndicator16, null);
39
+
40
+ case DIMENSION_TYPE_DATA_ELEMENT_OPERAND:
41
+ case DIMENSION_TYPE_DATA_ELEMENT:
42
+ return DataElementIcon;
43
+
44
+ case REPORTING_RATE:
45
+ return /*#__PURE__*/React.createElement(IconDimensionDataSet16, null);
46
+
47
+ case DIMENSION_TYPE_EVENT_DATA_ITEM:
48
+ case DIMENSION_TYPE_PROGRAM_DATA_ELEMENT:
49
+ case DIMENSION_TYPE_PROGRAM_ATTRIBUTE:
50
+ return /*#__PURE__*/React.createElement(IconDimensionEventDataItem16, null);
51
+
52
+ case DIMENSION_TYPE_PROGRAM_INDICATOR:
53
+ return /*#__PURE__*/React.createElement(IconDimensionProgramIndicator16, null);
54
+
55
+ case DIMENSION_TYPE_EXPRESSION_DIMENSION_ITEM:
56
+ return CalculationIcon;
57
+
58
+ default:
59
+ return GenericIcon;
60
+ }
61
+ };
@@ -0,0 +1,131 @@
1
+ import i18n from '../locales/index.js';
2
+ export const EXPRESSION_TYPE_NUMBER = 'EXPRESSION_TYPE_NUMBER';
3
+ export const EXPRESSION_TYPE_OPERATOR = 'EXPRESSION_TYPE_OPERATOR';
4
+ export const EXPRESSION_TYPE_DATA = 'EXPRESSION_TYPE_DATA';
5
+ export const VALID_EXPRESSION = 'OK';
6
+ export const INVALID_EXPRESSION = 'ERROR';
7
+ export const getOperators = () => [{
8
+ value: '+',
9
+ label: '+',
10
+ type: EXPRESSION_TYPE_OPERATOR
11
+ }, {
12
+ value: '-',
13
+ label: '-',
14
+ type: EXPRESSION_TYPE_OPERATOR
15
+ }, {
16
+ value: '*',
17
+ label: '×',
18
+ type: EXPRESSION_TYPE_OPERATOR
19
+ }, {
20
+ value: '/',
21
+ label: '/',
22
+ type: EXPRESSION_TYPE_OPERATOR
23
+ }, {
24
+ value: '(',
25
+ label: '(',
26
+ type: EXPRESSION_TYPE_OPERATOR
27
+ }, {
28
+ value: ')',
29
+ label: ')',
30
+ type: EXPRESSION_TYPE_OPERATOR
31
+ }, {
32
+ value: '',
33
+ label: i18n.t('Number'),
34
+ type: EXPRESSION_TYPE_NUMBER
35
+ }];
36
+ export const parseExpression = input => {
37
+ const regex = /(#{[a-zA-Z0-9#.]+}|[+\-*/()])|(\d+(\.\d+)?)/g;
38
+ return input.match(regex) || [];
39
+ };
40
+ export const parseExpressionToArray = function () {
41
+ let expression = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
42
+ let metadata = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
43
+ return parseExpression(expression).map(value => {
44
+ if (value.startsWith('#{') && value.endsWith('}')) {
45
+ var _metadata$find;
46
+
47
+ const id = value.slice(2, -1);
48
+ const label = ((_metadata$find = metadata.find(item => item.id === id)) === null || _metadata$find === void 0 ? void 0 : _metadata$find.name) || id;
49
+ return {
50
+ value,
51
+ label,
52
+ type: EXPRESSION_TYPE_DATA
53
+ };
54
+ }
55
+
56
+ if (isNaN(value)) {
57
+ return {
58
+ value,
59
+ label: getOperators().find(op => op.value === value).label,
60
+ type: EXPRESSION_TYPE_OPERATOR
61
+ };
62
+ }
63
+
64
+ return {
65
+ value,
66
+ label: value,
67
+ type: EXPRESSION_TYPE_NUMBER
68
+ };
69
+ }) || [];
70
+ };
71
+ export const parseArrayToExpression = function () {
72
+ let input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
73
+ return input.map(item => item.value).join('');
74
+ };
75
+ export const validateExpression = expression => {
76
+ let result;
77
+ const leftParenthesisCount = expression.split('(').length - 1;
78
+ const rightParenthesisCount = expression.split(')').length - 1;
79
+
80
+ if (!expression) {
81
+ // empty formula
82
+ result = {
83
+ status: INVALID_EXPRESSION,
84
+ message: i18n.t('Formula is empty. Add items to the formula from the lists on the left.')
85
+ }; // TODO: reimplement this but allow negative values, e.g. 10 / -5
86
+ // } else if (/[-+/*]{2,}/.test(expression)) {
87
+ // // two math operators next to each other
88
+ // result = {
89
+ // status: INVALID_EXPRESSION,
90
+ // message: i18n.t('Consecutive math operators'),
91
+ // }
92
+ } else if (/}#/.test(expression)) {
93
+ // two data elements next to each other
94
+ result = {
95
+ status: INVALID_EXPRESSION,
96
+ message: i18n.t('Consecutive data elements')
97
+ };
98
+ } else if (/^[+\-*/]|[+\-*/]$/.test(expression)) {
99
+ // starting or ending with a math operator
100
+ result = {
101
+ status: INVALID_EXPRESSION,
102
+ message: i18n.t('Starts or ends with a math operator')
103
+ };
104
+ } else if (/\(\)/.test(expression)) {
105
+ // contains an empty set of parentheses
106
+ result = {
107
+ status: INVALID_EXPRESSION,
108
+ message: i18n.t('Empty parentheses')
109
+ };
110
+ } else if (leftParenthesisCount > rightParenthesisCount) {
111
+ // ( but no )
112
+ result = {
113
+ status: INVALID_EXPRESSION,
114
+ message: i18n.t('Missing right parenthesis )')
115
+ };
116
+ } else if (rightParenthesisCount > leftParenthesisCount) {
117
+ // ) but no (
118
+ result = {
119
+ status: INVALID_EXPRESSION,
120
+ message: i18n.t('Missing left parenthesis (')
121
+ };
122
+ }
123
+
124
+ return result;
125
+ };
126
+ export const getItemIdsFromExpression = function () {
127
+ let expression = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
128
+ const regex = /#{([a-zA-Z0-9#]+.*?)}/g;
129
+ const matches = expression.match(regex);
130
+ return matches ? matches.map(match => match.slice(2, -1)) : [];
131
+ };
@@ -0,0 +1,12 @@
1
+ import SHA1 from 'crypto-js/sha1';
2
+ import { DIMENSION_TYPE_EXPRESSION_DIMENSION_ITEM } from './dataTypes.js';
3
+ import { layoutGetAllItems } from './layout/layoutGetAllItems.js';
4
+
5
+ const isValidValue = value => typeof value === 'string' && value.length;
6
+
7
+ export const getHash = value => isValidValue(value) ? SHA1(value).toString() : undefined;
8
+ export const getExpressionHashFromVisualization = visualization => {
9
+ var _layoutGetAllItems;
10
+
11
+ return getHash((_layoutGetAllItems = layoutGetAllItems(visualization)) === null || _layoutGetAllItems === void 0 ? void 0 : _layoutGetAllItems.filter(item => item.dimensionItemType === DIMENSION_TYPE_EXPRESSION_DIMENSION_ITEM && isValidValue(item.expression)).sort((i1, i2) => i1.id < i2.id ? -1 : i1.id > i2.id ? 1 : 0).map(edi => edi.expression).join(''));
12
+ };
@@ -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
  }