diff --git a/CHANGELOG.md b/CHANGELOG.md index 564922249..f366e4f6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,45 @@ +# [26.2.0](https://github.com/dhis2/analytics/compare/v26.1.8...v26.2.0) (2023-12-14) + + +### Features + +* implement cumulative values in PT engine (DHIS2-5497) ([#1567](https://github.com/dhis2/analytics/issues/1567)) ([2b404b4](https://github.com/dhis2/analytics/commit/2b404b423cfb49347bdf57125870bb7b55338322)) + +## [26.1.8](https://github.com/dhis2/analytics/compare/v26.1.7...v26.1.8) (2023-11-26) + + +### Bug Fixes + +* **translations:** sync translations from transifex (master) ([43a607b](https://github.com/dhis2/analytics/commit/43a607ba0310b19edd07d866f15337e736567b2f)) + +## [26.1.7](https://github.com/dhis2/analytics/compare/v26.1.6...v26.1.7) (2023-11-19) + + +### Bug Fixes + +* **translations:** sync translations from transifex (master) ([cb4a052](https://github.com/dhis2/analytics/commit/cb4a052a19ebdced3ea7b6ffb7d1c40064abac38)) + +## [26.1.6](https://github.com/dhis2/analytics/compare/v26.1.5...v26.1.6) (2023-11-07) + + +### Bug Fixes + +* use json+patch for PATCH requests ([#1593](https://github.com/dhis2/analytics/issues/1593)) ([bdb0b9c](https://github.com/dhis2/analytics/commit/bdb0b9ceaa5ef102ef9768038c763b70a3d5e7d1)) + +## [26.1.5](https://github.com/dhis2/analytics/compare/v26.1.4...v26.1.5) (2023-11-03) + + +### Bug Fixes + +* use correct colors in "basic" color set ([#1592](https://github.com/dhis2/analytics/issues/1592)) ([6356250](https://github.com/dhis2/analytics/commit/63562501bd8b020742bb7ee8b17621975790c3c6)) + +## [26.1.4](https://github.com/dhis2/analytics/compare/v26.1.3...v26.1.4) (2023-10-29) + + +### Bug Fixes + +* **translations:** sync translations from transifex (master) ([435e077](https://github.com/dhis2/analytics/commit/435e07717c586365dfa67ad9ffb8acd5167e9ea2)) + ## [26.1.3](https://github.com/dhis2/analytics/compare/v26.1.2...v26.1.3) (2023-10-22) diff --git a/i18n/uz_UZ_Cyrl.po b/i18n/uz_UZ_Cyrl.po index 8a67b872d..9b9dc5769 100644 --- a/i18n/uz_UZ_Cyrl.po +++ b/i18n/uz_UZ_Cyrl.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2023-07-06T08:30:33.216Z\n" +"POT-Creation-Date: 2023-09-27T14:15:13.876Z\n" "PO-Revision-Date: 2020-04-28 22:05+0000\n" "Last-Translator: Ibatov , 2023\n" "Language-Team: Uzbek (Cyrillic) (https://app.transifex.com/hisp-uio/teams/100509/uz@Cyrl/)\n" @@ -77,7 +77,7 @@ msgid "This app could not retrieve required data." msgstr "" msgid "Network error" -msgstr "" +msgstr "Тармоқда хатолик" msgid "Data / Edit calculation" msgstr "" @@ -237,7 +237,7 @@ msgid "Dimension recommended with selected data" msgstr "Танланган маълумотлар учун созламалар тавсия этилади" msgid "All items" -msgstr "" +msgstr "Барча элементлар" msgid "Automatically include all items" msgstr "" @@ -333,7 +333,7 @@ msgid "map" msgstr "харита" msgid "visualization" -msgstr "" +msgstr "Визуализация" msgid "Edit" msgstr "Таҳрирлаш" @@ -373,6 +373,10 @@ msgstr "" msgid "Write an interpretation" msgstr "Талқин ёзиш" +msgid "" +"Other people viewing this interpretation in the future may see more data." +msgstr "" + msgid "Post interpretation" msgstr "" @@ -595,10 +599,10 @@ msgid_plural "{{count}} groups" msgstr[0] "" msgid "Selected: {{commaSeparatedListOfOrganisationUnits}}" -msgstr "" +msgstr "Танланган: {{commaSeparatedListOfOrganisationUnits}}" msgid "Nothing selected" -msgstr "" +msgstr "Ҳеч нима танланмаган" msgid "User organisation unit" msgstr "Ташкилий бўлим фойдаланувчиси" @@ -922,7 +926,7 @@ msgid "Indicator group" msgstr "Индикатор гуруҳи" msgid "All groups" -msgstr "" +msgstr "Барча гуруҳлар" msgid "Indicator" msgstr "Индикатор" diff --git a/i18n/zh.po b/i18n/zh.po index 2f8962ac4..90fabf54a 100644 --- a/i18n/zh.po +++ b/i18n/zh.po @@ -1,14 +1,14 @@ # # Translators: # Viktor Varland , 2021 -# phil_dhis2, 2022 +# Philip Larsen Donnelly, 2022 # 晓东 林 <13981924470@126.com>, 2023 # easylin , 2023 # msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2023-07-06T08:30:33.216Z\n" +"POT-Creation-Date: 2023-09-27T14:15:13.876Z\n" "PO-Revision-Date: 2020-04-28 22:05+0000\n" "Last-Translator: easylin , 2023\n" "Language-Team: Chinese (https://app.transifex.com/hisp-uio/teams/100509/zh/)\n" @@ -376,6 +376,10 @@ msgstr "隐藏解释" msgid "Write an interpretation" msgstr "书写注释" +msgid "" +"Other people viewing this interpretation in the future may see more data." +msgstr "将来查看此解释的其他人可能会看到更多数据。" + msgid "Post interpretation" msgstr "后解释" diff --git a/i18n/zh_CN.po b/i18n/zh_CN.po index 1d45656ca..654ffecea 100644 --- a/i18n/zh_CN.po +++ b/i18n/zh_CN.po @@ -1,14 +1,14 @@ # # Translators: -# easylin , 2023 # 晓东 林 <13981924470@126.com>, 2023 +# easylin , 2023 # msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2023-07-06T08:30:33.216Z\n" +"POT-Creation-Date: 2023-09-27T14:15:13.876Z\n" "PO-Revision-Date: 2020-04-28 22:05+0000\n" -"Last-Translator: 晓东 林 <13981924470@126.com>, 2023\n" +"Last-Translator: easylin , 2023\n" "Language-Team: Chinese (China) (https://app.transifex.com/hisp-uio/teams/100509/zh_CN/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -78,7 +78,7 @@ msgid "This app could not retrieve required data." msgstr "" msgid "Network error" -msgstr "" +msgstr "网络错误" msgid "Data / Edit calculation" msgstr "" @@ -374,6 +374,10 @@ msgstr "" msgid "Write an interpretation" msgstr "" +msgid "" +"Other people viewing this interpretation in the future may see more data." +msgstr "" + msgid "Post interpretation" msgstr "" @@ -596,10 +600,10 @@ msgid_plural "{{count}} groups" msgstr[0] "" msgid "Selected: {{commaSeparatedListOfOrganisationUnits}}" -msgstr "" +msgstr "已选择:{{commaSeparatedListOfOrganisationUnits}}" msgid "Nothing selected" -msgstr "" +msgstr "未选择任何内容" msgid "User organisation unit" msgstr "用户所在机构" @@ -611,7 +615,7 @@ msgid "User sub-x2-units" msgstr "二级下属机构" msgid "Select a level" -msgstr "" +msgstr "选择一个等级" msgid "Select a group" msgstr "" diff --git a/package.json b/package.json index d9e922203..1eb2d51a0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@dhis2/analytics", - "version": "26.1.3", + "version": "26.2.0", "main": "./build/cjs/index.js", "module": "./build/es/index.js", "exports": { diff --git a/src/__demo__/PivotTable.stories.js b/src/__demo__/PivotTable.stories.js index e06782146..93ca49ae9 100644 --- a/src/__demo__/PivotTable.stories.js +++ b/src/__demo__/PivotTable.stories.js @@ -784,6 +784,26 @@ storiesOf('PivotTable', module).add( } ) +storiesOf('PivotTable', module).add( + 'cumulative + empty columns (weekly) - shown', + (_, { pivotTableOptions }) => { + const visualization = { + ...weeklyColumnsVisualization, + ...pivotTableOptions, + hideEmptyColumns: false, + cumulativeValues: true, + } + return ( +
+ +
+ ) + } +) + storiesOf('PivotTable', module).add( 'empty columns (weekly) - hidden', (_, { pivotTableOptions }) => { @@ -803,6 +823,26 @@ storiesOf('PivotTable', module).add( } ) +storiesOf('PivotTable', module).add( + 'cumulative + empty columns (weekly) - hidden', + (_, { pivotTableOptions }) => { + const visualization = { + ...weeklyColumnsVisualization, + ...pivotTableOptions, + hideEmptyColumns: true, + cumulativeValues: true, + } + return ( +
+ +
+ ) + } +) + storiesOf('PivotTable', module).add( 'empty columns + assigned cats (shown)', (_, { pivotTableOptions }) => { diff --git a/src/api/expression.js b/src/api/expression.js index 2b7e85bb6..64bb61c30 100644 --- a/src/api/expression.js +++ b/src/api/expression.js @@ -11,11 +11,14 @@ export const createCalculationMutation = { } export const updateCalculationMutation = { - type: 'update', + type: 'json-patch', resource: 'expressionDimensionItems', - partial: true, id: ({ id }) => id, - data: ({ name, expression }) => ({ name, shortName: name, expression }), + data: ({ name, expression }) => [ + { op: 'add', path: '/name', value: name }, + { op: 'add', path: '/shortName', value: name }, + { op: 'add', path: '/expression', value: expression }, + ], } export const deleteCalculationMutation = { diff --git a/src/components/FileMenu/RenameDialog.js b/src/components/FileMenu/RenameDialog.js index 9692e5838..fe8f64328 100644 --- a/src/components/FileMenu/RenameDialog.js +++ b/src/components/FileMenu/RenameDialog.js @@ -19,12 +19,25 @@ import { labelForFileType, } from './utils.js' +const formatPayload = (name, description) => { + const payload = [{ op: 'add', path: '/name', value: name }] + + if (description) { + payload.push({ + op: 'add', + path: '/description', + value: description, + }) + } + + return payload +} + const getMutation = (type) => ({ resource: endpointFromFileType(type), id: ({ id }) => id, - type: 'update', - partial: true, - data: ({ name, description }) => ({ name, description }), + type: 'json-patch', + data: ({ name, description }) => formatPayload(name, description), }) export const RenameDialog = ({ type, object, onClose, onRename, onError }) => { @@ -52,7 +65,7 @@ export const RenameDialog = ({ type, object, onClose, onRename, onError }) => { } return ( - + {i18n.t('Rename {{fileType}}', { @@ -67,6 +80,7 @@ export const RenameDialog = ({ type, object, onClose, onRename, onError }) => { required value={name} onChange={({ value }) => setName(value)} + dataTest="file-menu-rename-modal-name" /> { value={description} rows={3} onChange={({ value }) => setDescription(value)} + dataTest="file-menu-rename-modal-description" /> - - diff --git a/src/components/Options/VisualizationOptions.js b/src/components/Options/VisualizationOptions.js index 3bb3e3174..c8eb10e72 100644 --- a/src/components/Options/VisualizationOptions.js +++ b/src/components/Options/VisualizationOptions.js @@ -18,6 +18,7 @@ import { modalContent, tabSection, tabSectionTitle, + tabSectionTitleDisabled, tabSectionTitleMargin, tabSectionOption, tabSectionOptionItem, @@ -95,6 +96,7 @@ const VisualizationOptions = ({ {tabContent.styles} {tabSection.styles} {tabSectionTitle.styles} + {tabSectionTitleDisabled.styles} {tabSectionTitleMargin.styles} {tabSectionOption.styles} {tabSectionOptionItem.styles} diff --git a/src/components/Options/styles/VisualizationOptions.style.js b/src/components/Options/styles/VisualizationOptions.style.js index bcd74f517..61c87803f 100644 --- a/src/components/Options/styles/VisualizationOptions.style.js +++ b/src/components/Options/styles/VisualizationOptions.style.js @@ -51,6 +51,12 @@ export const tabSectionTitle = css.resolve` } ` +export const tabSectionTitleDisabled = css.resolve` + span { + color: ${colors.grey600}; + } +` + export const tabSectionTitleMargin = css.resolve` span { margin-top: ${spacers.dp8}; diff --git a/src/modules/pivotTable/PivotTableEngine.js b/src/modules/pivotTable/PivotTableEngine.js index d59559d2b..b5788fee1 100644 --- a/src/modules/pivotTable/PivotTableEngine.js +++ b/src/modules/pivotTable/PivotTableEngine.js @@ -55,6 +55,7 @@ const defaultOptions = { showColumnSubtotals: false, fixColumnHeaders: false, fixRowHeaders: false, + cumulativeValues: false, } const defaultVisualizationProps = { @@ -268,6 +269,7 @@ export class PivotTableEngine { data = [] rowMap = [] columnMap = [] + accumulators = { rows: {} } constructor(visualization, data, legendSets) { this.visualization = Object.assign( @@ -306,6 +308,7 @@ export class PivotTableEngine { fixRowHeaders: this.dimensionLookup.rows.length ? visualization.fixRowHeaders : false, + cumulativeValues: visualization.cumulativeValues, } this.adaptiveClippingController = new AdaptiveClippingController(this) @@ -333,6 +336,7 @@ export class PivotTableEngine { getRaw({ row, column }) { const cellType = this.getRawCellType({ row, column }) const dxDimension = this.getRawCellDxDimension({ row, column }) + const valueType = dxDimension?.valueType || VALUE_TYPE_TEXT const headers = [ ...this.getRawRowHeader(row), @@ -346,55 +350,79 @@ export class PivotTableEngine { header?.dimensionItemType === DIMENSION_TYPE_ORGANISATION_UNIT )?.uid + const rawCell = { + cellType, + valueType, + ouId, + peId, + } + if (!this.data[row] || !this.data[row][column]) { - return { - cellType, - empty: true, - ouId, - peId, + rawCell.empty = true + } else { + const dataRow = this.data[row][column] + + let rawValue = + cellType === CELL_TYPE_VALUE + ? dataRow[this.dimensionLookup.dataHeaders.value] + : dataRow.value + let renderedValue = rawValue + + if (valueType === VALUE_TYPE_NUMBER) { + rawValue = parseValue(rawValue) + switch (this.visualization.numberType) { + case NUMBER_TYPE_ROW_PERCENTAGE: + renderedValue = + rawValue / this.percentageTotals[row].value + break + case NUMBER_TYPE_COLUMN_PERCENTAGE: + renderedValue = + rawValue / this.percentageTotals[column].value + break + default: + break + } } - } - const dataRow = this.data[row][column] + renderedValue = renderValue( + renderedValue, + valueType, + this.visualization + ) - let rawValue = - cellType === CELL_TYPE_VALUE - ? dataRow[this.dimensionLookup.dataHeaders.value] - : dataRow.value - let renderedValue = rawValue - const valueType = dxDimension?.valueType || VALUE_TYPE_TEXT + rawCell.dxDimension = dxDimension + rawCell.empty = false + rawCell.rawValue = rawValue + rawCell.renderedValue = renderedValue + } - if (valueType === VALUE_TYPE_NUMBER) { - rawValue = parseValue(rawValue) - switch (this.visualization.numberType) { - case NUMBER_TYPE_ROW_PERCENTAGE: - renderedValue = rawValue / this.percentageTotals[row].value - break - case NUMBER_TYPE_COLUMN_PERCENTAGE: - renderedValue = - rawValue / this.percentageTotals[column].value - break - default: - break + if (this.options.cumulativeValues) { + const cumulativeValue = this.getCumulative({ + row, + column, + }) + + if (cumulativeValue !== undefined && cumulativeValue !== null) { + // force to NUMBER for accumulated values + rawCell.valueType = + valueType === undefined || valueType === null + ? VALUE_TYPE_NUMBER + : valueType + rawCell.empty = false + rawCell.rawValue = cumulativeValue + rawCell.renderedValue = renderValue( + cumulativeValue, + valueType, + this.visualization + ) } } - renderedValue = renderValue( - renderedValue, - valueType, - this.visualization - ) + return rawCell + } - return { - cellType, - empty: false, - valueType, - rawValue, - renderedValue, - dxDimension, - ouId, - peId, - } + getCumulative({ row, column }) { + return this.accumulators.rows[row][column] } get({ row, column }) { @@ -488,10 +516,8 @@ export class PivotTableEngine { return undefined } const cellValue = this.data[row][column] - if (!cellValue) { - return undefined - } - if (!Array.isArray(cellValue)) { + + if (cellValue && !Array.isArray(cellValue)) { // This is a total cell return { valueType: cellValue.valueType, @@ -527,6 +553,11 @@ export class PivotTableEngine { } } + // Empty cell + // The cell still needs to get the valueType to render correctly 0 and cumulative values + // + // OR + // // Data is in Filter // TODO : This assumes the server ignores text types, we should confirm this is the case return { @@ -539,7 +570,7 @@ export class PivotTableEngine { return !this.data[row] || this.data[row].length === 0 } columnIsEmpty(column) { - return !this.adaptiveClippingController.columns.sizes[column] + return !this.rowMap.some((row) => this.data[row][column]) } getRawColumnHeader(column) { @@ -967,6 +998,45 @@ export class PivotTableEngine { : times(this.dataWidth, (n) => n) } + resetAccumulators() { + if (this.options.cumulativeValues) { + this.rowMap.forEach((row) => { + this.accumulators.rows[row] = {} + this.columnMap.reduce((acc, column) => { + const cellType = this.getRawCellType({ row, column }) + const dxDimension = this.getRawCellDxDimension({ + row, + column, + }) + const valueType = dxDimension?.valueType || VALUE_TYPE_TEXT + + // only accumulate numeric values + // accumulating text values does not make sense + if (valueType === VALUE_TYPE_NUMBER) { + if (this.data[row] && this.data[row][column]) { + const dataRow = this.data[row][column] + + const rawValue = + cellType === CELL_TYPE_VALUE + ? dataRow[ + this.dimensionLookup.dataHeaders.value + ] + : dataRow.value + + acc += parseValue(rawValue) + } + + this.accumulators.rows[row][column] = acc + } + + return acc + }, 0) + }) + } else { + this.accumulators = { rows: {} } + } + } + get cellPadding() { switch (this.visualization.displayDensity) { case DISPLAY_DENSITY_OPTION_COMPACT: @@ -1059,19 +1129,22 @@ export class PivotTableEngine { this.finalizeTotals() - this.rawData.rows.forEach((dataRow) => { - const pos = lookup(dataRow, this.dimensionLookup, this) - if (pos) { + this.resetRowMap() + this.resetColumnMap() + + this.resetAccumulators() + + this.rowMap.forEach((row) => { + this.columnMap.forEach((column) => { + const pos = { row, column } + this.adaptiveClippingController.add( pos, this.getRaw(pos).renderedValue ) - } + }) }) - this.resetRowMap() - this.resetColumnMap() - this.height = this.rowMap.length this.width = this.columnMap.length diff --git a/src/visualizations/util/colors/colorSets.js b/src/visualizations/util/colors/colorSets.js index 2eeb9a7e9..4c50dc98f 100644 --- a/src/visualizations/util/colors/colorSets.js +++ b/src/visualizations/util/colors/colorSets.js @@ -37,7 +37,14 @@ export const colorSets = { ], }, [COLOR_SET_BASIC]: { - colors: ['#348F41', '#9F2241', '#DDDDDD', '#B4A269'], + colors: [ + '#348F41', + '#9F2241', + '#B4A269', + '#EBEEF3', + '#58595B', + '#1A5632', + ], }, [COLOR_SET_EXTENDED]: { colors: [