Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 23 additions & 6 deletions i18n/en.pot
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand All @@ -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"

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
26 changes: 26 additions & 0 deletions patches/@dhis2+analytics+29.4.1.patch
Original file line number Diff line number Diff line change
@@ -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
58 changes: 52 additions & 6 deletions src/components/VisualizationOptions/Options/ColorSet.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}) => (
<div className={styles.tabSectionOption}>
<div className={styles.tabSectionOption}>
<Checkbox
checked={useItemColor}
label={i18n.t('Use configured item color when available')}
onChange={({ checked }) => onUseItemColorChange(checked)}
disabled={disabled}
dense
dataTest="option-use-item-color"
/>
<Help>
{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.'
)}
</Help>
</div>
<Field name="colorSet-selector" dense>
{[
[
Expand Down Expand Up @@ -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 }) => (
Expand Down Expand Up @@ -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)
50 changes: 49 additions & 1 deletion src/components/VisualizationPlugin/VisualizationPlugin.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,54 @@
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',
Expand Down Expand Up @@ -424,7 +472,7 @@
.catch((error) => setError(error))
// since errors are rendered here, always call loading complete
.finally(() => onLoadingComplete())
}, [

Check warning on line 475 in src/components/VisualizationPlugin/VisualizationPlugin.jsx

View workflow job for this annotation

GitHub Actions / lint

React Hook useEffect has missing dependencies: 'baseUrl', 'doFetchData', 'doFetchLegendSets', and 'onLoadingComplete'. Either include them or remove the dependency array. If 'onLoadingComplete' changes too often, find the parent component that defines it and wrap that definition in useCallback
originalVisualization,
filters,
forDashboard,
Expand Down Expand Up @@ -618,7 +666,7 @@
} else {
return (
<ChartPlugin
visualization={visualization}
visualization={applyConfiguredItemColors(visualization)}
responses={fetchResult.responses}
extraOptions={fetchResult.extraOptions}
legendSets={legendSets}
Expand Down
1 change: 1 addition & 0 deletions src/modules/fields/baseFields.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export const fieldsByType = {
getFieldObject('showData', { option: true }),
getFieldObject('outlierAnalysis', { option: true }),
getFieldObject(BASE_FIELD_TYPE, { option: true }),
getFieldObject('useItemColor', { option: true }),
],
eventReport_eventChart: [
getFieldObject('attributeValueDimension'),
Expand Down
2 changes: 1 addition & 1 deletion src/modules/fields/nestedFields.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const NAME = 'name,displayName,displayShortName'
const DIMENSION_ITEM = `dimensionItem~rename(${ID})`
const LEGEND_SET = `${ID},${NAME}`

const ITEMS = `${DIMENSION_ITEM},${NAME},dimensionItemType,expression,access`
const ITEMS = `${DIMENSION_ITEM},${NAME},dimensionItemType,expression,access,style[color]`

const AXIS = `dimension,filter,legendSet[${LEGEND_SET}],items[${ITEMS}]`
const INTERPRETATIONS = 'id,created'
Expand Down
6 changes: 6 additions & 0 deletions src/modules/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const OPTION_SORT_ORDER = 'sortOrder'
export const OPTION_SUBTITLE = 'subtitle'
export const OPTION_TITLE = 'title'
export const OPTION_TOP_LIMIT = 'topLimit'
export const OPTION_USE_ITEM_COLOR = 'useItemColor'

export const OPTION_SHOW_SERIES_KEY = 'showSeriesKey'
export const OPTION_SHOW_LEGEND_KEY = 'showLegendKey'
Expand Down Expand Up @@ -299,6 +300,11 @@ export const options = {
requestable: false,
savable: true,
},
[OPTION_USE_ITEM_COLOR]: {
defaultValue: false,
requestable: false,
savable: true,
},
}

export default options
Expand Down
2 changes: 2 additions & 0 deletions src/modules/options/defaultConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
OPTION_SORT_ORDER,
OPTION_SUBTITLE,
OPTION_TITLE,
OPTION_USE_ITEM_COLOR,
} from '../options.js'
import getAdvancedSection from './sections/advanced.jsx'
import getChartStyleSection from './sections/chartStyle.jsx'
Expand Down Expand Up @@ -104,6 +105,7 @@ export const defaultOptionNames = ({
OPTION_SUBTITLE,
OPTION_HIDE_SUBTITLE,
OPTION_COLOR_SET,
OPTION_USE_ITEM_COLOR,
// Limit values tab
OPTION_MEASURE_CRITERIA,
]
Expand Down
Loading
Loading