Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
16 changes: 14 additions & 2 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-12T18:11:56.380Z\n"
"PO-Revision-Date: 2026-03-12T18:11:56.380Z\n"
"POT-Creation-Date: 2026-04-24T10:52:00.804Z\n"
"PO-Revision-Date: 2026-04-24T10:52:00.804Z\n"

msgid "2020"
msgstr "2020"
Expand Down Expand Up @@ -1638,6 +1638,18 @@ msgstr "Equal intervals"
msgid "Equal counts"
msgstr "Equal counts"

msgid "Natural breaks (intervals)"
msgstr "Natural breaks (intervals)"

msgid "Natural breaks (clusters)"
msgstr "Natural breaks (clusters)"

msgid "Pretty breaks"
msgstr "Pretty breaks"

msgid "Logarithmic scale"
msgstr "Logarithmic scale"

msgid "Symbol"
msgstr "Symbol"

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"redux": "^4.2.1",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.4.2",
"simple-statistics": "^7.8.9",
"styled-jsx": "^4.0.1",
"url-polyfill": "^1.1.14"
},
Expand Down
18 changes: 16 additions & 2 deletions src/components/classification/LegendSetSelect.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useDataQuery } from '@dhis2/app-runtime'
import i18n from '@dhis2/d2-i18n'
import PropTypes from 'prop-types'
import React from 'react'
import React, { useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { setLegendSet } from '../../actions/layerEdit.js'
import { SelectField } from '../core/index.js'
Expand All @@ -21,11 +21,22 @@ const style = {
width: '100%',
}

const LegendSetSelect = ({ legendSetError }) => {
const LegendSetSelect = ({ defaultLegendSet, legendSetError }) => {
const legendSet = useSelector((state) => state.layerEdit.legendSet)
const dispatch = useDispatch()
const { loading, error, data } = useDataQuery(LEGEND_SETS_QUERY)

useEffect(() => {
if (!legendSet && data?.sets.legendSets?.length) {
const legendSets = data.sets.legendSets
const defaultItem = defaultLegendSet
? legendSets.find((ls) => ls.id === defaultLegendSet.id) ??
legendSets[0]
: legendSets[0]
dispatch(setLegendSet(defaultItem))
}
}, [legendSet, data, defaultLegendSet, dispatch])

return (
<SelectField
label={i18n.t('Legend set')}
Expand All @@ -40,6 +51,9 @@ const LegendSetSelect = ({ legendSetError }) => {
}

LegendSetSelect.propTypes = {
defaultLegendSet: PropTypes.shape({
id: PropTypes.string.isRequired,
}),
legendSetError: PropTypes.string,
}

Expand Down
16 changes: 9 additions & 7 deletions src/components/classification/LegendTypeSelect.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,26 @@ import { connect } from 'react-redux'
import { setClassification } from '../../actions/layerEdit.js'
import {
getLegendTypes,
CLASSIFICATION_EQUAL_INTERVALS,
CLASSIFICATION_EQUAL_COUNTS,
getClassificationTypes,
CLASSIFICATION_AUTO_DEFAULT,
} from '../../constants/layers.js'
import { Radio, RadioGroup } from '../core/index.js'

// Select between user defined (automatic), predefined or single color
const LegendTypeSelect = ({ mapType, method, setClassification }) =>
method ? (
<RadioGroup
value={
method === CLASSIFICATION_EQUAL_COUNTS
? CLASSIFICATION_EQUAL_INTERVALS
value={String(
getClassificationTypes()
.map(({ id }) => id)
.includes(method)
? CLASSIFICATION_AUTO_DEFAULT
: method
}
)}
onChange={(method) => setClassification(Number(method))}
>
{getLegendTypes(mapType === 'BUBBLE').map(({ id, name }) => (
<Radio key={id} value={id} label={name} />
<Radio key={id} value={String(id)} label={name} />
))}
</RadioGroup>
) : null
Expand Down
25 changes: 8 additions & 17 deletions src/components/classification/NumericLegendStyle.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import PropTypes from 'prop-types'
import React, { useEffect } from 'react'
import { connect } from 'react-redux'
import { setClassification, setLegendSet } from '../../actions/layerEdit.js'
import { setClassification } from '../../actions/layerEdit.js'
import {
CLASSIFICATION_PREDEFINED,
CLASSIFICATION_EQUAL_INTERVALS,
CLASSIFICATION_AUTO_DEFAULT,
CLASSIFICATION_SINGLE_COLOR,
} from '../../constants/layers.js'
import Classification from './Classification.jsx'
Expand All @@ -18,9 +18,7 @@ const NumericLegendStyle = (props) => {
mapType,
method,
dataItem,
legendSet,
setClassification,
setLegendSet,
legendSetError,
style,
} = props
Expand All @@ -35,18 +33,11 @@ const NumericLegendStyle = (props) => {
setClassification(
dataItem && dataItem.legendSet
? CLASSIFICATION_PREDEFINED
: CLASSIFICATION_EQUAL_INTERVALS
: CLASSIFICATION_AUTO_DEFAULT
)
}
}, [method, dataItem, setClassification])

useEffect(() => {
// Set legend set defined for data item in use by default
if (isPredefined && !legendSet && dataItem?.legendSet) {
setLegendSet(dataItem.legendSet)
}
}, [isPredefined, legendSet, dataItem, setLegendSet])

return (
<div style={style}>
<LegendTypeSelect
Expand All @@ -57,7 +48,10 @@ const NumericLegendStyle = (props) => {
{isSingleColor ? (
<SingleColor />
) : isPredefined ? (
<LegendSetSelect legendSetError={legendSetError} />
<LegendSetSelect
legendSetError={legendSetError}
defaultLegendSet={dataItem?.legendSet}
/>
) : (
<Classification />
)}
Expand All @@ -67,9 +61,7 @@ const NumericLegendStyle = (props) => {

NumericLegendStyle.propTypes = {
setClassification: PropTypes.func.isRequired,
setLegendSet: PropTypes.func.isRequired,
dataItem: PropTypes.object,
legendSet: PropTypes.object,
legendSetError: PropTypes.string,
mapType: PropTypes.string,
method: PropTypes.number,
Expand All @@ -79,7 +71,6 @@ NumericLegendStyle.propTypes = {
export default connect(
({ layerEdit }) => ({
method: layerEdit.method,
legendSet: layerEdit.legendSet,
}),
{ setClassification, setLegendSet }
{ setClassification }
)(NumericLegendStyle)
28 changes: 27 additions & 1 deletion src/constants/layers.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,17 @@ export const EE_BUFFER = 5000
export const CLASSIFICATION_PREDEFINED = 1
export const CLASSIFICATION_EQUAL_INTERVALS = 2
export const CLASSIFICATION_EQUAL_COUNTS = 3
export const CLASSIFICATION_NATURAL_BREAKS_RANGES = 4
export const CLASSIFICATION_NATURAL_BREAKS_CLUSTERS = 5
export const CLASSIFICATION_PRETTY_BREAKS = 6
export const CLASSIFICATION_LOGARITHMIC = 7
export const CLASSIFICATION_STANDARD_DEVIATION = 8
export const CLASSIFICATION_SINGLE_COLOR = 10
export const CLASSIFICATION_AUTO_DEFAULT = CLASSIFICATION_EQUAL_INTERVALS

export const getLegendTypes = (isBubble) => [
{
id: CLASSIFICATION_EQUAL_INTERVALS,
id: CLASSIFICATION_AUTO_DEFAULT,
name: i18n.t('Automatic color legend'),
},
{
Expand All @@ -174,6 +180,26 @@ export const getClassificationTypes = () => [
id: CLASSIFICATION_EQUAL_COUNTS,
name: i18n.t('Equal counts'),
},
{
id: CLASSIFICATION_NATURAL_BREAKS_RANGES,
name: i18n.t('Natural breaks (intervals)'),
},
{
id: CLASSIFICATION_NATURAL_BREAKS_CLUSTERS,
name: i18n.t('Natural breaks (clusters)'),
},
{
id: CLASSIFICATION_PRETTY_BREAKS,
name: i18n.t('Pretty breaks'),
},
{
id: CLASSIFICATION_LOGARITHMIC,
name: i18n.t('Logarithmic scale'),
},
{
id: CLASSIFICATION_STANDARD_DEVIATION,
name: i18n.t('Standard deviation'),
},
]

export const STYLE_TYPE_COLOR = 'COLOR'
Expand Down
1 change: 1 addition & 0 deletions src/loaders/thematicLoader.js
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ const thematicLoader = async ({
getLegendItemForValue({
value,
valueFormat,
method,
legendItems: legend.items.filter((item) => !item.noData),
clamp: method !== CLASSIFICATION_PREDEFINED,
})
Expand Down
10 changes: 4 additions & 6 deletions src/reducers/layerEdit.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ import * as types from '../constants/actionTypes.js'
import { EVENT_STATUS_ALL } from '../constants/eventStatuses.js'
import {
CLASSIFICATION_SINGLE_COLOR,
CLASSIFICATION_EQUAL_INTERVALS,
CLASSIFICATION_EQUAL_COUNTS,
CLASSIFICATION_PREDEFINED,
getClassificationTypes,
THEMATIC_CHOROPLETH,
EE_BUFFER,
NONE,
Expand Down Expand Up @@ -279,10 +278,9 @@ const layerEdit = (state = null, action) => {

if (
state.method === CLASSIFICATION_SINGLE_COLOR ||
![
CLASSIFICATION_EQUAL_INTERVALS,
CLASSIFICATION_EQUAL_COUNTS,
].includes(action.method)
!getClassificationTypes()
.map((t) => t.id)
.includes(action.method)
) {
delete newState.colorScale
delete newState.classes
Expand Down
56 changes: 56 additions & 0 deletions src/util/__tests__/bubbles.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,34 @@ jest.mock('../numbers.js', () => ({
}))

describe('createBubbleItems', () => {
it('should return a single bubble when minValue === maxValue', () => {
const mockScale = { domain: jest.fn(() => () => 5) }
const classes = [{ startValue: 7, endValue: 7, color: '#abc' }]
const bubbles = createBubbleItems({
classes,
minValue: 7,
maxValue: 7,
scale: mockScale,
radiusHigh: 20,
})
expect(bubbles).toHaveLength(1)
expect(bubbles[0].text).toBe('7')
expect(bubbles[0].color).toBe('#abc')
})

it('should not produce NaN text when minValue === maxValue', () => {
const mockScale = { domain: jest.fn(() => () => 5) }
const classes = [{ startValue: 42, endValue: 42, color: '#def' }]
const bubbles = createBubbleItems({
classes,
minValue: 42,
maxValue: 42,
scale: mockScale,
radiusHigh: 20,
})
expect(bubbles[0].text).not.toContain('NaN')
})

it('should create bubble items from class breaks', () => {
const mockScale = {
domain: jest.fn(() => (value) => value * 2),
Expand All @@ -57,6 +85,34 @@ describe('createBubbleItems', () => {
})

describe('createSingleColorBubbles', () => {
it('should return a single bubble when minValue === maxValue', () => {
const mockScale = { domain: jest.fn(() => () => 5) }
const bubbles = createSingleColorBubbles({
color: '#abc',
minValue: 50,
maxValue: 50,
scale: mockScale,
radiusLow: 5,
radiusHigh: 20,
})
expect(bubbles).toHaveLength(1)
expect(bubbles[0].text).toBe('50')
expect(bubbles[0].color).toBe('#abc')
})

it('should not produce NaN text when minValue === maxValue', () => {
const mockScale = { domain: jest.fn(() => () => 5) }
const bubbles = createSingleColorBubbles({
color: '#abc',
minValue: 100,
maxValue: 100,
scale: mockScale,
radiusLow: 5,
radiusHigh: 20,
})
expect(bubbles[0].text).not.toContain('NaN')
})

it('should return three bubbles with single color and formatted values', () => {
const mockScale = {
domain: jest.fn(() => (value) => value * 0.5),
Expand Down
Loading
Loading