@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.
- package/CHANGELOG.md +19 -0
- package/build/cjs/__demo__/CalculationModal.stories.js +448 -0
- package/build/cjs/api/analytics/AnalyticsRequest.js +12 -1
- package/build/cjs/api/dimensions.js +1 -1
- package/build/cjs/api/expression.js +67 -0
- package/build/cjs/assets/DimensionItemIcons/CalculationIcon.js +25 -0
- package/build/cjs/assets/FormulaIcon.js +40 -0
- package/build/cjs/components/DataDimension/Calculation/CalculationModal.js +448 -0
- package/build/cjs/components/DataDimension/Calculation/DataElementOption.js +78 -0
- package/build/cjs/components/DataDimension/Calculation/DataElementSelector.js +309 -0
- package/build/cjs/components/DataDimension/Calculation/DndContext.js +213 -0
- package/build/cjs/components/DataDimension/Calculation/DragHandleIcon.js +23 -0
- package/build/cjs/components/DataDimension/Calculation/DraggingItem.js +58 -0
- package/build/cjs/components/DataDimension/Calculation/DropZone.js +58 -0
- package/build/cjs/components/DataDimension/Calculation/FormulaField.js +121 -0
- package/build/cjs/components/DataDimension/Calculation/FormulaItem.js +232 -0
- package/build/cjs/components/DataDimension/Calculation/MathOperatorSelector.js +58 -0
- package/build/cjs/components/DataDimension/Calculation/Operator.js +81 -0
- package/build/cjs/components/DataDimension/Calculation/styles/CalculationModal.style.js +13 -0
- package/build/cjs/components/DataDimension/Calculation/styles/DataElementOption.style.js +13 -0
- package/build/cjs/components/DataDimension/Calculation/styles/DataElementSelector.style.js +13 -0
- package/build/cjs/components/DataDimension/Calculation/styles/DraggingItem.style.js +13 -0
- package/build/cjs/components/DataDimension/Calculation/styles/DropZone.style.js +13 -0
- package/build/cjs/components/DataDimension/Calculation/styles/FormulaField.style.js +13 -0
- package/build/cjs/components/DataDimension/Calculation/styles/FormulaItem.style.js +13 -0
- package/build/cjs/components/DataDimension/Calculation/styles/MathOperatorSelector.style.js +13 -0
- package/build/cjs/components/DataDimension/Calculation/styles/Operator.style.js +13 -0
- package/build/cjs/components/DataDimension/DataDimension.js +22 -6
- package/build/cjs/components/DataDimension/DataTypeSelector.js +5 -3
- package/build/cjs/components/DataDimension/ItemSelector.js +111 -73
- package/build/cjs/components/LegendKey/LegendKey.js +1 -1
- package/build/cjs/components/TransferOption.js +13 -4
- package/build/cjs/components/styles/DimensionSelector.style.js +2 -2
- package/build/cjs/components/styles/TransferOption.style.js +2 -2
- package/build/cjs/index.js +6 -0
- package/build/cjs/locales/en/translations.json +32 -7
- package/build/cjs/modules/__tests__/expressions.spec.js +139 -0
- package/build/cjs/modules/__tests__/hash.spec.js +92 -0
- package/build/cjs/modules/__tests__/parseExpression.spec.js +46 -0
- package/build/cjs/modules/dataTypes.js +8 -1
- package/build/cjs/modules/dimensionListItem.js +82 -0
- package/build/cjs/modules/expressions.js +164 -0
- package/build/cjs/modules/hash.js +28 -0
- package/build/cjs/visualizations/config/generators/dhis/singleValue.js +112 -58
- package/build/es/__demo__/CalculationModal.stories.js +440 -0
- package/build/es/api/analytics/AnalyticsRequest.js +11 -1
- package/build/es/api/dimensions.js +1 -1
- package/build/es/api/expression.js +57 -0
- package/build/es/assets/DimensionItemIcons/CalculationIcon.js +13 -0
- package/build/es/assets/FormulaIcon.js +30 -0
- package/build/es/components/DataDimension/Calculation/CalculationModal.js +419 -0
- package/build/es/components/DataDimension/Calculation/DataElementOption.js +61 -0
- package/build/es/components/DataDimension/Calculation/DataElementSelector.js +283 -0
- package/build/es/components/DataDimension/Calculation/DndContext.js +194 -0
- package/build/es/components/DataDimension/Calculation/DragHandleIcon.js +11 -0
- package/build/es/components/DataDimension/Calculation/DraggingItem.js +40 -0
- package/build/es/components/DataDimension/Calculation/DropZone.js +43 -0
- package/build/es/components/DataDimension/Calculation/FormulaField.js +98 -0
- package/build/es/components/DataDimension/Calculation/FormulaItem.js +207 -0
- package/build/es/components/DataDimension/Calculation/MathOperatorSelector.js +42 -0
- package/build/es/components/DataDimension/Calculation/Operator.js +64 -0
- package/build/es/components/DataDimension/Calculation/styles/CalculationModal.style.js +4 -0
- package/build/es/components/DataDimension/Calculation/styles/DataElementOption.style.js +4 -0
- package/build/es/components/DataDimension/Calculation/styles/DataElementSelector.style.js +4 -0
- package/build/es/components/DataDimension/Calculation/styles/DraggingItem.style.js +4 -0
- package/build/es/components/DataDimension/Calculation/styles/DropZone.style.js +4 -0
- package/build/es/components/DataDimension/Calculation/styles/FormulaField.style.js +4 -0
- package/build/es/components/DataDimension/Calculation/styles/FormulaItem.style.js +4 -0
- package/build/es/components/DataDimension/Calculation/styles/MathOperatorSelector.style.js +4 -0
- package/build/es/components/DataDimension/Calculation/styles/Operator.style.js +4 -0
- package/build/es/components/DataDimension/DataDimension.js +21 -6
- package/build/es/components/DataDimension/DataTypeSelector.js +6 -4
- package/build/es/components/DataDimension/ItemSelector.js +111 -73
- package/build/es/components/LegendKey/LegendKey.js +1 -1
- package/build/es/components/TransferOption.js +14 -5
- package/build/es/components/styles/DimensionSelector.style.js +2 -2
- package/build/es/components/styles/TransferOption.style.js +2 -2
- package/build/es/index.js +1 -1
- package/build/es/locales/en/translations.json +32 -7
- package/build/es/modules/__tests__/expressions.spec.js +136 -0
- package/build/es/modules/__tests__/hash.spec.js +88 -0
- package/build/es/modules/__tests__/parseExpression.spec.js +43 -0
- package/build/es/modules/dataTypes.js +6 -0
- package/build/es/modules/dimensionListItem.js +61 -0
- package/build/es/modules/expressions.js +131 -0
- package/build/es/modules/hash.js +12 -0
- package/build/es/visualizations/config/generators/dhis/singleValue.js +112 -58
- 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
|
-
|
|
21
|
+
containerWidth,
|
|
22
|
+
containerHeight
|
|
13
23
|
} = _ref;
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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('
|
|
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('
|
|
55
|
-
subTextNode.setAttribute('
|
|
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
|
-
|
|
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;
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
232
|
+
svgWrapper.appendChild(subtitle);
|
|
200
233
|
}
|
|
201
234
|
|
|
202
|
-
|
|
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
|
-
|
|
241
|
+
icon,
|
|
242
|
+
containerWidth: width,
|
|
243
|
+
containerHeight: height
|
|
208
244
|
}));
|
|
209
|
-
return
|
|
245
|
+
return svgContainer;
|
|
210
246
|
};
|
|
211
247
|
|
|
212
|
-
const shouldUseContrastColor =
|
|
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
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
328
|
+
noData,
|
|
329
|
+
icon,
|
|
330
|
+
fontStyle
|
|
277
331
|
});
|
|
278
332
|
}
|
|
279
333
|
}
|