Skip to content
Open
18 changes: 13 additions & 5 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-04-24T10:52:00.804Z\n"
"PO-Revision-Date: 2026-04-24T10:52:00.804Z\n"
"POT-Creation-Date: 2026-04-26T11:10:26.962Z\n"
"PO-Revision-Date: 2026-04-26T11:10:26.963Z\n"

msgid "2020"
msgstr "2020"
Expand Down Expand Up @@ -1758,12 +1758,12 @@ msgstr "Data item was not found"
msgid "Thematic layer"
msgstr "Thematic layer"

msgid "Tracked entity"
msgstr "Tracked entity"

msgid "related"
msgstr "related"

msgid "Tracked entity"
msgstr "Tracked entity"

msgid "not one of"
msgstr "not one of"

Expand Down Expand Up @@ -1824,3 +1824,11 @@ msgstr "End date is invalid"

msgid "End date cannot be earlier than start date"
msgstr "End date cannot be earlier than start date"

msgctxt "Application title"
msgid "__MANIFEST_APP_TITLE"
msgstr "Maps"

msgctxt "Application description"
msgid "__MANIFEST_APP_DESCRIPTION"
msgstr "DHIS2 Maps"
13 changes: 12 additions & 1 deletion src/components/datatable/DataTable.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import { highlightFeature, setFeatureProfile } from '../../actions/feature.js'
import { setOrgUnitProfile } from '../../actions/orgUnits.js'
import { EVENT_LAYER, GEOJSON_URL_LAYER } from '../../constants/layers.js'
import { isDarkColor } from '../../util/colors.js'
import { formatWithSeparator } from '../../util/numbers.js'
import { useCachedData } from '../cachedDataProvider/CachedDataProvider.jsx'
import FilterInput from './FilterInput.jsx'
import styles from './styles/DataTable.module.css'
import { useTableData } from './useTableData.js'
Expand Down Expand Up @@ -88,6 +90,10 @@ const TableComponents = {
}

