Skip to content
Open
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
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)
107 changes: 74 additions & 33 deletions src/components/legend/Bubbles.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,27 @@ export const digitWidth = 6.8
export const guideLength = 16
export const textPadding = 4

const Bubbles = ({
radiusLow,
radiusHigh,
const filterBubbleText = (bubbles, showNumbers) => {
if (!showNumbers) {
return
}
bubbles.forEach((b, i) => {
if (!showNumbers.includes(i)) {
delete b.text
}
})
}

const computeBubbleLayout = ({
bubbleClasses,
color,
minValue,
maxValue,
classes,
isPlugin,
scale,
radiusLow,
radiusHigh,
legendWidth,
}) => {
const legendWidth = isPlugin ? 150 : 245
const noDataClass = classes.find((c) => c.noData === true)
const bubbleClasses = classes.filter((c) => !c.noData)

const height = radiusHigh * 2 + 4
const scale = scaleSqrt().range([radiusLow, radiusHigh])

if (isNaN(radiusLow) || isNaN(radiusHigh)) {
return null
}

const bubbles = bubbleClasses.length
? createBubbleItems({
classes: bubbleClasses,
Expand All @@ -53,32 +54,76 @@ const Bubbles = ({
radiusHigh,
})

const { alternate, offset, showNumbers } = computeLayout({
const layout = computeLayout({
bubbles,
bubbleClasses,
radiusHigh,
legendWidth,
})
filterBubbleText(bubbles, layout.showNumbers)

return { bubbles, alternate: layout.alternate, offset: layout.offset }
}

if (showNumbers) {
bubbles.forEach((b, i) => {
if (!showNumbers.includes(i)) {
delete b.text
}
})
const Bubbles = ({
radiusLow,
radiusHigh,
color,
minValue,
maxValue,
classes,
isPlugin,
}) => {
const legendWidth = isPlugin ? 150 : 245
const noDataClass = classes.find((c) => c.noData === true)
const bubbleClasses = classes.filter((c) => !c.noData)
const hasDataRange = minValue != null && maxValue != null
const height = hasDataRange
? radiusHigh * 2 + 4
: THEMATIC_RADIUS_DEFAULT + 2
const scale = scaleSqrt().range([radiusLow, radiusHigh])
const noDataTranslateY = hasDataRange ? 20 : 0

if (isNaN(radiusLow) || isNaN(radiusHigh)) {
return null
}

if (!hasDataRange && !noDataClass) {
return null
}

let bubbles = []
let alternate = false
let offset = '2'

if (hasDataRange) {
;({ bubbles, alternate, offset } = computeBubbleLayout({
bubbleClasses,
color,
minValue,
maxValue,
scale,
radiusLow,
radiusHigh,
legendWidth,
}))
}

const xTranslate = alternate ? offset : '2'

return (
<div style={style}>
<svg
width={legendWidth}
height={
height +
20 +
(noDataClass ? THEMATIC_RADIUS_DEFAULT + 1 : 0)
hasDataRange
? height +
20 +
(noDataClass ? THEMATIC_RADIUS_DEFAULT + 1 : 0)
: 20
}
>
<g transform={`translate(${alternate ? offset : '2'} 10)`}>
<g transform={`translate(${xTranslate} 10)`}>
{bubbles.map((bubble, i) => (
<Bubble
key={i}
Expand All @@ -93,9 +138,7 @@ const Bubbles = ({
<>
{' '}
<circle
transform={`translate(${
alternate ? offset : '2'
} 20)`}
transform={`translate(${xTranslate} ${noDataTranslateY})`}
cx={radiusHigh}
cy={height}
r={THEMATIC_RADIUS_DEFAULT}
Expand All @@ -106,9 +149,7 @@ const Bubbles = ({
}}
/>
<text
transform={`translate(${
alternate ? offset : '2'
} 20)`}
transform={`translate(${xTranslate} ${noDataTranslateY})`}
x={radiusHigh + THEMATIC_RADIUS_DEFAULT + 5}
y={height + 4}
fontSize={12}
Expand Down
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
Loading
Loading