diff --git a/i18n/en.pot b/i18n/en.pot
index 545bed738..332c55c5f 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-04-26T11:10:26.962Z\n"
-"PO-Revision-Date: 2026-04-26T11:10:26.963Z\n"
+"POT-Creation-Date: 2026-04-27T11:12:19.709Z\n"
+"PO-Revision-Date: 2026-04-27T11:12:19.709Z\n"
msgid "2020"
msgstr "2020"
@@ -38,6 +38,12 @@ msgstr "Classification"
msgid "Classes"
msgstr "Classes"
+msgid "Auto"
+msgstr "Auto"
+
+msgid "Decimal places"
+msgstr "Decimal places"
+
msgid "Legend set"
msgstr "Legend set"
@@ -353,8 +359,8 @@ msgstr "Max value is required"
msgid "Max should be greater than min"
msgstr "Max should be greater than min"
-msgid "Valid steps are {{minSteps}} to {{maxSteps}}"
-msgstr "Valid steps are {{minSteps}} to {{maxSteps}}"
+msgid "Valid classes are {{minSteps}} to {{maxSteps}}"
+msgstr "Valid classes are {{minSteps}} to {{maxSteps}}"
msgid "Min"
msgstr "Min"
@@ -362,9 +368,6 @@ msgstr "Min"
msgid "Max"
msgstr "Max"
-msgid "Steps"
-msgstr "Steps"
-
msgid "Facility buffer"
msgstr "Facility buffer"
@@ -1824,11 +1827,3 @@ 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"
diff --git a/src/actions/layerEdit.js b/src/actions/layerEdit.js
index 836dfae32..eaf16f49d 100644
--- a/src/actions/layerEdit.js
+++ b/src/actions/layerEdit.js
@@ -92,6 +92,11 @@ export const setColorScale = (colorScale) => ({
colorScale,
})
+export const setLegendDecimalPlaces = (legendDecimalPlaces) => ({
+ type: types.LAYER_EDIT_LEGEND_DECIMAL_PLACES_SET,
+ legendDecimalPlaces,
+})
+
// Set event status
export const setEventStatus = (status) => ({
type: types.LAYER_EDIT_EVENT_STATUS_SET,
diff --git a/src/components/classification/Classification.jsx b/src/components/classification/Classification.jsx
index 5227c6168..0fd5d3aac 100644
--- a/src/components/classification/Classification.jsx
+++ b/src/components/classification/Classification.jsx
@@ -3,7 +3,11 @@ import { range } from 'lodash/fp'
import PropTypes from 'prop-types'
import React from 'react'
import { connect } from 'react-redux'
-import { setClassification, setColorScale } from '../../actions/layerEdit.js'
+import {
+ setClassification,
+ setColorScale,
+ setLegendDecimalPlaces,
+} from '../../actions/layerEdit.js'
import {
getClassificationTypes,
CLASSIFICATION_EQUAL_INTERVALS,
@@ -16,6 +20,7 @@ import {
getColorScale,
} from '../../util/colors.js'
import { SelectField, ColorScaleSelect } from '../core/index.js'
+import DecimalPlacesSelect from './DecimalPlacesSelect.jsx'
import styles from './styles/Classification.module.css'
const classRange = range(3, 10).map((num) => ({
@@ -27,8 +32,10 @@ const Classification = ({
method,
classes,
colorScale,
+ legendDecimalPlaces,
setClassification,
setColorScale,
+ setLegendDecimalPlaces,
}) => {
const colorScaleName = colorScale
? getColorScale(colorScale)
@@ -44,22 +51,28 @@ const Classification = ({
className={styles.select}
/>,
-
- setColorScale(getColorPalette(colorScaleName, item.id))
- }
- className={styles.classes}
- />
+
+
+ setColorScale(getColorPalette(colorScaleName, item.id))
+ }
+ className={styles.classes}
+ />
+
+
-
,
]
}
@@ -67,8 +80,10 @@ const Classification = ({
Classification.propTypes = {
setClassification: PropTypes.func.isRequired,
setColorScale: PropTypes.func.isRequired,
+ setLegendDecimalPlaces: PropTypes.func.isRequired,
classes: PropTypes.number,
colorScale: PropTypes.array,
+ legendDecimalPlaces: PropTypes.number,
method: PropTypes.number,
}
@@ -77,6 +92,7 @@ export default connect(
method: layerEdit.method,
classes: layerEdit.classes,
colorScale: layerEdit.colorScale,
+ legendDecimalPlaces: layerEdit.legendDecimalPlaces,
}),
- { setClassification, setColorScale }
+ { setClassification, setColorScale, setLegendDecimalPlaces }
)(Classification)
diff --git a/src/components/classification/DecimalPlacesSelect.jsx b/src/components/classification/DecimalPlacesSelect.jsx
new file mode 100644
index 000000000..34f36e1e3
--- /dev/null
+++ b/src/components/classification/DecimalPlacesSelect.jsx
@@ -0,0 +1,32 @@
+import i18n from '@dhis2/d2-i18n'
+import { range } from 'lodash/fp'
+import PropTypes from 'prop-types'
+import React from 'react'
+import { SelectField } from '../core/index.js'
+
+const DECIMAL_PLACES_AUTO = 'auto'
+
+const decimalPlacesItems = [
+ { id: DECIMAL_PLACES_AUTO, name: i18n.t('Auto') },
+ ...range(0, 7).map((num) => ({ id: num, name: num.toString() })),
+]
+
+const DecimalPlacesSelect = ({ value, onChange, className }) => (
+
+ onChange(item.id === DECIMAL_PLACES_AUTO ? undefined : item.id)
+ }
+ className={className}
+ />
+)
+
+DecimalPlacesSelect.propTypes = {
+ onChange: PropTypes.func.isRequired,
+ className: PropTypes.string,
+ value: PropTypes.number,
+}
+
+export default DecimalPlacesSelect
diff --git a/src/components/classification/SingleColor.jsx b/src/components/classification/SingleColor.jsx
index e4cc175da..980314acc 100644
--- a/src/components/classification/SingleColor.jsx
+++ b/src/components/classification/SingleColor.jsx
@@ -1,14 +1,23 @@
import i18n from '@dhis2/d2-i18n'
+import cx from 'classnames'
import PropTypes from 'prop-types'
import React, { useEffect } from 'react'
import { connect } from 'react-redux'
-import { setColorScale } from '../../actions/layerEdit.js'
+import {
+ setColorScale,
+ setLegendDecimalPlaces,
+} from '../../actions/layerEdit.js'
import { THEMATIC_COLOR } from '../../constants/layers.js'
import { ColorPicker } from '../core/index.js'
+import DecimalPlacesSelect from './DecimalPlacesSelect.jsx'
+import styles from './styles/Classification.module.css'
-// Displays a color picker for single color layer
-const SingleColor = ({ color, setColorScale }) => {
- // Set default color
+const SingleColor = ({
+ color,
+ legendDecimalPlaces,
+ setColorScale,
+ setLegendDecimalPlaces,
+}) => {
useEffect(() => {
if (!color || color.length !== 7) {
setColorScale(THEMATIC_COLOR)
@@ -16,26 +25,34 @@ const SingleColor = ({ color, setColorScale }) => {
}, [color, setColorScale])
return color ? (
-
+
+
+
+
) : null
}
SingleColor.propTypes = {
setColorScale: PropTypes.func.isRequired,
+ setLegendDecimalPlaces: PropTypes.func.isRequired,
color: PropTypes.string,
+ legendDecimalPlaces: PropTypes.number,
}
export default connect(
({ layerEdit }) => ({
color: layerEdit.colorScale,
+ legendDecimalPlaces: layerEdit.legendDecimalPlaces,
}),
- { setColorScale }
+ { setColorScale, setLegendDecimalPlaces }
)(SingleColor)
diff --git a/src/components/classification/styles/Classification.module.css b/src/components/classification/styles/Classification.module.css
index 020cde86e..3f0a05e49 100644
--- a/src/components/classification/styles/Classification.module.css
+++ b/src/components/classification/styles/Classification.module.css
@@ -2,19 +2,32 @@
width: 100%;
}
+.classesRow {
+ display: flex;
+ gap: var(--spacers-dp16);
+}
+
.classes {
- width: 50px;
- margin-right: var(--spacers-dp16);
- top: -8px;
- float: left;
+ flex: 0 0 auto;
}
.scale {
display: block;
padding-top: var(--spacers-dp8);
- clear: both;
}
-.clear {
- clear: both;
+.decimalPlaces {
+ flex: 0 0 auto;
+}
+
+.singleColorRow {
+ display: flex;
+ gap: var(--spacers-dp16);
+ align-items: flex-end;
+ margin-bottom: var(--spacers-dp12);
+}
+
+.singleColorField {
+ flex-shrink: 0;
+ margin-bottom: 0;
}
diff --git a/src/components/core/styles/ColorScale.module.css b/src/components/core/styles/ColorScale.module.css
index a47463de2..276b7794a 100644
--- a/src/components/core/styles/ColorScale.module.css
+++ b/src/components/core/styles/ColorScale.module.css
@@ -1,5 +1,5 @@
.colorScale {
- margin: var(--spacers-dp8) 0 0 0;
+ margin: 0 0 var(--spacers-dp8) 0;
padding-left: 0;
height: 36px;
cursor: pointer;
diff --git a/src/components/datatable/__tests__/useTableData.spec.jsx b/src/components/datatable/__tests__/useTableData.spec.jsx
index 1340843b7..64355c186 100644
--- a/src/components/datatable/__tests__/useTableData.spec.jsx
+++ b/src/components/datatable/__tests__/useTableData.spec.jsx
@@ -289,6 +289,97 @@ describe('useTableData headers', () => {
expect(isLoading).toBe(false)
})
+ test('treats NUMBER header with optionSet as string type', () => {
+ const store = { aggregations: {} }
+ const layer = {
+ layer: 'event',
+ dataFilters: null,
+ isExtended: true,
+ headers: [
+ {
+ name: 'AbCdEfGhIjK',
+ column: 'Severity',
+ valueType: 'NUMBER',
+ optionSet: { id: 'xyz123' },
+ },
+ ],
+ data: [
+ {
+ properties: {
+ id: 'evt1',
+ type: 'Point',
+ ouname: 'Test OU',
+ eventdate: '2023-01-01',
+ AbCdEfGhIjK: 'high',
+ },
+ },
+ ],
+ }
+
+ const { result } = renderHook(
+ () =>
+ useTableData({
+ layer,
+ sortField: 'name',
+ sortDirection: 'asc',
+ }),
+ {
+ wrapper: ({ children }) => (
+ {children}
+ ),
+ }
+ )
+
+ const { headers } = result.current
+ const severityHeader = headers.find((h) => h.dataKey === 'AbCdEfGhIjK')
+ expect(severityHeader.type).toBe('string')
+ })
+
+ test('treats NUMBER header without optionSet as number type', () => {
+ const store = { aggregations: {} }
+ const layer = {
+ layer: 'event',
+ dataFilters: null,
+ isExtended: true,
+ headers: [
+ {
+ name: 'AbCdEfGhIjK',
+ column: 'Score',
+ valueType: 'NUMBER',
+ },
+ ],
+ data: [
+ {
+ properties: {
+ id: 'evt2',
+ type: 'Point',
+ ouname: 'Test OU',
+ eventdate: '2023-01-01',
+ AbCdEfGhIjK: 42,
+ },
+ },
+ ],
+ }
+
+ const { result } = renderHook(
+ () =>
+ useTableData({
+ layer,
+ sortField: 'name',
+ sortDirection: 'asc',
+ }),
+ {
+ wrapper: ({ children }) => (
+ {children}
+ ),
+ }
+ )
+
+ const { headers } = result.current
+ const scoreHeader = headers.find((h) => h.dataKey === 'AbCdEfGhIjK')
+ expect(scoreHeader.type).toBe('number')
+ })
+
test('gets headers and rows for EE population layer', () => {
const store = {
aggregations: {
diff --git a/src/components/datatable/useTableData.js b/src/components/datatable/useTableData.js
index 778d54c7c..52a780230 100644
--- a/src/components/datatable/useTableData.js
+++ b/src/components/datatable/useTableData.js
@@ -13,6 +13,7 @@ import { numberValueTypes } from '../../constants/valueTypes.js'
import { hasClasses } from '../../util/earthEngine.js'
import { filterData } from '../../util/filter.js'
import { getGeojsonDisplayData } from '../../util/geojson.js'
+import { parseRange } from '../../util/legend.js'
import { getRoundToPrecisionFn, getPrecision } from '../../util/numbers.js'
import { isValidUid } from '../../util/uid.js'
@@ -111,12 +112,13 @@ const getEventHeaders = ({ layerHeaders = [], styleDataItem }) => {
const customFields = layerHeaders
.filter(({ name }) => isValidUid(name))
- .map(({ name: dataKey, column: name, valueType }) => ({
+ .map(({ name: dataKey, column: name, valueType, optionSet }) => ({
name,
dataKey,
- type: numberValueTypes.includes(valueType)
- ? TYPE_NUMBER
- : TYPE_STRING,
+ type:
+ !optionSet && numberValueTypes.includes(valueType)
+ ? TYPE_NUMBER
+ : TYPE_STRING,
}))
customFields.push(defaultFieldsMap()[TYPE])
@@ -307,17 +309,30 @@ export const useTableData = ({ layer, sortField, sortDirection }) => {
}
if (aVal === undefined) {
- return 1 // a goes to end
+ return 1 // aVal goes to end
}
if (bVal === undefined) {
- return -1 // b goes to end
+ return -1 // bVal goes to end
}
if (typeof aVal === TYPE_NUMBER) {
return sortDirection === ASCENDING ? aVal - bVal : bVal - aVal
}
+ if (sortField === RANGE) {
+ const [aStart, aEnd] = parseRange(aVal)
+ const [bStart, bEnd] = parseRange(bVal)
+ const startDiff =
+ sortDirection === ASCENDING
+ ? aStart - bStart
+ : bStart - aStart
+ if (startDiff !== 0) {
+ return startDiff
+ }
+ return sortDirection === ASCENDING ? aEnd - bEnd : bEnd - aEnd
+ }
+
// TODO: Make sure sorting works across different locales
return sortDirection === ASCENDING
? aVal.localeCompare(bVal)
diff --git a/src/components/edit/earthEngine/StyleSelect.jsx b/src/components/edit/earthEngine/StyleSelect.jsx
index fccb53faf..095842415 100644
--- a/src/components/edit/earthEngine/StyleSelect.jsx
+++ b/src/components/edit/earthEngine/StyleSelect.jsx
@@ -22,7 +22,7 @@ export const getStyleSelectError = ({ min, max, steps, palette, ranges }) => {
return i18n.t('Max should be greater than min')
}
if (!ranges && steps && (steps < minSteps || steps > maxSteps)) {
- return i18n.t('Valid steps are {{minSteps}} to {{maxSteps}}', {
+ return i18n.t('Valid classes are {{minSteps}} to {{maxSteps}}', {
minSteps,
maxSteps,
})
@@ -75,7 +75,7 @@ const StyleSelect = ({ unit, style, setStyle }) => {
className={styles.flexInnerColumn}
/>
{
@@ -120,6 +121,7 @@ const Bubbles = ({
keyAnalysisDigitGroupSeparator,
{
force: true,
+ precision: legendDecimalPlaces,
}
)
}
@@ -186,6 +188,7 @@ Bubbles.propTypes = {
classes: PropTypes.array,
color: PropTypes.string,
isPlugin: PropTypes.bool,
+ legendDecimalPlaces: PropTypes.number,
maxValue: PropTypes.number,
minValue: PropTypes.number,
}
diff --git a/src/components/legend/Legend.jsx b/src/components/legend/Legend.jsx
index 91ce3e822..6ad671885 100644
--- a/src/components/legend/Legend.jsx
+++ b/src/components/legend/Legend.jsx
@@ -19,6 +19,7 @@ const Legend = ({
url,
source,
sourceUrl,
+ decimalPlaces,
isPlugin = false,
}) => (
@@ -44,8 +45,14 @@ const Legend = ({
Array.isArray(items) && (
- {sortLegendItems(items).map((item, index) => (
-
+ {sortLegendItems(items).map((item) => (
+
))}
@@ -97,6 +104,7 @@ Legend.propTypes = {
color: PropTypes.string,
}),
coordinateFields: PropTypes.array,
+ decimalPlaces: PropTypes.number,
description: PropTypes.string,
explanation: PropTypes.array,
filters: PropTypes.array,
diff --git a/src/components/legend/LegendItem.jsx b/src/components/legend/LegendItem.jsx
index 6b46e16ed..286c6a3e4 100644
--- a/src/components/legend/LegendItem.jsx
+++ b/src/components/legend/LegendItem.jsx
@@ -20,6 +20,7 @@ const LegendItem = ({
startValue,
endValue,
count,
+ decimalPlaces,
}) => {
if (!name && startValue === undefined) {
return null
@@ -66,6 +67,7 @@ const LegendItem = ({
startValue={startValue}
endValue={endValue}
count={count}
+ decimalPlaces={decimalPlaces}
/>
)
@@ -74,6 +76,7 @@ const LegendItem = ({
LegendItem.propTypes = {
color: PropTypes.string,
count: PropTypes.number,
+ decimalPlaces: PropTypes.number,
endValue: PropTypes.number,
fillColor: PropTypes.string,
image: PropTypes.string,
diff --git a/src/components/legend/LegendItemRange.jsx b/src/components/legend/LegendItemRange.jsx
index 28b78ca7e..78ace6ba0 100644
--- a/src/components/legend/LegendItemRange.jsx
+++ b/src/components/legend/LegendItemRange.jsx
@@ -4,7 +4,13 @@ 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 }) => {
+const LegendItemRange = ({
+ name = '',
+ startValue,
+ endValue,
+ count,
+ decimalPlaces,
+}) => {
const {
systemSettings: { keyAnalysisDigitGroupSeparator },
} = useCachedData()
@@ -15,10 +21,16 @@ const LegendItemRange = ({ name = '', startValue, endValue, count }) => {
? ''
: `${formatWithSeparator(
startValue,
- keyAnalysisDigitGroupSeparator
+ keyAnalysisDigitGroupSeparator,
+ {
+ precision: decimalPlaces,
+ }
)} - ${formatWithSeparator(
endValue,
- keyAnalysisDigitGroupSeparator
+ keyAnalysisDigitGroupSeparator,
+ {
+ precision: decimalPlaces,
+ }
)}`
const countLabel =
count === undefined
@@ -36,6 +48,7 @@ const LegendItemRange = ({ name = '', startValue, endValue, count }) => {
LegendItemRange.propTypes = {
count: PropTypes.number,
+ decimalPlaces: PropTypes.number,
endValue: PropTypes.number,
name: PropTypes.string,
startValue: PropTypes.number,
diff --git a/src/components/optionSet/OptionStyle.jsx b/src/components/optionSet/OptionStyle.jsx
index f40b13750..ae50f0dd3 100644
--- a/src/components/optionSet/OptionStyle.jsx
+++ b/src/components/optionSet/OptionStyle.jsx
@@ -9,6 +9,7 @@ const OptionStyle = ({ name, color, onChange }) => (
color={color}
onChange={onChange}
className={styles.color}
+ width={50}
/>
{name}
diff --git a/src/components/optionSet/styles/OptionStyle.module.css b/src/components/optionSet/styles/OptionStyle.module.css
index 66627757e..b4e6ddb48 100644
--- a/src/components/optionSet/styles/OptionStyle.module.css
+++ b/src/components/optionSet/styles/OptionStyle.module.css
@@ -1,21 +1,18 @@
.item {
- white-space: nowrap;
+ display: flex;
+ align-items: center;
+ gap: var(--spacers-dp8);
font-size: 14px;
margin-bottom: var(--spacers-dp4);
}
.color {
- display: inline-block;
- vertical-align: top;
- width: var(--spacers-dp32);
- height: var(--spacers-dp32);
- margin: 0 var(--spacers-dp8) 0 0;
+ flex-shrink: 0;
+ margin-bottom: 0;
}
.label {
- display: inline-block;
- vertical-align: top;
- height: 32px;
- line-height: 32px;
overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
diff --git a/src/constants/actionTypes.js b/src/constants/actionTypes.js
index 3c0805e05..dac41a921 100644
--- a/src/constants/actionTypes.js
+++ b/src/constants/actionTypes.js
@@ -85,6 +85,8 @@ export const LAYER_EDIT_THEMATIC_MAP_TYPE_SET =
'LAYER_EDIT_THEMATIC_MAP_TYPE_SET'
export const LAYER_EDIT_CLASSIFICATION_SET = 'LAYER_EDIT_CLASSIFICATION_SET'
export const LAYER_EDIT_COLOR_SCALE_SET = 'LAYER_EDIT_COLOR_SCALE_SET'
+export const LAYER_EDIT_LEGEND_DECIMAL_PLACES_SET =
+ 'LAYER_EDIT_LEGEND_DECIMAL_PLACES_SET'
export const LAYER_EDIT_DATA_ITEM_SET = 'LAYER_EDIT_DATA_ITEM_SET'
export const LAYER_EDIT_EVENT_STATUS_SET = 'LAYER_EDIT_EVENT_STATUS_SET'
export const LAYER_EDIT_EVENT_COORDINATE_FIELD_SET =
diff --git a/src/loaders/eventLoader.js b/src/loaders/eventLoader.js
index 076775831..03797a9c5 100644
--- a/src/loaders/eventLoader.js
+++ b/src/loaders/eventLoader.js
@@ -15,6 +15,7 @@ import {
getPeriodNameFromId,
} from '../util/analytics.js'
import { cssColor, getContrastColor } from '../util/colors.js'
+import { parseJsonConfig } from '../util/config.js'
import { loadEventCoordinateFieldName } from '../util/coordinatesName.js'
import { getAnalyticsRequest, loadData } from '../util/event.js'
import { getBounds } from '../util/geojson.js'
@@ -101,6 +102,12 @@ const loadEventLayer = async ({
periodTypeData,
loadExtended,
}) => {
+ const { legendDecimalPlaces } = parseJsonConfig(config.config)
+ if (legendDecimalPlaces !== undefined) {
+ config.legendDecimalPlaces = legendDecimalPlaces
+ }
+ delete config.config
+
const {
columns,
endDate,
@@ -144,6 +151,9 @@ const loadEventLayer = async ({
getDateArray(endDate)
),
items: [],
+ ...(config.legendDecimalPlaces !== undefined && {
+ decimalPlaces: config.legendDecimalPlaces,
+ }),
}
// Delete serverCluster option if previously set
@@ -200,7 +210,8 @@ const loadEventLayer = async ({
const numericDataItemHeaders = config.headers.filter(
(header) =>
isValidUid(header.name) &&
- numberValueTypes.includes(header.valueType)
+ numberValueTypes.includes(header.valueType) &&
+ !header.optionSet
)
if (numericDataItemHeaders.length) {
diff --git a/src/loaders/thematicLoader.js b/src/loaders/thematicLoader.js
index 397d0eba5..d5f472a1f 100644
--- a/src/loaders/thematicLoader.js
+++ b/src/loaders/thematicLoader.js
@@ -28,6 +28,7 @@ import {
getApiResponseNames,
} from '../util/analytics.js'
import { getLegendItemForValue } from '../util/classify.js'
+import { parseJsonConfig } from '../util/config.js'
import { hasValue } from '../util/helpers.js'
import {
getPredefinedLegendItems,
@@ -62,6 +63,12 @@ const thematicLoader = async ({
noDataColor,
} = config
+ const { legendDecimalPlaces } = parseJsonConfig(config.config)
+ if (legendDecimalPlaces !== undefined) {
+ config.legendDecimalPlaces = legendDecimalPlaces
+ }
+ delete config.config
+
const dataItem = getDataItemFromColumns(columns)
const coordinateField = getCoordinateField(config)
@@ -177,6 +184,7 @@ const thematicLoader = async ({
method,
classes,
colorScale,
+ legendDecimalPlaces: config.legendDecimalPlaces,
})
legendItems = classification.items
valueFormat = classification.valueFormat
@@ -193,6 +201,9 @@ const thematicLoader = async ({
getDateArray(config.endDate)
),
items: legendItems,
+ ...(config.legendDecimalPlaces !== undefined && {
+ decimalPlaces: config.legendDecimalPlaces,
+ }),
}
if (dimensions && dimensions.length) {
@@ -223,6 +234,7 @@ const thematicLoader = async ({
minValue,
maxValue,
color: isSingleColor ? colorScale : null,
+ legendDecimalPlaces: config.legendDecimalPlaces,
}
}
@@ -309,10 +321,12 @@ const thematicLoader = async ({
properties.legend = legendItem.name // Shown in data table
properties.range = `${formatWithSeparator(
legendItem.startValue,
- keyAnalysisDigitGroupSeparator
+ keyAnalysisDigitGroupSeparator,
+ { precision: config.legendDecimalPlaces }
)} - ${formatWithSeparator(
legendItem.endValue,
- keyAnalysisDigitGroupSeparator
+ keyAnalysisDigitGroupSeparator,
+ { precision: config.legendDecimalPlaces }
)}` // Shown in data table
}
diff --git a/src/reducers/layerEdit.js b/src/reducers/layerEdit.js
index 9474d0c17..a5885cadf 100644
--- a/src/reducers/layerEdit.js
+++ b/src/reducers/layerEdit.js
@@ -309,6 +309,18 @@ const layerEdit = (state = null, action) => {
return newState
+ case types.LAYER_EDIT_LEGEND_DECIMAL_PLACES_SET:
+ newState = {
+ ...state,
+ legendDecimalPlaces: action.legendDecimalPlaces,
+ }
+
+ if (newState.legendDecimalPlaces === undefined) {
+ delete newState.legendDecimalPlaces
+ }
+
+ return newState
+
case types.LAYER_EDIT_EVENT_STATUS_SET:
newState = { ...state }
diff --git a/src/util/__tests__/classify.spec.js b/src/util/__tests__/classify.spec.js
index ace09a558..3da31f588 100644
--- a/src/util/__tests__/classify.spec.js
+++ b/src/util/__tests__/classify.spec.js
@@ -18,7 +18,7 @@ jest.mock('../helpers.js', () => ({
}))
jest.mock('../numbers.js', () => ({
- getRoundToPrecisionFn: jest.fn(() => (val) => Number(val.toFixed(2))),
+ getRoundToPrecisionFn: jest.fn((p) => (val) => Number(val.toFixed(p ?? 2))),
}))
describe('getLegendItemForValue', () => {
@@ -153,7 +153,7 @@ describe('getLegendItems', () => {
const { items } = getLegendItems(
values,
CLASSIFICATION_EQUAL_INTERVALS,
- 4
+ { numClasses: 4 }
)
expect(items).toEqual([
{ startValue: 0.0, endValue: 25.0 },
@@ -165,7 +165,9 @@ describe('getLegendItems', () => {
it('returns quantiles for CLASSIFICATION_EQUAL_COUNTS', () => {
const values = [1, 2, 3, 4, 5, 6]
- const { items } = getLegendItems(values, CLASSIFICATION_EQUAL_COUNTS, 3)
+ const { items } = getLegendItems(values, CLASSIFICATION_EQUAL_COUNTS, {
+ numClasses: 3,
+ })
expect(items).toEqual([
{ startValue: 1.0, endValue: 3.0 },
{ startValue: 3.0, endValue: 5.0 },
@@ -174,7 +176,7 @@ describe('getLegendItems', () => {
})
it('returns undefined if method is unknown', () => {
- const { items } = getLegendItems([0, 100], 'UNKNOWN', 3)
+ const { items } = getLegendItems([0, 100], 'UNKNOWN', { numClasses: 3 })
expect(items).toBeUndefined()
})
@@ -182,7 +184,7 @@ describe('getLegendItems', () => {
const { valueFormat } = getLegendItems(
[0, 100],
CLASSIFICATION_EQUAL_INTERVALS,
- 4
+ { numClasses: 4 }
)
expect(typeof valueFormat).toBe('function')
})
@@ -192,7 +194,7 @@ describe('getLegendItems', () => {
const { items } = getLegendItems(
values,
CLASSIFICATION_NATURAL_BREAKS_RANGES,
- 3
+ { numClasses: 3 }
)
expect(items).toHaveLength(3)
expect(items[0].endValue).toBe(items[1].startValue)
@@ -206,7 +208,7 @@ describe('getLegendItems', () => {
const { items } = getLegendItems(
values,
CLASSIFICATION_NATURAL_BREAKS_CLUSTERS,
- 3
+ { numClasses: 3 }
)
expect(items).toHaveLength(3)
expect(items[0].endValue).toBeLessThan(items[1].startValue)
@@ -215,7 +217,9 @@ describe('getLegendItems', () => {
it('returns logarithmic bins for strictly positive data', () => {
const values = [1, 10, 100, 1000, 10000]
- const { items } = getLegendItems(values, CLASSIFICATION_LOGARITHMIC, 4)
+ const { items } = getLegendItems(values, CLASSIFICATION_LOGARITHMIC, {
+ numClasses: 4,
+ })
expect(items).toHaveLength(4)
expect(items[0].startValue).toBe(1)
expect(items[3].endValue).toBe(10000)
@@ -227,12 +231,12 @@ describe('getLegendItems', () => {
const { items: logItems } = getLegendItems(
values,
CLASSIFICATION_LOGARITHMIC,
- 4
+ { numClasses: 4 }
)
const { items: equalItems } = getLegendItems(
values,
CLASSIFICATION_EQUAL_INTERVALS,
- 4
+ { numClasses: 4 }
)
expect(logItems).toEqual(equalItems)
})
@@ -242,7 +246,7 @@ describe('getLegendItems', () => {
const { items } = getLegendItems(
values,
CLASSIFICATION_STANDARD_DEVIATION,
- 5
+ { numClasses: 5 }
)
expect(items[0].startValue).toBe(0)
expect(items[items.length - 1].endValue).toBe(100)
@@ -254,14 +258,16 @@ describe('getLegendItems', () => {
const { items } = getLegendItems(
[0, 100],
CLASSIFICATION_PRETTY_BREAKS,
- 5
+ { numClasses: 5 }
)
expect(items[0].endValue).toBe(20)
})
it('removes consecutive duplicate bins', () => {
const values = [5, 5, 5, 5, 5, 10, 10, 10]
- const { items } = getLegendItems(values, CLASSIFICATION_EQUAL_COUNTS, 5)
+ const { items } = getLegendItems(values, CLASSIFICATION_EQUAL_COUNTS, {
+ numClasses: 5,
+ })
for (let i = 1; i < items.length; i++) {
expect(
items[i].startValue === items[i - 1].startValue &&
@@ -274,7 +280,7 @@ describe('getLegendItems', () => {
const { items } = getLegendItems(
[1, 2, 3],
CLASSIFICATION_NATURAL_BREAKS_RANGES,
- 5
+ { numClasses: 5 }
)
expect(items).toHaveLength(3)
})
@@ -283,7 +289,7 @@ describe('getLegendItems', () => {
const { items } = getLegendItems(
[1, 2, 3],
CLASSIFICATION_NATURAL_BREAKS_CLUSTERS,
- 5
+ { numClasses: 5 }
)
expect(items).toHaveLength(3)
})
@@ -292,20 +298,24 @@ describe('getLegendItems', () => {
const { items } = getLegendItems(
[1, 2, 3],
CLASSIFICATION_EQUAL_COUNTS,
- 5
+ { numClasses: 5 }
)
expect(items.length).toBeLessThanOrEqual(3)
})
it('does not throw for pretty breaks with few distinct values', () => {
expect(() =>
- getLegendItems([1, 2], CLASSIFICATION_PRETTY_BREAKS, 5)
+ getLegendItems([1, 2], CLASSIFICATION_PRETTY_BREAKS, {
+ numClasses: 5,
+ })
).not.toThrow()
})
it('does not throw for standard deviation with few distinct values', () => {
expect(() =>
- getLegendItems([1, 2], CLASSIFICATION_STANDARD_DEVIATION, 5)
+ getLegendItems([1, 2], CLASSIFICATION_STANDARD_DEVIATION, {
+ numClasses: 5,
+ })
).not.toThrow()
})
@@ -313,7 +323,7 @@ describe('getLegendItems', () => {
const { items } = getLegendItems(
[5, 5, 5, 5],
CLASSIFICATION_EQUAL_INTERVALS,
- 4
+ { numClasses: 4 }
)
expect(items).toEqual([{ startValue: 5, endValue: 5 }])
})
@@ -329,7 +339,9 @@ describe('getLegendItems', () => {
CLASSIFICATION_PRETTY_BREAKS,
]
methods.forEach((method) => {
- const { items } = getLegendItems([7, 7, 7], method, 5)
+ const { items } = getLegendItems([7, 7, 7], method, {
+ numClasses: 5,
+ })
expect(items).toEqual([{ startValue: 7, endValue: 7 }])
})
})
@@ -338,10 +350,34 @@ describe('getLegendItems', () => {
const { items } = getLegendItems(
[7, 7, 7],
CLASSIFICATION_EQUAL_INTERVALS,
- 5
+ { numClasses: 5 }
)
expect(getLegendItemForValue({ value: 7, legendItems: items })).toEqual(
items[0]
)
})
+
+ it('respects precision: 0 by rounding range values to whole numbers', () => {
+ const { items } = getLegendItems(
+ [0, 100],
+ CLASSIFICATION_EQUAL_INTERVALS,
+ { numClasses: 4, precision: 0 }
+ )
+ items.forEach(({ startValue, endValue }) => {
+ expect(Number.isInteger(startValue)).toBe(true)
+ expect(Number.isInteger(endValue)).toBe(true)
+ })
+ })
+
+ it('respects precision: 3 by rounding range values to 3 decimal places', () => {
+ const { items } = getLegendItems(
+ [0, 1],
+ CLASSIFICATION_EQUAL_INTERVALS,
+ { numClasses: 4, precision: 3 }
+ )
+ items.forEach(({ startValue, endValue }) => {
+ expect(startValue).toBe(Number.parseFloat(startValue.toFixed(3)))
+ expect(endValue).toBe(Number.parseFloat(endValue.toFixed(3)))
+ })
+ })
})
diff --git a/src/util/__tests__/favorites.spec.js b/src/util/__tests__/favorites.spec.js
index c81c444f4..76d9985d6 100644
--- a/src/util/__tests__/favorites.spec.js
+++ b/src/util/__tests__/favorites.spec.js
@@ -286,6 +286,90 @@ describe('cleanMapConfig', () => {
})
})
+ test('serializes legendDecimalPlaces into config JSON for thematic layer', () => {
+ const config = {
+ mapViews: [
+ {
+ layer: 'thematic',
+ name: 'ANC 1 Coverage',
+ opacity: 1,
+ legendDecimalPlaces: 2,
+ isLoaded: true,
+ isLoading: false,
+ isExpanded: true,
+ isVisible: true,
+ },
+ ],
+ }
+
+ const cleanedConfig = cleanMapConfig({
+ config,
+ defaultBasemapId: 'thedefaultBasemap',
+ })
+
+ expect(cleanedConfig.mapViews[0].config).toBe(
+ '{"legendDecimalPlaces":2}'
+ )
+ expect(cleanedConfig.mapViews[0]).not.toHaveProperty(
+ 'legendDecimalPlaces'
+ )
+ })
+
+ test('serializes legendDecimalPlaces into config JSON for event layer', () => {
+ const config = {
+ mapViews: [
+ {
+ layer: 'event',
+ name: 'Birth weight',
+ opacity: 1,
+ legendDecimalPlaces: 0,
+ isLoaded: true,
+ isLoading: false,
+ isExpanded: true,
+ isVisible: true,
+ },
+ ],
+ }
+
+ const cleanedConfig = cleanMapConfig({
+ config,
+ defaultBasemapId: 'thedefaultBasemap',
+ })
+
+ expect(cleanedConfig.mapViews[0].config).toBe(
+ '{"legendDecimalPlaces":0}'
+ )
+ expect(cleanedConfig.mapViews[0]).not.toHaveProperty(
+ 'legendDecimalPlaces'
+ )
+ })
+
+ test('does not add config for thematic layer without legendDecimalPlaces', () => {
+ const config = {
+ mapViews: [
+ {
+ layer: 'thematic',
+ name: 'ANC 1 Coverage',
+ opacity: 1,
+ isLoaded: true,
+ isLoading: false,
+ isExpanded: true,
+ isVisible: true,
+ },
+ ],
+ }
+
+ const cleanedConfig = cleanMapConfig({
+ config,
+ defaultBasemapId: 'thedefaultBasemap',
+ })
+
+ expect(cleanedConfig.mapViews[0]).not.toHaveProperty('config')
+ expect(cleanedConfig.mapViews[0]).not.toHaveProperty(
+ 'legendDecimalPlaces'
+ )
+ })
+
test('correctly converts TEI mapview', () => {
const config = {
bounds: [
diff --git a/src/util/__tests__/legend.spec.js b/src/util/__tests__/legend.spec.js
index 115860109..c7492620c 100644
--- a/src/util/__tests__/legend.spec.js
+++ b/src/util/__tests__/legend.spec.js
@@ -14,6 +14,7 @@ import {
getPredefinedLegendItems,
getAutomaticLegendItems,
getRenderingLabel,
+ parseRange,
} from '../legend.js'
describe('sortLegendItems', () => {
@@ -185,6 +186,56 @@ describe('legend utils', () => {
expect(items.length).toBe(3)
expect(typeof valueFormat).toBe('function')
})
+
+ it('does not add decimalPlaces when legendDecimalPlaces is undefined', () => {
+ const { items } = getAutomaticLegendItems({
+ data: [0, 100],
+ method: CLASSIFICATION_EQUAL_INTERVALS,
+ classes: 4,
+ colorScale: defaultColorScale,
+ })
+ items.forEach((item) => {
+ expect(item).not.toHaveProperty('decimalPlaces')
+ })
+ })
+
+ it('applies legendDecimalPlaces as precision to item boundary values', () => {
+ const { items } = getAutomaticLegendItems({
+ data: [0, 0.5, 1],
+ method: CLASSIFICATION_EQUAL_INTERVALS,
+ classes: 3,
+ colorScale: defaultColorScale,
+ legendDecimalPlaces: 2,
+ })
+ expect(items[0].endValue).toBe(0.33)
+ expect(items[1].startValue).toBe(0.33)
+ expect(items[1].endValue).toBe(0.67)
+ expect(items[2].startValue).toBe(0.67)
+ })
+
+ it('rounds item boundary values to integers when legendDecimalPlaces is 0', () => {
+ const { items } = getAutomaticLegendItems({
+ data: [0, 0.5, 1],
+ method: CLASSIFICATION_EQUAL_INTERVALS,
+ classes: 3,
+ colorScale: defaultColorScale,
+ legendDecimalPlaces: 0,
+ })
+ items.forEach((item) => {
+ expect(Number.isInteger(item.startValue)).toBe(true)
+ expect(Number.isInteger(item.endValue)).toBe(true)
+ })
+ })
+ })
+
+ describe('parseRange', () => {
+ it('parses a range string into numeric start and end values', () => {
+ expect(parseRange('10 - 20')).toEqual([10, 20])
+ })
+
+ it('parses a range string with decimal values', () => {
+ expect(parseRange('1.5 - 3.75')).toEqual([1.5, 3.75])
+ })
})
describe('getRenderingLabel', () => {
diff --git a/src/util/classify.js b/src/util/classify.js
index eb3b3cd5c..5cf68d202 100644
--- a/src/util/classify.js
+++ b/src/util/classify.js
@@ -48,7 +48,11 @@ export const getLegendItemForValue = ({
)
}
-export const getLegendItems = (values, method, numClasses) => {
+export const getLegendItems = (
+ values,
+ method,
+ { numClasses, precision } = {}
+) => {
const minValue = values[0]
const maxValue = values[values.length - 1]
if (minValue === maxValue) {
@@ -61,25 +65,27 @@ export const getLegendItems = (values, method, numClasses) => {
const k = Math.min(numClasses, distinctValues.length)
let classification
-
if (method === CLASSIFICATION_EQUAL_INTERVALS) {
classification = getEqualIntervals(minValue, maxValue, {
numClasses: k,
+ precision,
})
} else if (method === CLASSIFICATION_EQUAL_COUNTS) {
- classification = getQuantiles(values, { numClasses: k })
+ classification = getQuantiles(values, { numClasses: k, precision })
} else if (method === CLASSIFICATION_NATURAL_BREAKS_RANGES) {
classification = getCkMeans(values, {
numClasses: k,
continuous: true,
+ precision,
})
} else if (method === CLASSIFICATION_NATURAL_BREAKS_CLUSTERS) {
classification = getCkMeans(values, {
numClasses: k,
continuous: false,
+ precision,
})
} else if (method === CLASSIFICATION_STANDARD_DEVIATION) {
- classification = getStandardDeviation(values, { numClasses })
+ classification = getStandardDeviation(values, { numClasses, precision })
} else if (method === CLASSIFICATION_LOGARITHMIC) {
if (minValue <= 0) {
// Logarithmic scale requires strictly positive values.
@@ -89,12 +95,19 @@ export const getLegendItems = (values, method, numClasses) => {
// them to the unclassified bucket instead of falling back.
classification = getEqualIntervals(minValue, maxValue, {
numClasses,
+ precision,
})
} else {
- classification = getLogarithmic(minValue, maxValue, { numClasses })
+ classification = getLogarithmic(minValue, maxValue, {
+ numClasses,
+ precision,
+ })
}
} else if (method === CLASSIFICATION_PRETTY_BREAKS) {
- classification = getPrettyBreaks(minValue, maxValue, { numClasses })
+ classification = getPrettyBreaks(minValue, maxValue, {
+ numClasses,
+ precision,
+ })
}
if (!classification) {
@@ -111,11 +124,11 @@ export const getLegendItems = (values, method, numClasses) => {
}
}
-const getEqualIntervals = (minValue, maxValue, { numClasses }) => {
+const getEqualIntervals = (minValue, maxValue, { numClasses, precision }) => {
const items = []
const classSize = (maxValue - minValue) / numClasses
- const precision = precisionRound(classSize, maxValue)
- const valueFormat = getRoundToPrecisionFn(precision)
+ const resolvedPrecision = precision ?? precisionRound(classSize, maxValue)
+ const valueFormat = getRoundToPrecisionFn(resolvedPrecision)
for (let i = 0; i < numClasses; i++) {
const startValue = minValue + i * classSize
@@ -130,16 +143,15 @@ const getEqualIntervals = (minValue, maxValue, { numClasses }) => {
return { items, valueFormat }
}
-const getQuantiles = (values, { numClasses }) => {
+const getQuantiles = (values, { numClasses, precision }) => {
const minValue = values[0]
const maxValue = values[values.length - 1]
const items = []
const valuesCount = values.length / numClasses
- const precision = precisionRound(
- (maxValue - minValue) / numClasses,
- maxValue
- )
- const valueFormat = getRoundToPrecisionFn(precision)
+ const resolvedPrecision =
+ precision ??
+ precisionRound((maxValue - minValue) / numClasses, maxValue)
+ const valueFormat = getRoundToPrecisionFn(resolvedPrecision)
let lastValuePosition = valuesCount
if (values.length > 0) {
@@ -162,14 +174,13 @@ const getQuantiles = (values, { numClasses }) => {
}
}
-const getCkMeans = (values, { numClasses, continuous }) => {
+const getCkMeans = (values, { numClasses, continuous, precision }) => {
const minValue = values[0]
const maxValue = values[values.length - 1]
- const precision = precisionRound(
- (maxValue - minValue) / numClasses,
- maxValue
- )
- const valueFormat = getRoundToPrecisionFn(precision)
+ const resolvedPrecision =
+ precision ??
+ precisionRound((maxValue - minValue) / numClasses, maxValue)
+ const valueFormat = getRoundToPrecisionFn(resolvedPrecision)
const k = Math.min(numClasses, values.length)
const clusters = ckmeans(values, k)
@@ -205,7 +216,7 @@ const getCkMeans = (values, { numClasses, continuous }) => {
}
}
-const getStandardDeviation = (values, { numClasses }) => {
+const getStandardDeviation = (values, { numClasses, precision }) => {
// TODO: DHIS2-19812 std - dev classification is best interpreted when
// breaks fall at μ±Nσ regardless of data extremes. Currently:
// - breaks outside [minValue, maxValue] are filtered (producing
@@ -218,11 +229,10 @@ const getStandardDeviation = (values, { numClasses }) => {
const maxValue = values[values.length - 1]
const mu = mean(values)
const sigma = standardDeviation(values)
- const precision = precisionRound(
- (maxValue - minValue) / numClasses,
- maxValue
- )
- const valueFormat = getRoundToPrecisionFn(precision)
+ const resolvedPrecision =
+ precision ??
+ precisionRound((maxValue - minValue) / numClasses, maxValue)
+ const valueFormat = getRoundToPrecisionFn(resolvedPrecision)
// Place breaks at 1-sigma intervals centered on the mean.
const internalBreaks = []
@@ -249,15 +259,14 @@ const getStandardDeviation = (values, { numClasses }) => {
}
}
-const getLogarithmic = (minValue, maxValue, { numClasses }) => {
+const getLogarithmic = (minValue, maxValue, { numClasses, precision }) => {
const logMin = Math.log(minValue)
const logMax = Math.log(maxValue)
const logStep = (logMax - logMin) / numClasses
- const precision = precisionRound(
- (maxValue - minValue) / numClasses,
- maxValue
- )
- const valueFormat = getRoundToPrecisionFn(precision)
+ const resolvedPrecision =
+ precision ??
+ precisionRound((maxValue - minValue) / numClasses, maxValue)
+ const valueFormat = getRoundToPrecisionFn(resolvedPrecision)
const items = []
for (let i = 0; i < numClasses; i++) {
@@ -273,15 +282,17 @@ const getLogarithmic = (minValue, maxValue, { numClasses }) => {
return { items, valueFormat }
}
-const getPrettyBreaks = (minValue, maxValue, { numClasses }) => {
+const getPrettyBreaks = (minValue, maxValue, { numClasses, precision }) => {
const range = maxValue - minValue
const roughStep = range / numClasses
const magnitude = Math.pow(10, Math.floor(Math.log10(roughStep)))
const niceSteps = [1, 2, 5].map((n) => n * magnitude)
const niceStep = niceSteps.findLast((s) => s <= roughStep) ?? niceSteps[0]
- const precision = precisionRound(niceStep, maxValue)
- const valueFormat = getRoundToPrecisionFn(precision)
+ const resolvedPrecision =
+ precision ??
+ precisionRound((maxValue - minValue) / numClasses, maxValue)
+ const valueFormat = getRoundToPrecisionFn(resolvedPrecision)
const internalBreaks = []
let b = Math.ceil(minValue / niceStep) * niceStep
diff --git a/src/util/config.js b/src/util/config.js
new file mode 100644
index 000000000..8fd602ffa
--- /dev/null
+++ b/src/util/config.js
@@ -0,0 +1,10 @@
+export const parseJsonConfig = (jsonString) => {
+ if (!jsonString || typeof jsonString !== 'string') {
+ return {}
+ }
+ try {
+ return JSON.parse(jsonString)
+ } catch {
+ return {}
+ }
+}
diff --git a/src/util/favorites.js b/src/util/favorites.js
index 9b5271264..c729cec26 100644
--- a/src/util/favorites.js
+++ b/src/util/favorites.js
@@ -1,7 +1,9 @@
import { isNil, omitBy, pick, isObject, omit } from 'lodash/fp'
import {
EARTH_ENGINE_LAYER,
+ EVENT_LAYER,
GEOJSON_URL_LAYER,
+ THEMATIC_LAYER,
TRACKED_ENTITY_LAYER,
} from '../constants/layers.js'
@@ -52,6 +54,7 @@ const validLayerProperties = [
'labelFontWeight',
'labelFontColor',
'labelTemplate',
+ 'legendDecimalPlaces',
'lastUpdated',
'layer',
'layerId',
@@ -191,6 +194,16 @@ const models2objects = (layer, cleanMapviewConfig) => {
delete layer.relationshipLineColor
delete layer.relationshipOutsideProgram
delete layer.periodType
+ } else if (layerType === THEMATIC_LAYER || layerType === EVENT_LAYER) {
+ if (cleanMapviewConfig) {
+ if (layer.legendDecimalPlaces !== undefined) {
+ layer.config = JSON.stringify({
+ legendDecimalPlaces: layer.legendDecimalPlaces,
+ })
+ }
+ }
+
+ delete layer.legendDecimalPlaces
} else if (layerType === GEOJSON_URL_LAYER) {
if (cleanMapviewConfig) {
layer.config = {
diff --git a/src/util/legend.js b/src/util/legend.js
index d21c1499f..390d3f561 100644
--- a/src/util/legend.js
+++ b/src/util/legend.js
@@ -12,6 +12,7 @@ import {
} from '../constants/layers.js'
import { getLegendItems } from '../util/classify.js'
import { defaultClasses, defaultColorScale } from '../util/colors.js'
+import { parseWithSeparator } from './numbers.js'
const INDICATOR_QUERY = {
dimension: {
@@ -73,6 +74,11 @@ export const sortLegendItems = (items) =>
: bRange.start - aRange.start
})
+export const parseRange = (str) => {
+ const [start, end] = str.split(' - ')
+ return [parseWithSeparator(start), parseWithSeparator(end)]
+}
+
export const loadDataItemLegendSet = async (dataItem, engine) => {
if (!dataItem) {
return null
@@ -154,12 +160,16 @@ export const getAutomaticLegendItems = ({
method = CLASSIFICATION_EQUAL_INTERVALS,
classes = defaultClasses,
colorScale = defaultColorScale,
+ legendDecimalPlaces,
}) => {
if (data.length === 0) {
return { items: [] }
}
- const classification = getLegendItems(data, method, classes)
+ const classification = getLegendItems(data, method, {
+ numClasses: classes,
+ precision: legendDecimalPlaces,
+ })
return {
items: classification.items.map((item, index) => ({
...item,
diff --git a/src/util/styleByDataItem.js b/src/util/styleByDataItem.js
index c58c89991..b3d3505ad 100644
--- a/src/util/styleByDataItem.js
+++ b/src/util/styleByDataItem.js
@@ -137,6 +137,7 @@ const styleByNumeric = async (config, engine) => {
colorScale,
eventPointColor,
eventPointRadius,
+ legendDecimalPlaces,
} = config
let valueFormat
@@ -170,6 +171,7 @@ const styleByNumeric = async (config, engine) => {
method,
classes,
colorScale,
+ legendDecimalPlaces,
})
legend.items = classification.items
valueFormat = classification.valueFormat