From 06be89b79453fae7555d53aecc3698a6f0682aa1 Mon Sep 17 00:00:00 2001 From: Karoline Tufte Lien Date: Tue, 2 Jun 2026 15:45:32 +0200 Subject: [PATCH] feat: apply configured data element/indicator color [prototype] Prototype toggle in Style -> Color set: "Use configured item color when available". When on (and legend is off), each data element / indicator with a configured style.color uses it instead of the color set; items without one fall back to the palette. Precedence: legend > configured item color > color set. Includes a patch-package patch to @dhis2/analytics so the chart series adapter honors a per-series color instead of always overwriting it from the color set (the one-line change belongs upstream for a real feature). Scope: column charts validated. Not for merge. AI Assisted --- i18n/en.pot | 29 +++- package.json | 3 +- patches/@dhis2+analytics+29.4.1.patch | 26 ++++ .../VisualizationOptions/Options/ColorSet.jsx | 58 +++++++- .../VisualizationPlugin.jsx | 50 ++++++- src/modules/fields/baseFields.js | 1 + src/modules/fields/nestedFields.js | 2 +- src/modules/options.js | 6 + src/modules/options/defaultConfig.js | 2 + yarn.lock | 126 +++++++++++++++++- 10 files changed, 287 insertions(+), 16 deletions(-) create mode 100644 patches/@dhis2+analytics+29.4.1.patch diff --git a/i18n/en.pot b/i18n/en.pot index c8942e421e..0ea3da71d4 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-03-11T11:54:22.309Z\n" -"PO-Revision-Date: 2026-03-11T11:54:22.310Z\n" +"POT-Creation-Date: 2026-06-02T13:42:03.156Z\n" +"PO-Revision-Date: 2026-06-02T13:42:03.157Z\n" msgid "All items" msgstr "All items" @@ -346,11 +346,22 @@ msgstr "Reset style to default" msgid "Base line" msgstr "Base line" -msgid "Column sub-totals" -msgstr "Column sub-totals" +msgid "Use configured item color when available" +msgstr "Use configured item color when available" -msgid "Columns totals" -msgstr "Columns totals" +msgid "" +"Legend is currently controlling colors. This setting will apply when legend " +"is turned off." +msgstr "" +"Legend is currently controlling colors. This setting will apply when legend " +"is turned off." + +msgid "" +"For data elements and indicators with a color configured in their " +"maintenance settings, use that color instead of the color set below." +msgstr "" +"For data elements and indicators with a color configured in their " +"maintenance settings, use that color instead of the color set below." msgid "Default" msgstr "Default" @@ -376,6 +387,12 @@ msgstr "Color blind" msgid "Patterns" msgstr "Patterns" +msgid "Column sub-totals" +msgstr "Column sub-totals" + +msgid "Columns totals" +msgstr "Columns totals" + msgid "Event data" msgstr "Event data" diff --git a/package.json b/package.json index fae4a2507d..f8ac140abc 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "validate-push": "yarn test", "cy:open": "start-server-and-test 'yarn start:nobrowser' http://localhost:3000 'yarn cypress open --e2e --env networkMode=live'", "cy:run": "start-server-and-test 'yarn start:nobrowser' http://localhost:3000 'yarn cypress run --browser chrome headless --env networkMode=live'", - "postinstall": "cp ./node_modules/jspdf/dist/jspdf.umd.min.js ./public/vendor/jspdf.js && cp ./node_modules/svg2pdf.js/dist/svg2pdf.umd.min.js ./public/vendor/svg2pdf.js" + "postinstall": "patch-package && cp ./node_modules/jspdf/dist/jspdf.umd.min.js ./public/vendor/jspdf.js && cp ./node_modules/svg2pdf.js/dist/svg2pdf.umd.min.js ./public/vendor/svg2pdf.js" }, "devDependencies": { "@dhis2/cli-app-scripts": "^12.11.0", @@ -39,6 +39,7 @@ "jest-webgl-canvas-mock": "^2.5.3", "jspdf": "^3.0.1", "loglevel": "^1.8.1", + "patch-package": "^8.0.0", "postinstall-postinstall": "^2.1.0", "redux-mock-store": "^1.5.4", "semantic-release": "^20", diff --git a/patches/@dhis2+analytics+29.4.1.patch b/patches/@dhis2+analytics+29.4.1.patch new file mode 100644 index 0000000000..fc4cfcb944 --- /dev/null +++ b/patches/@dhis2+analytics+29.4.1.patch @@ -0,0 +1,26 @@ +diff --git a/node_modules/@dhis2/analytics/build/cjs/visualizations/config/adapters/dhis_highcharts/series/index.js b/node_modules/@dhis2/analytics/build/cjs/visualizations/config/adapters/dhis_highcharts/series/index.js +index 4d67c30..0ee8f21 100644 +--- a/node_modules/@dhis2/analytics/build/cjs/visualizations/config/adapters/dhis_highcharts/series/index.js ++++ b/node_modules/@dhis2/analytics/build/cjs/visualizations/config/adapters/dhis_highcharts/series/index.js +@@ -132,7 +132,7 @@ function getDefault({ + seriesObj.color = indexColorPatternMap[index]; + } else { + // Default: Either generate colors or fetch from color sets +- seriesObj.color = idColorMap[seriesObj.id]; ++ seriesObj.color = (matchedObject && matchedObject.color) ? matchedObject.color : idColorMap[seriesObj.id]; + } + + // axis number +diff --git a/node_modules/@dhis2/analytics/build/es/visualizations/config/adapters/dhis_highcharts/series/index.js b/node_modules/@dhis2/analytics/build/es/visualizations/config/adapters/dhis_highcharts/series/index.js +index f27a5d4..aad494b 100644 +--- a/node_modules/@dhis2/analytics/build/es/visualizations/config/adapters/dhis_highcharts/series/index.js ++++ b/node_modules/@dhis2/analytics/build/es/visualizations/config/adapters/dhis_highcharts/series/index.js +@@ -125,7 +125,7 @@ function getDefault({ + seriesObj.color = indexColorPatternMap[index]; + } else { + // Default: Either generate colors or fetch from color sets +- seriesObj.color = idColorMap[seriesObj.id]; ++ seriesObj.color = (matchedObject && matchedObject.color) ? matchedObject.color : idColorMap[seriesObj.id]; + } + + // axis number diff --git a/src/components/VisualizationOptions/Options/ColorSet.jsx b/src/components/VisualizationOptions/Options/ColorSet.jsx index 02bfd38d2b..192b5a5253 100644 --- a/src/components/VisualizationOptions/Options/ColorSet.jsx +++ b/src/components/VisualizationOptions/Options/ColorSet.jsx @@ -8,19 +8,51 @@ import { COLOR_SET_GRAY, COLOR_SET_COLOR_BLIND, COLOR_SET_PATTERNS, + LEGEND_DISPLAY_STRATEGY_BY_DATA_ITEM, } from '@dhis2/analytics' import i18n from '@dhis2/d2-i18n' -import { Field, Radio } from '@dhis2/ui' +import { Checkbox, Field, Help, Radio } from '@dhis2/ui' import PropTypes from 'prop-types' import React from 'react' import { connect } from 'react-redux' import { acSetUiOptions } from '../../../actions/ui.js' -import { OPTION_COLOR_SET } from '../../../modules/options.js' +import { + OPTION_COLOR_SET, + OPTION_LEGEND_DISPLAY_STRATEGY, + OPTION_LEGEND_SET, + OPTION_USE_ITEM_COLOR, +} from '../../../modules/options.js' import { sGetUiOptions } from '../../../reducers/ui.js' import styles from '../styles/VisualizationOptions.module.css' -const ColorSet = ({ value, onChange, disabled }) => ( +const ColorSet = ({ + value, + onChange, + disabled, + useItemColor, + onUseItemColorChange, + legendActive, +}) => (
+
+ onUseItemColorChange(checked)} + disabled={disabled} + dense + dataTest="option-use-item-color" + /> + + {legendActive + ? i18n.t( + 'Legend is currently controlling colors. This setting will apply when legend is turned off.' + ) + : i18n.t( + 'For data elements and indicators with a color configured in their maintenance settings, use that color instead of the color set below.' + )} + +
{[ [ @@ -84,7 +116,10 @@ const ColorSet = ({ value, onChange, disabled }) => ( ColorSet.propTypes = { value: PropTypes.string.isRequired, onChange: PropTypes.func.isRequired, + onUseItemColorChange: PropTypes.func.isRequired, disabled: PropTypes.bool, + legendActive: PropTypes.bool, + useItemColor: PropTypes.bool, } const ColorSetPreview = ({ colorSet, disabled }) => ( @@ -134,13 +169,24 @@ ColorSetPreview.propTypes = { disabled: PropTypes.bool, } -const mapStateToProps = (state) => ({ - value: sGetUiOptions(state)[OPTION_COLOR_SET], -}) +const mapStateToProps = (state) => { + const uiOptions = sGetUiOptions(state) + const strategy = uiOptions[OPTION_LEGEND_DISPLAY_STRATEGY] + const legendSet = uiOptions[OPTION_LEGEND_SET] + return { + value: uiOptions[OPTION_COLOR_SET], + useItemColor: Boolean(uiOptions[OPTION_USE_ITEM_COLOR]), + legendActive: + strategy === LEGEND_DISPLAY_STRATEGY_BY_DATA_ITEM || + Boolean(legendSet), + } +} const mapDispatchToProps = (dispatch) => ({ onChange: (colorSet) => dispatch(acSetUiOptions({ [OPTION_COLOR_SET]: colorSet })), + onUseItemColorChange: (checked) => + dispatch(acSetUiOptions({ [OPTION_USE_ITEM_COLOR]: checked })), }) export default connect(mapStateToProps, mapDispatchToProps)(ColorSet) diff --git a/src/components/VisualizationPlugin/VisualizationPlugin.jsx b/src/components/VisualizationPlugin/VisualizationPlugin.jsx index 5cf9f32dde..946a55f731 100644 --- a/src/components/VisualizationPlugin/VisualizationPlugin.jsx +++ b/src/components/VisualizationPlugin/VisualizationPlugin.jsx @@ -54,6 +54,54 @@ import styles from './styles/VisualizationPlugin.module.css' const FILTERS_PROP_DEFAULT = {} const ICON_TYPE_DATA_ITEM = 'DATA_ITEM' +// PROTOTYPE: apply configured per-item color from a data element / indicator +// `style.color`, overriding the active color set. Legend (when on) still wins. +const applyConfiguredItemColors = (visualization) => { + if (!visualization?.useItemColor) { + return visualization + } + + const legend = visualization.legend + const legendActive = + legend?.strategy === LEGEND_DISPLAY_STRATEGY_BY_DATA_ITEM || + Boolean(legend?.set?.id) + if (legendActive) { + return visualization + } + + const itemColorById = {} + ;(visualization.dataDimensionItems || []).forEach((ddi) => { + Object.values(ddi).forEach((item) => { + if ( + item && + typeof item === 'object' && + item.id && + item.style?.color + ) { + itemColorById[item.id] = item.style.color + } + }) + }) + + if (!Object.keys(itemColorById).length) { + return visualization + } + + const existingSeries = visualization.series || [] + const seriesByItem = new Map( + existingSeries.map((s) => [s.dimensionItem, s]) + ) + Object.entries(itemColorById).forEach(([id, color]) => { + // Preserve any existing series entry (keeps its axis/type); for items + // without one, create a well-formed entry with axis 0 so downstream + // axis maps never see an undefined axis. + const existing = seriesByItem.get(id) || { dimensionItem: id, axis: 0 } + seriesByItem.set(id, { ...existing, color }) + }) + + return { ...visualization, series: Array.from(seriesByItem.values()) } +} + export const VisualizationPlugin = ({ visualization: originalVisualization = {}, displayProperty = 'name', @@ -618,7 +666,7 @@ export const VisualizationPlugin = ({ } else { return (