const Table = ({ availableHeight, availableWidth }) => {
const {
systemSettings: { keyAnalysisDigitGroupSeparator },
} = useCachedData()

const headerRowRef = useRef(null)
const [columnWidths, setColumnWidths] = useState([])
const { mapViews } = useSelector((state) => state.map)
Expand Down Expand Up @@ -293,7 +299,12 @@ const Table = ({ availableHeight, availableWidth }) => {
backgroundColor={dataKey === 'color' ? value : null}
align={align}
>
{dataKey === 'color' ? value?.toLowerCase() : value}
{dataKey === 'color'
? value?.toLowerCase()
: formatWithSeparator(
value,
keyAnalysisDigitGroupSeparator
)}
</DataTableCell>
))
}
Expand Down
39 changes: 20 additions & 19 deletions src/components/datatable/__tests__/useTableData.spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@ describe('useTableData headers', () => {
color: '#FFFFB2',
legend: 'Great',
range: '90 - 120',
value: 106.3,
value: '106.3',
rawValue: 106.3,
},
},
],
Expand All @@ -160,7 +161,7 @@ describe('useTableData headers', () => {
{ name: 'Index', dataKey: 'index', type: 'number' },
{ name: 'Name', dataKey: 'name', type: 'string' },
{ name: 'Id', dataKey: 'id', type: 'string' },
{ name: 'Value', dataKey: 'value', type: 'number' },
{ name: 'Value', dataKey: 'rawValue', type: 'number' },
{ name: 'Legend', dataKey: 'legend', type: 'string' },
{ name: 'Range', dataKey: 'range', type: 'string' },
{ name: 'Level', dataKey: 'level', type: 'number' },
Expand All @@ -179,7 +180,7 @@ describe('useTableData headers', () => {
{ value: 0, dataKey: 'index' },
{ value: 'Ngelehun CHC', dataKey: 'name' },
{ value: 'thematicId-1', dataKey: 'id' },
{ value: 106.3, dataKey: 'value' },
{ value: 106.3, dataKey: 'rawValue' },
{ value: 'Great', dataKey: 'legend' },
{ value: '90 - 120', dataKey: 'range' },
{ value: 4, dataKey: 'level' },
Expand Down Expand Up @@ -544,11 +545,11 @@ describe('useTableData sorting', () => {
layer: 'thematic',
dataFilters: null,
data: [
{ id: '1', properties: { name: 'Item A', value: 10 } },
{ id: '2', properties: { name: 'Item B', value: 5 } },
{ id: '3', properties: { name: 'Item C', value: undefined } },
{ id: '4', properties: { name: 'Item D', value: 15 } },
{ id: '5', properties: { name: 'Item E', value: undefined } },
{ id: '1', properties: { name: 'Item A', rawValue: 10 } },
{ id: '2', properties: { name: 'Item B', rawValue: 5 } },
{ id: '3', properties: { name: 'Item C', rawValue: undefined } },
{ id: '4', properties: { name: 'Item D', rawValue: 15 } },
{ id: '5', properties: { name: 'Item E', rawValue: undefined } },
],
}

Expand All @@ -560,7 +561,7 @@ describe('useTableData sorting', () => {
() =>
useTableData({
layer: mockLayer,
sortField: 'value',
sortField: 'rawValue',
sortDirection: 'asc',
}),
{
Expand All @@ -582,7 +583,7 @@ describe('useTableData sorting', () => {
() =>
useTableData({
layer: mockLayer,
sortField: 'value',
sortField: 'rawValue',
sortDirection: 'desc',
}),
{
Expand Down Expand Up @@ -670,16 +671,16 @@ describe('useTableData sorting', () => {
layer: 'thematic',
dataFilters: null,
data: [
{ id: '1', properties: { name: 'Item A', value: 10 } },
{ id: '1', properties: { name: 'Item A', rawValue: 10 } },
{
id: '2',
properties: { name: 'Item B', value: undefined },
properties: { name: 'Item B', rawValue: undefined },
},
{
id: '3',
properties: { name: 'Item C', value: undefined },
properties: { name: 'Item C', rawValue: undefined },
},
{ id: '4', properties: { name: 'Item D', value: 5 } },
{ id: '4', properties: { name: 'Item D', rawValue: 5 } },
],
}

Expand All @@ -690,7 +691,7 @@ describe('useTableData sorting', () => {
() =>
useTableData({
layer: layerWithManyUndefined,
sortField: 'value',
sortField: 'rawValue',
sortDirection: 'asc',
}),
{
Expand All @@ -712,15 +713,15 @@ describe('useTableData sorting', () => {
data: [
{
id: '1',
properties: { name: 'Item A', value: undefined },
properties: { name: 'Item A', rawValue: undefined },
},
{
id: '2',
properties: { name: 'Item B', value: undefined },
properties: { name: 'Item B', rawValue: undefined },
},
{
id: '3',
properties: { name: 'Item C', value: undefined },
properties: { name: 'Item C', rawValue: undefined },
},
],
}
Expand All @@ -732,7 +733,7 @@ describe('useTableData sorting', () => {
() =>
useTableData({
layer: layerWithAllUndefined,
sortField: 'value',
sortField: 'rawValue',
sortDirection: 'asc',
}),
{
Expand Down
20 changes: 10 additions & 10 deletions src/components/datatable/useTableData.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const TYPE_DATE = 'date'
const INDEX = 'index'
const NAME = 'name'
const ID = 'id'
const VALUE = 'value'
const VALUE = 'rawValue'
const LEGEND = 'legend'
const RANGE = 'range'
const LEVEL = 'level'
Expand Down Expand Up @@ -298,30 +298,30 @@ export const useTableData = ({ layer, sortField, sortDirection }) => {

//sort
filteredData.sort((a, b) => {
a = a[sortField]
b = b[sortField]
const aVal = a[sortField]
const bVal = b[sortField]

// All undefined values should be sorted to the end
if (a === undefined && b === undefined) {
if (aVal === undefined && bVal === undefined) {
return 0
}

if (a === undefined) {
if (aVal === undefined) {
return 1 // a goes to end
}

if (b === undefined) {
if (bVal === undefined) {
return -1 // b goes to end
}

if (typeof a === TYPE_NUMBER) {
return sortDirection === ASCENDING ? a - b : b - a
if (typeof aVal === TYPE_NUMBER) {
return sortDirection === ASCENDING ? aVal - bVal : bVal - aVal
}

// TODO: Make sure sorting works across different locales
return sortDirection === ASCENDING
? a.localeCompare(b)
: b.localeCompare(a)
? aVal.localeCompare(bVal)
: bVal.localeCompare(aVal)
})

return filteredData.map((item) =>
Expand Down
8 changes: 7 additions & 1 deletion src/components/edit/earthEngine/LegendPreview.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,20 @@ import PropTypes from 'prop-types'
import React from 'react'
import { createLegend } from '../../../loaders/earthEngineLoader.js'
import { sortLegendItems } from '../../../util/legend.js'
import { useCachedData } from '../../cachedDataProvider/CachedDataProvider.jsx'
import LegendItem from '../../legend/LegendItem.jsx'
import styles from '../styles/LayerDialog.module.css'

const styleIsValid = ({ min, max }) =>
!Number.isNaN(min) && !Number.isNaN(max) && max > min

const LegendPreview = ({ style, showBelowMin }) => {
const legend = styleIsValid(style) && createLegend(style, showBelowMin)
const {
systemSettings: { keyAnalysisDigitGroupSeparator },
} = useCachedData()
const legend =
styleIsValid(style) &&
createLegend(style, showBelowMin, keyAnalysisDigitGroupSeparator)

return legend ? (
<div className={styles.flexColumn}>
Expand Down
17 changes: 17 additions & 0 deletions src/components/legend/Bubbles.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
createSingleColorBubbles,
computeLayout,
} from '../../util/bubbles.js'
import { formatWithSeparator } from '../../util/numbers.js'
import { useCachedData } from '../cachedDataProvider/CachedDataProvider.jsx'
import Bubble from './Bubble.jsx'

const style = {
Expand Down Expand Up @@ -74,6 +76,9 @@ const Bubbles = ({
classes,
isPlugin,
}) => {
const {
systemSettings: { keyAnalysisDigitGroupSeparator },
} = useCachedData()
const legendWidth = isPlugin ? 150 : 245
const noDataClass = classes.find((c) => c.noData === true)
const bubbleClasses = classes.filter((c) => !c.noData)
Expand Down Expand Up @@ -107,6 +112,18 @@ const Bubbles = ({
radiusHigh,
legendWidth,
}))

bubbles.forEach((bubble) => {
if (bubble.text !== undefined) {
bubble.text = formatWithSeparator(
bubble.text,
keyAnalysisDigitGroupSeparator,
{
force: true,
}
)
}
})
}

const xTranslate = alternate ? offset : '2'
Expand Down
37 changes: 31 additions & 6 deletions src/components/legend/LegendItemRange.jsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,38 @@
import PropTypes from 'prop-types'
import React from 'react'
import { formatWithSeparator } from '../../util/numbers.js'
import { useCachedData } from '../cachedDataProvider/CachedDataProvider.jsx'
import styles from './styles/LegendItemRange.module.css'

const LegendItemRange = ({ name = '', startValue, endValue, count }) => (
<td className={styles.legendItemRange}>
{isNaN(startValue) ? name : `${name} ${startValue} - ${endValue}`}
{count !== undefined ? ` (${count})` : ''}
</td>
)
const LegendItemRange = ({ name = '', startValue, endValue, count }) => {
const {
systemSettings: { keyAnalysisDigitGroupSeparator },
} = useCachedData()

const nameLabel = name ? `${name} ` : ''
const rangeLabel =
startValue === undefined || Number.isNaN(startValue)
? ''
: `${formatWithSeparator(
startValue,
keyAnalysisDigitGroupSeparator
)} - ${formatWithSeparator(
endValue,
keyAnalysisDigitGroupSeparator
)}`
const countLabel =
count === undefined
? ''
: ` (${formatWithSeparator(count, keyAnalysisDigitGroupSeparator)})`

return (
<td className={styles.legendItemRange}>
{nameLabel}
{rangeLabel}
{countLabel}
</td>
)
}

LegendItemRange.propTypes = {
count: PropTypes.number,
Expand Down
4 changes: 3 additions & 1 deletion src/components/map/layers/EventLayer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,14 +152,16 @@ class EventLayer extends Layer {
}

render() {
const { styleDataItem, nameProperty } = this.props
const { styleDataItem, nameProperty, keyAnalysisDigitGroupSeparator } =
this.props
const { popup, displayItems, eventCoordinateFieldName } = this.state

return popup && displayItems ? (
<EventPopup
{...popup}
styleDataItem={styleDataItem}
nameProperty={nameProperty}
keyAnalysisDigitGroupSeparator={keyAnalysisDigitGroupSeparator}
displayItems={displayItems}
eventCoordinateFieldName={eventCoordinateFieldName}
onClose={this.onPopupClose}
Expand Down
Loading
Loading