diff --git a/i18n/en.pot b/i18n/en.pot index 72d26d6fad..53625cd57f 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-09-01T12:42:29.454Z\n" -"PO-Revision-Date: 2026-09-01T12:42:29.454Z\n" +"POT-Creation-Date: 2026-09-10T13:44:55.087Z\n" +"PO-Revision-Date: 2026-09-10T13:44:55.087Z\n" msgid "2020" msgstr "2020" @@ -38,6 +38,9 @@ msgstr "Classification" msgid "Classes" msgstr "Classes" +msgid "Color scale" +msgstr "Color scale" + msgid "Auto" msgstr "Auto" @@ -437,6 +440,9 @@ msgstr "Group events" msgid "View all events" msgstr "View all events" +msgid "View heat map" +msgstr "View heat map" + msgid "Radius" msgstr "Radius" diff --git a/package.json b/package.json index 83ec322d06..34a6e348b4 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "@dhis2/analytics": "^29.5.5", "@dhis2/app-runtime": "^3.17.3", "@dhis2/app-service-datastore": "^1.0.0-beta.3", - "@dhis2/maps-gl": "^4.4.3", + "@dhis2/maps-gl": "git+https://github.com/d2-ci/maps-gl.git#a0c0a650843414776d42d94d38b476499823cffb", "@dhis2/ui": "^10.17.0", "@dnd-kit/core": "^6.0.8", "@dnd-kit/modifiers": "^9.0.0", diff --git a/public/images/heatmap.png b/public/images/heatmap.png new file mode 100644 index 0000000000..5e1b879d8f Binary files /dev/null and b/public/images/heatmap.png differ diff --git a/src/actions/layerEdit.js b/src/actions/layerEdit.js index 988c73484e..f4617dd239 100644 --- a/src/actions/layerEdit.js +++ b/src/actions/layerEdit.js @@ -121,6 +121,12 @@ export const setFallbackCoordinateField = (fieldId) => ({ fieldId, }) +// Set if event heatmap should be used (event) +export const setEventHeatmap = (checked) => ({ + type: types.LAYER_EDIT_EVENT_HEATMAP_SET, + checked, +}) + // Set if event clustering should be used (event) export const setEventClustering = (checked) => ({ type: types.LAYER_EDIT_EVENT_CLUSTERING_SET, diff --git a/src/actions/layers.js b/src/actions/layers.js index 1af92b1d58..ccf663e1cf 100644 --- a/src/actions/layers.js +++ b/src/actions/layers.js @@ -54,6 +54,20 @@ export const changeLayerOpacity = (id, opacity) => ({ opacity, }) +// Set overlay heat intensity +export const changeLayerIntensity = (id, heatIntensity) => ({ + type: types.LAYER_CHANGE_INTENSITY, + id, + heatIntensity, +}) + +// Set overlay heat radius +export const changeLayerRadius = (id, heatRadius) => ({ + type: types.LAYER_CHANGE_RADIUS, + id, + heatRadius, +}) + // Change ordering of overlays export const sortLayers = ({ oldIndex, newIndex }) => ({ type: types.LAYER_SORT, diff --git a/src/components/classification/ContinuousScale.jsx b/src/components/classification/ContinuousScale.jsx new file mode 100644 index 0000000000..8f5743f84b --- /dev/null +++ b/src/components/classification/ContinuousScale.jsx @@ -0,0 +1,38 @@ +import i18n from '@dhis2/d2-i18n' +import PropTypes from 'prop-types' +import React from 'react' +import { connect } from 'react-redux' +import { setColorScale } from '../../actions/layerEdit.js' +import { getColorPalette } from '../../util/colors.js' +import { ColorScaleSelect } from '../core/index.js' +import styles from './styles/Classification.module.css' + +const ContinuousScale = ({ colorScale, setColorScale }) => { + if (!colorScale) { + setColorScale(getColorPalette('RdYlBu_reverse', 9)) + } + return ( +
+
{i18n.t('Color scale')}
+ +
+ ) +} + +ContinuousScale.propTypes = { + setColorScale: PropTypes.func.isRequired, + colorScale: PropTypes.array, +} + +export default connect( + ({ layerEdit }) => ({ + colorScale: layerEdit.colorScale, + }), + { setColorScale } +)(ContinuousScale) diff --git a/src/components/classification/styles/Classification.module.css b/src/components/classification/styles/Classification.module.css index 610ab79c44..46c73a2800 100644 --- a/src/components/classification/styles/Classification.module.css +++ b/src/components/classification/styles/Classification.module.css @@ -1,3 +1,12 @@ +.title { + color: var(--colors-grey900); + padding-top: 8px; + padding-left: 2px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + .select { width: 100%; } diff --git a/src/components/core/ColorScaleSelect.jsx b/src/components/core/ColorScaleSelect.jsx index 037d4f5cfd..fe2c04327a 100644 --- a/src/components/core/ColorScaleSelect.jsx +++ b/src/components/core/ColorScaleSelect.jsx @@ -4,18 +4,19 @@ import PropTypes from 'prop-types' import React, { Fragment, useState, useRef } from 'react' import { colorScales, + heatScales, getColorScale, getColorPalette, } from '../../util/colors.js' import ColorScale from './ColorScale.jsx' import styles from './styles/ColorScaleSelect.module.css' -const ColorScaleSelect = ({ palette, width, onChange, className }) => { +const ColorScaleSelect = ({ palette, width, heat, onChange, className }) => { const [isOpen, setIsOpen] = useState(false) const anchorRef = useRef() const bins = palette.length - const scale = getColorScale(palette) + const scale = getColorScale(palette, heat) const onColorScaleSelect = (scale) => { onChange(getColorPalette(scale, bins)) @@ -43,15 +44,17 @@ const ColorScaleSelect = ({ palette, width, onChange, className }) => { className={styles.popover} style={{ width: width + 24 || 260 }} > - {colorScales.map((scale, index) => ( - - ))} + {(heat ? heatScales : colorScales).map( + (scale, index) => ( + + ) + )} )} @@ -63,6 +66,7 @@ ColorScaleSelect.propTypes = { palette: PropTypes.array.isRequired, onChange: PropTypes.func.isRequired, className: PropTypes.string, + heat: PropTypes.bool, width: PropTypes.number, } diff --git a/src/components/edit/event/EventDialog.jsx b/src/components/edit/event/EventDialog.jsx index 5302dd24e5..3226ff24bf 100644 --- a/src/components/edit/event/EventDialog.jsx +++ b/src/components/edit/event/EventDialog.jsx @@ -11,6 +11,7 @@ import { setEventStatus, setEventCoordinateField, setEventClustering, + setEventHeatmap, setEventPointColor, setEventPointRadius, // setFallbackCoordinateField, @@ -41,6 +42,7 @@ import { splitFilterColumns, } from '../../../util/analytics.js' import { cssColor } from '../../../util/colors.js' +import ContinuousScale from '../../classification/ContinuousScale.jsx' import { isValidIsolatedClass } from '../../classification/IsolatedClass.jsx' import { Tab, @@ -77,6 +79,7 @@ const EventDialog = ({ eventClustering, eventCoordinateField, eventCoordinateFieldType, + eventHeatmap, eventPointColor, eventPointRadius, eventStatus, @@ -448,9 +451,10 @@ const EventDialog = ({ id="cluster" img="images/cluster.png" title={i18n.t('Group events')} - onClick={() => + onClick={() => { dispatch(setEventClustering(true)) - } + dispatch(setEventHeatmap(false)) + }} isSelected={eventClustering} className={styles.flexInnerColumn} /> @@ -458,10 +462,24 @@ const EventDialog = ({ id="nocluster" img="images/nocluster.png" title={i18n.t('View all events')} - onClick={() => + onClick={() => { dispatch(setEventClustering(false)) + dispatch(setEventHeatmap(false)) + }} + isSelected={ + !eventClustering && !eventHeatmap } - isSelected={!eventClustering} + className={styles.flexInnerColumn} + /> + { + dispatch(setEventClustering(false)) + dispatch(setEventHeatmap(true)) + }} + isSelected={eventHeatmap} className={styles.flexInnerColumn} /> @@ -494,7 +512,7 @@ const EventDialog = ({ @@ -526,7 +544,9 @@ const EventDialog = ({ />
- {program ? ( + {eventHeatmap ? ( + + ) : program ? ( @@ -559,6 +579,7 @@ EventDialog.propTypes = { eventClustering: PropTypes.bool, eventCoordinateField: PropTypes.string, eventCoordinateFieldType: PropTypes.string, + eventHeatmap: PropTypes.bool, eventPointColor: PropTypes.string, eventPointRadius: PropTypes.number, eventStatus: PropTypes.string, diff --git a/src/components/layers/overlays/HeatSlider.jsx b/src/components/layers/overlays/HeatSlider.jsx new file mode 100644 index 0000000000..6074df4cb0 --- /dev/null +++ b/src/components/layers/overlays/HeatSlider.jsx @@ -0,0 +1,45 @@ +import PropTypes from 'prop-types' +import React, { useCallback } from 'react' +import styles from './styles/HeatSlider.module.css' + +const HeatSlider = ({ heat, disabled, onChange }) => { + const lowerFill = `var(--colors-grey${disabled ? 400 : 600})` + const upperFill = `var(--colors-grey${disabled ? 300 : 400})` + + const onSliderChange = useCallback( + (evt) => onChange(Number(evt.target.value)), + [onChange] + ) + + return ( +
+ +
+ ) +} + +HeatSlider.propTypes = { + heat: PropTypes.number.isRequired, + onChange: PropTypes.func.isRequired, + disabled: PropTypes.bool, +} + +export default HeatSlider diff --git a/src/components/layers/overlays/OverlayCard.jsx b/src/components/layers/overlays/OverlayCard.jsx index a490f28cda..1f85d270d5 100644 --- a/src/components/layers/overlays/OverlayCard.jsx +++ b/src/components/layers/overlays/OverlayCard.jsx @@ -11,6 +11,8 @@ import { removeLayer, duplicateLayer, changeLayerOpacity, + changeLayerIntensity, + changeLayerRadius, toggleLayerExpand, toggleLayerVisibility, } from '../../../actions/layers.js' @@ -34,6 +36,7 @@ import Legend from '../../legend/Legend.jsx' import LegendAlert from '../../legend/LegendAlert.jsx' import DataDownloadDialog from '../download/DataDownloadDialog.jsx' import LayerCard from '../LayerCard.jsx' +import HeatSlider from './HeatSlider.jsx' import styles from './styles/OverlayCard.module.css' const OverlayCard = ({ @@ -42,6 +45,8 @@ const OverlayCard = ({ removeLayer, duplicateLayer, changeLayerOpacity, + changeLayerIntensity, + changeLayerRadius, toggleLayerExpand, toggleLayerVisibility, toggleDataTable, @@ -57,6 +62,8 @@ const OverlayCard = ({ legend, isExpanded = true, opacity, + heatIntensity = 0.5, + heatRadius = 0.5, isVisible, layer: layerType, isLoaded, @@ -85,6 +92,58 @@ const OverlayCard = ({ legend && (
+ {layer.eventHeatmap && ( +
+ + + Intensity: + + + changeLayerIntensity(id, newIntensity) + } + disabled={false} + /> + + + + Radius: + + + changeLayerRadius(id, newRadius) + } + disabled={false} + /> + +
+ )}
) ) @@ -155,7 +214,9 @@ const OverlayCard = ({ } OverlayCard.propTypes = { + changeLayerIntensity: PropTypes.func.isRequired, changeLayerOpacity: PropTypes.func.isRequired, + changeLayerRadius: PropTypes.func.isRequired, duplicateLayer: PropTypes.func.isRequired, editLayer: PropTypes.func.isRequired, layer: PropTypes.object.isRequired, @@ -170,6 +231,8 @@ export default connect(null, { removeLayer, duplicateLayer, changeLayerOpacity, + changeLayerIntensity, + changeLayerRadius, toggleLayerExpand, toggleLayerVisibility, toggleDataTable, diff --git a/src/components/layers/overlays/styles/HeatSlider.module.css b/src/components/layers/overlays/styles/HeatSlider.module.css new file mode 100644 index 0000000000..3849198964 --- /dev/null +++ b/src/components/layers/overlays/styles/HeatSlider.module.css @@ -0,0 +1,56 @@ +.container { + width: 100px; + height: 28px; +} + +.slider { + width: 100%; + -webkit-appearance: none; + appearance: none; + height: 2px; + transition: background 450ms ease-in; + outline: none; + border-radius: 2px; + cursor: pointer; +} + +.slider:disabled { + cursor: not-allowed; +} + +.slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 12px; + height: 12px; + background: var(--colors-grey700); + border-radius: 50%; +} + +.slider::-moz-range-thumb { + width: 12px; + height: 12px; + background: var(--colors-grey700); + border: 0; + border-radius: 50%; +} + +.slider::-webkit-slider-thumb:hover { + background: var(--colors-grey900); +} + +.slider::-moz-range-thumb:hover { + background: var(--colors-grey900); +} + +.slider::-moz-focus-outer { + border: 0; +} + +.slider:disabled::-webkit-slider-thumb { + background: var(--colors-grey400); +} + +.slider:disabled::-moz-range-thumb { + background: var(--colors-grey400); +} diff --git a/src/components/map/layers/EventLayer.jsx b/src/components/map/layers/EventLayer.jsx index b132e09e74..daeb1bd8ca 100644 --- a/src/components/map/layers/EventLayer.jsx +++ b/src/components/map/layers/EventLayer.jsx @@ -38,10 +38,13 @@ class EventLayer extends Layer { id, index, opacity, + heatIntensity, + heatRadius, isVisible, bounds, data, engine, + eventHeatmap, eventClustering, eventCoordinateField, eventPointColor, @@ -62,6 +65,8 @@ class EventLayer extends Layer { labelFontSize, labelFontWeight, labelFontStyle, + classes, + colorScale, } = this.props const analyticsEngine = Analytics.getAnalytics(engine) @@ -122,6 +127,8 @@ class EventLayer extends Layer { id, index, opacity, + heatIntensity, + heatRadius, isVisible, data: labeledData, fillColor, @@ -155,6 +162,9 @@ class EventLayer extends Layer { engine, analyticsEngine, geometryCentroid, + eventHeatmap, + classes, + colorScale, }) if (program && programStage) { @@ -194,6 +204,9 @@ class EventLayer extends Layer { engine, analyticsEngine, geometryCentroid, + eventHeatmap, + classes, + colorScale, } ) { let eventRequest @@ -238,6 +251,23 @@ class EventLayer extends Layer { config.type = 'clientCluster' } } + } else if (eventHeatmap) { + config.type = 'heat' + config.heatWeight = 1 + + const step = 1 / classes + const colorScaleReady = colorScale.flatMap((heatColor, i) => [ + (i + 1) * step, + heatColor, + ]) + config.heatColor = [ + 'interpolate', + ['linear'], + ['heatmap-density'], + 0, + 'rgba(33,102,172,0)', + ...colorScaleReady, + ] } else if (areaRadius) { config.buffer = areaRadius config.bufferStyle = { diff --git a/src/components/map/layers/Layer.js b/src/components/map/layers/Layer.js index c8bff13635..f274536fac 100644 --- a/src/components/map/layers/Layer.js +++ b/src/components/map/layers/Layer.js @@ -21,6 +21,8 @@ class Layer extends PureComponent { editCounter: PropTypes.number, externalPeriod: PropTypes.object, // eslint-disable-line react/no-unused-prop-types feature: PropTypes.object, + heatIntensity: PropTypes.number, + heatRadius: PropTypes.number, index: PropTypes.number, isVisible: PropTypes.bool, opacity: PropTypes.number, @@ -29,6 +31,8 @@ class Layer extends PureComponent { static defaultProps = { opacity: 1, + heatIntensity: 0.5, + heatRadius: 0.5, isVisible: true, } @@ -46,6 +50,8 @@ class Layer extends PureComponent { data, index, opacity, + heatIntensity, + heatRadius, isVisible, editCounter, dataFilters, @@ -79,6 +85,14 @@ class Layer extends PureComponent { this.setLayerOpacity() } + if (heatIntensity !== prevProps.heatIntensity) { + this.setLayerIntensity() + } + + if (heatRadius !== prevProps.heatRadius) { + this.setLayerRadius() + } + if (isVisible !== prevProps.isVisible) { this.setLayerVisibility() } @@ -94,7 +108,15 @@ class Layer extends PureComponent { // Create new layer from config object (override in subclasses) async createLayer() { - const { id, index = 0, config, opacity, isVisible } = this.props + const { + id, + index = 0, + config, + opacity, + heatIntensity, + heatRadius, + isVisible, + } = this.props const { map } = this.context this.layer = map.createLayer({ @@ -102,6 +124,8 @@ class Layer extends PureComponent { id, index, opacity, + heatIntensity, + heatRadius, isVisible, }) @@ -131,6 +155,14 @@ class Layer extends PureComponent { this.layer.setOpacity(this.props.opacity) } + setLayerIntensity() { + this.layer.setIntensity(this.props.heatIntensity) + } + + setLayerRadius() { + this.layer.setRadius(this.props.heatRadius) + } + setLayerOrder() { if (this.layer) { this.layer.setIndex(this.props.index) diff --git a/src/constants/actionTypes.js b/src/constants/actionTypes.js index 04bc55e885..918707340e 100644 --- a/src/constants/actionTypes.js +++ b/src/constants/actionTypes.js @@ -24,6 +24,8 @@ export const LAYER_ADD = 'LAYER_ADD' export const LAYER_REMOVE = 'LAYER_REMOVE' export const LAYER_DUPLICATE = 'LAYER_DUPLICATE' export const LAYER_CHANGE_OPACITY = 'LAYER_CHANGE_OPACITY' +export const LAYER_CHANGE_INTENSITY = 'LAYER_CHANGE_INTENSITY' +export const LAYER_CHANGE_RADIUS = 'LAYER_CHANGE_RADIUS' export const LAYER_EDIT = 'LAYER_EDIT' export const LAYER_EDIT_SUCCESS = 'LAYER_EDIT_SUCCESS' export const LAYER_CANCEL = 'LAYER_CANCEL' @@ -92,6 +94,7 @@ 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 = 'LAYER_EDIT_EVENT_COORDINATE_FIELD_SET' +export const LAYER_EDIT_EVENT_HEATMAP_SET = 'LAYER_EDIT_EVENT_HEATMAP_SET' export const LAYER_EDIT_EVENT_CLUSTERING_SET = 'LAYER_EDIT_EVENT_CLUSTERING_SET' export const LAYER_EDIT_EVENT_POINT_RADIUS_SET = 'LAYER_EDIT_EVENT_POINT_RADIUS_SET' diff --git a/src/constants/colorbrewer.js b/src/constants/colorbrewer.js index 687cf637dd..bc747e6332 100644 --- a/src/constants/colorbrewer.js +++ b/src/constants/colorbrewer.js @@ -1520,6 +1520,915 @@ const colorbrewer = { '#ffed6f', ], }, + YlOrBr_reverse: { + 3: ['#d95f0e', '#fec44f', '#fff7bc'], + 4: ['#cc4c02', '#fe9929', '#fed98e', '#ffffd4'], + 5: ['#993404', '#d95f0e', '#fe9929', '#fed98e', '#ffffd4'], + 6: ['#993404', '#d95f0e', '#fe9929', '#fec44f', '#fee391', '#ffffd4'], + 7: [ + '#8c2d04', + '#cc4c02', + '#ec7014', + '#fe9929', + '#fec44f', + '#fee391', + '#ffffd4', + ], + 8: [ + '#8c2d04', + '#cc4c02', + '#ec7014', + '#fe9929', + '#fec44f', + '#fee391', + '#fff7bc', + '#ffffe5', + ], + 9: [ + '#662506', + '#993404', + '#cc4c02', + '#ec7014', + '#fe9929', + '#fec44f', + '#fee391', + '#fff7bc', + '#ffffe5', + ], + }, + Reds_reverse: { + 3: ['#de2d26', '#fc9272', '#fee0d2'], + 4: ['#cb181d', '#fb6a4a', '#fcae91', '#fee5d9'], + 5: ['#a50f15', '#de2d26', '#fb6a4a', '#fcae91', '#fee5d9'], + 6: ['#a50f15', '#de2d26', '#fb6a4a', '#fc9272', '#fcbba1', '#fee5d9'], + 7: [ + '#99000d', + '#cb181d', + '#ef3b2c', + '#fb6a4a', + '#fc9272', + '#fcbba1', + '#fee5d9', + ], + 8: [ + '#99000d', + '#cb181d', + '#ef3b2c', + '#fb6a4a', + '#fc9272', + '#fcbba1', + '#fee0d2', + '#fff5f0', + ], + 9: [ + '#67000d', + '#a50f15', + '#cb181d', + '#ef3b2c', + '#fb6a4a', + '#fc9272', + '#fcbba1', + '#fee0d2', + '#fff5f0', + ], + }, + YlGn_reverse: { + 3: ['#31a354', '#addd8e', '#f7fcb9'], + 4: ['#238443', '#78c679', '#c2e699', '#ffffcc'], + 5: ['#006837', '#31a354', '#78c679', '#c2e699', '#ffffcc'], + 6: ['#006837', '#31a354', '#78c679', '#addd8e', '#d9f0a3', '#ffffcc'], + 7: [ + '#005a32', + '#238443', + '#41ab5d', + '#78c679', + '#addd8e', + '#d9f0a3', + '#ffffcc', + ], + 8: [ + '#005a32', + '#238443', + '#41ab5d', + '#78c679', + '#addd8e', + '#d9f0a3', + '#f7fcb9', + '#ffffe5', + ], + 9: [ + '#004529', + '#006837', + '#238443', + '#41ab5d', + '#78c679', + '#addd8e', + '#d9f0a3', + '#f7fcb9', + '#ffffe5', + ], + }, + Purples_reverse: { + 3: ['#756bb1', '#bcbddc', '#efedf5'], + 4: ['#6a51a3', '#9e9ac8', '#cbc9e2', '#f2f0f7'], + 5: ['#54278f', '#756bb1', '#9e9ac8', '#cbc9e2', '#f2f0f7'], + 6: ['#54278f', '#756bb1', '#9e9ac8', '#bcbddc', '#dadaeb', '#f2f0f7'], + 7: [ + '#4a1486', + '#6a51a3', + '#807dba', + '#9e9ac8', + '#bcbddc', + '#dadaeb', + '#f2f0f7' + ], + 8: [ + '#4a1486', + '#6a51a3', + '#807dba', + '#9e9ac8', + '#bcbddc', + '#dadaeb', + '#efedf5', + '#fcfbfd' + ], + 9: [ + '#3f007d', + '#54278f', + '#6a51a3', + '#807dba', + '#9e9ac8', + '#bcbddc', + '#dadaeb', + '#efedf5', + '#fcfbfd' + ] + }, + Greens_reverse: { + 3: ['#31a354', '#a1d99b', '#e5f5e0'], + 4: ['#238b45', '#74c476', '#bae4b3', '#edf8e9'], + 5: ['#006d2c', '#31a354', '#74c476', '#bae4b3', '#edf8e9'], + 6: ['#006d2c', '#31a354', '#74c476', '#a1d99b', '#c7e9c0', '#edf8e9'], + 7: [ + '#005a32', + '#238b45', + '#41ab5d', + '#74c476', + '#a1d99b', + '#c7e9c0', + '#edf8e9', + ], + 8: [ + '#005a32', + '#238b45', + '#41ab5d', + '#74c476', + '#a1d99b', + '#c7e9c0', + '#e5f5e0', + '#f7fcf5', + ], + 9: [ + '#00441b', + '#006d2c', + '#238b45', + '#41ab5d', + '#74c476', + '#a1d99b', + '#c7e9c0', + '#e5f5e0', + '#f7fcf5', + ], + }, + Blues_reverse: { + 3: ['#3182bd', '#9ecae1', '#deebf7'], + 4: ['#2171b5', '#6baed6', '#bdd7e7', '#eff3ff'], + 5: ['#08519c', '#3182bd', '#6baed6', '#bdd7e7', '#eff3ff'], + 6: ['#08519c', '#3182bd', '#6baed6', '#9ecae1', '#c6dbef', '#eff3ff'], + 7: [ + '#084594', + '#2171b5', + '#4292c6', + '#6baed6', + '#9ecae1', + '#c6dbef', + '#eff3ff', + ], + 8: [ + '#084594', + '#2171b5', + '#4292c6', + '#6baed6', + '#9ecae1', + '#c6dbef', + '#deebf7', + '#f7fbff', + ], + 9: [ + '#08306b', + '#08519c', + '#2171b5', + '#4292c6', + '#6baed6', + '#9ecae1', + '#c6dbef', + '#deebf7', + '#f7fbff', + ], + }, + BuPu_reverse: { + 3: ['#8856a7', '#9ebcda', '#e0ecf4'], + 4: ['#88419d', '#8c96c6', '#b3cde3', '#edf8fb'], + 5: ['#810f7c', '#8856a7', '#8c96c6', '#b3cde3', '#edf8fb'], + 6: ['#810f7c', '#8856a7', '#8c96c6', '#9ebcda', '#bfd3e6', '#edf8fb'], + 7: [ + '#6e016b', + '#88419d', + '#8c6bb1', + '#8c96c6', + '#9ebcda', + '#bfd3e6', + '#edf8fb', + ], + 8: [ + '#6e016b', + '#88419d', + '#8c6bb1', + '#8c96c6', + '#9ebcda', + '#bfd3e6', + '#e0ecf4', + '#f7fcfd', + ], + 9: [ + '#4d004b', + '#810f7c', + '#88419d', + '#8c6bb1', + '#8c96c6', + '#9ebcda', + '#bfd3e6', + '#e0ecf4', + '#f7fcfd', + ], + }, + RdPu_reverse: { + 3: ['#c51b8a', '#fa9fb5', '#fde0dd'], + 4: ['#ae017e', '#f768a1', '#fbb4b9', '#feebe2'], + 5: ['#7a0177', '#c51b8a', '#f768a1', '#fbb4b9', '#feebe2'], + 6: ['#7a0177', '#c51b8a', '#f768a1', '#fa9fb5', '#fcc5c0', '#feebe2'], + 7: [ + '#7a0177', + '#ae017e', + '#dd3497', + '#f768a1', + '#fa9fb5', + '#fcc5c0', + '#feebe2', + ], + 8: [ + '#7a0177', + '#ae017e', + '#dd3497', + '#f768a1', + '#fa9fb5', + '#fcc5c0', + '#fde0dd', + '#fff7f3', + ], + 9: [ + '#49006a', + '#7a0177', + '#ae017e', + '#dd3497', + '#f768a1', + '#fa9fb5', + '#fcc5c0', + '#fde0dd', + '#fff7f3', + ], + }, + PuRd_reverse: { + 3: ['#dd1c77', '#c994c7', '#e7e1ef'], + 4: ['#ce1256', '#df65b0', '#d7b5d8', '#f1eef6'], + 5: ['#980043', '#dd1c77', '#df65b0', '#d7b5d8', '#f1eef6'], + 6: ['#980043', '#dd1c77', '#df65b0', '#c994c7', '#d4b9da', '#f1eef6'], + 7: [ + '#91003f', + '#ce1256', + '#e7298a', + '#df65b0', + '#c994c7', + '#d4b9da', + '#f1eef6', + ], + 8: [ + '#91003f', + '#ce1256', + '#e7298a', + '#df65b0', + '#c994c7', + '#d4b9da', + '#e7e1ef', + '#f7f4f9', + ], + 9: [ + '#67001f', + '#980043', + '#ce1256', + '#e7298a', + '#df65b0', + '#c994c7', + '#d4b9da', + '#e7e1ef', + '#f7f4f9', + ], + }, + Greys_reverse: { + 3: ['#636363', '#bdbdbd', '#f0f0f0'], + 4: ['#525252', '#969696', '#cccccc', '#f7f7f7'], + 5: ['#252525', '#636363', '#969696', '#cccccc', '#f7f7f7'], + 6: ['#252525', '#636363', '#969696', '#bdbdbd', '#d9d9d9', '#f7f7f7'], + 7: [ + '#252525', + '#525252', + '#737373', + '#969696', + '#bdbdbd', + '#d9d9d9', + '#f7f7f7', + ], + 8: [ + '#252525', + '#525252', + '#737373', + '#969696', + '#bdbdbd', + '#d9d9d9', + '#f0f0f0', + '#ffffff', + ], + 9: [ + '#000000', + '#252525', + '#525252', + '#737373', + '#969696', + '#bdbdbd', + '#d9d9d9', + '#f0f0f0', + '#ffffff', + ], + }, + PuOr_reverse: { + 3: ['#998ec3', '#f7f7f7', '#f1a340'], + 4: ['#5e3c99', '#b2abd2', '#fdb863', '#e66101'], + 5: ['#5e3c99', '#b2abd2', '#f7f7f7', '#fdb863', '#e66101'], + 6: ['#542788', '#998ec3', '#d8daeb', '#fee0b6', '#f1a340', '#b35806'], + 7: [ + '#542788', + '#998ec3', + '#d8daeb', + '#f7f7f7', + '#fee0b6', + '#f1a340', + '#b35806', + ], + 8: [ + '#542788', + '#8073ac', + '#b2abd2', + '#d8daeb', + '#fee0b6', + '#fdb863', + '#e08214', + '#b35806', + ], + 9: [ + '#542788', + '#8073ac', + '#b2abd2', + '#d8daeb', + '#f7f7f7', + '#fee0b6', + '#fdb863', + '#e08214', + '#b35806', + ], + 10: [ + '#2d004b', + '#542788', + '#8073ac', + '#b2abd2', + '#d8daeb', + '#fee0b6', + '#fdb863', + '#e08214', + '#b35806', + '#7f3b08', + ], + 11: [ + '#2d004b', + '#542788', + '#8073ac', + '#b2abd2', + '#d8daeb', + '#f7f7f7', + '#fee0b6', + '#fdb863', + '#e08214', + '#b35806', + '#7f3b08', + ], + }, + BrBG_reverse: { + 3: ['#5ab4ac', '#f5f5f5', '#d8b365'], + 4: ['#018571', '#80cdc1', '#dfc27d', '#a6611a'], + 5: ['#018571', '#80cdc1', '#f5f5f5', '#dfc27d', '#a6611a'], + 6: ['#01665e', '#5ab4ac', '#c7eae5', '#f6e8c3', '#d8b365', '#8c510a'], + 7: [ + '#01665e', + '#5ab4ac', + '#c7eae5', + '#f5f5f5', + '#f6e8c3', + '#d8b365', + '#8c510a', + ], + 8: [ + '#01665e', + '#35978f', + '#80cdc1', + '#c7eae5', + '#f6e8c3', + '#dfc27d', + '#bf812d', + '#8c510a', + ], + 9: [ + '#01665e', + '#35978f', + '#80cdc1', + '#c7eae5', + '#f5f5f5', + '#f6e8c3', + '#dfc27d', + '#bf812d', + '#8c510a', + ], + 10: [ + '#003c30', + '#01665e', + '#35978f', + '#80cdc1', + '#c7eae5', + '#f6e8c3', + '#dfc27d', + '#bf812d', + '#8c510a', + '#543005', + ], + 11: [ + '#003c30', + '#01665e', + '#35978f', + '#80cdc1', + '#c7eae5', + '#f5f5f5', + '#f6e8c3', + '#dfc27d', + '#bf812d', + '#8c510a', + '#543005', + ], + }, + PRGn_reverse: { + 3: ['#7fbf7b', '#f7f7f7', '#af8dc3'], + 4: ['#008837', '#a6dba0', '#c2a5cf', '#7b3294'], + 5: ['#008837', '#a6dba0', '#f7f7f7', '#c2a5cf', '#7b3294'], + 6: ['#1b7837', '#7fbf7b', '#d9f0d3', '#e7d4e8', '#af8dc3', '#762a83'], + 7: [ + '#1b7837', + '#7fbf7b', + '#d9f0d3', + '#f7f7f7', + '#e7d4e8', + '#af8dc3', + '#762a83', + ], + 8: [ + '#1b7837', + '#5aae61', + '#a6dba0', + '#d9f0d3', + '#e7d4e8', + '#c2a5cf', + '#9970ab', + '#762a83', + ], + 9: [ + '#1b7837', + '#5aae61', + '#a6dba0', + '#d9f0d3', + '#f7f7f7', + '#e7d4e8', + '#c2a5cf', + '#9970ab', + '#762a83', + ], + 10: [ + '#00441b', + '#1b7837', + '#5aae61', + '#a6dba0', + '#d9f0d3', + '#e7d4e8', + '#c2a5cf', + '#9970ab', + '#762a83', + '#40004b', + ], + 11: [ + '#00441b', + '#1b7837', + '#5aae61', + '#a6dba0', + '#d9f0d3', + '#f7f7f7', + '#e7d4e8', + '#c2a5cf', + '#9970ab', + '#762a83', + '#40004b', + ], + }, + PiYG_reverse: { + 3: ['#a1d76a', '#f7f7f7', '#e9a3c9'], + 4: ['#4dac26', '#b8e186', '#f1b6da', '#d01c8b'], + 5: ['#4dac26', '#b8e186', '#f7f7f7', '#f1b6da', '#d01c8b'], + 6: ['#4d9221', '#a1d76a', '#e6f5d0', '#fde0ef', '#e9a3c9', '#c51b7d'], + 7: [ + '#4d9221', + '#a1d76a', + '#e6f5d0', + '#f7f7f7', + '#fde0ef', + '#e9a3c9', + '#c51b7d', + ], + 8: [ + '#4d9221', + '#7fbc41', + '#b8e186', + '#e6f5d0', + '#fde0ef', + '#f1b6da', + '#de77ae', + '#c51b7d', + ], + 9: [ + '#4d9221', + '#7fbc41', + '#b8e186', + '#e6f5d0', + '#f7f7f7', + '#fde0ef', + '#f1b6da', + '#de77ae', + '#c51b7d', + ], + 10: [ + '#276419', + '#4d9221', + '#7fbc41', + '#b8e186', + '#e6f5d0', + '#fde0ef', + '#f1b6da', + '#de77ae', + '#c51b7d', + '#8e0152', + ], + 11: [ + '#276419', + '#4d9221', + '#7fbc41', + '#b8e186', + '#e6f5d0', + '#f7f7f7', + '#fde0ef', + '#f1b6da', + '#de77ae', + '#c51b7d', + '#8e0152', + ], + }, + RdBu_reverse: { + 3: ['#67a9cf', '#f7f7f7', '#ef8a62'], + 4: ['#0571b0', '#92c5de', '#f4a582', '#ca0020'], + 5: ['#0571b0', '#92c5de', '#f7f7f7', '#f4a582', '#ca0020'], + 6: ['#2166ac', '#67a9cf', '#d1e5f0', '#fddbc7', '#ef8a62', '#b2182b'], + 7: [ + '#2166ac', + '#67a9cf', + '#d1e5f0', + '#f7f7f7', + '#fddbc7', + '#ef8a62', + '#b2182b', + ], + 8: [ + '#2166ac', + '#4393c3', + '#92c5de', + '#d1e5f0', + '#fddbc7', + '#f4a582', + '#d6604d', + '#b2182b', + ], + 9: [ + '#2166ac', + '#4393c3', + '#92c5de', + '#d1e5f0', + '#f7f7f7', + '#fddbc7', + '#f4a582', + '#d6604d', + '#b2182b', + ], + 10: [ + '#053061', + '#2166ac', + '#4393c3', + '#92c5de', + '#d1e5f0', + '#fddbc7', + '#f4a582', + '#d6604d', + '#b2182b', + '#67001f', + ], + 11: [ + '#053061', + '#2166ac', + '#4393c3', + '#92c5de', + '#d1e5f0', + '#f7f7f7', + '#fddbc7', + '#f4a582', + '#d6604d', + '#b2182b', + '#67001f', + ], + }, + RdGy_reverse: { + 3: ['#999999', '#ffffff', '#ef8a62'], + 4: ['#404040', '#bababa', '#f4a582', '#ca0020'], + 5: ['#404040', '#bababa', '#ffffff', '#f4a582', '#ca0020'], + 6: ['#4d4d4d', '#999999', '#e0e0e0', '#fddbc7', '#ef8a62', '#b2182b'], + 7: [ + '#4d4d4d', + '#999999', + '#e0e0e0', + '#ffffff', + '#fddbc7', + '#ef8a62', + '#b2182b', + ], + 8: [ + '#4d4d4d', + '#878787', + '#bababa', + '#e0e0e0', + '#fddbc7', + '#f4a582', + '#d6604d', + '#b2182b', + ], + 9: [ + '#4d4d4d', + '#878787', + '#bababa', + '#e0e0e0', + '#ffffff', + '#fddbc7', + '#f4a582', + '#d6604d', + '#b2182b', + ], + 10: [ + '#1a1a1a', + '#4d4d4d', + '#878787', + '#bababa', + '#e0e0e0', + '#fddbc7', + '#f4a582', + '#d6604d', + '#b2182b', + '#67001f', + ], + 11: [ + '#1a1a1a', + '#4d4d4d', + '#878787', + '#bababa', + '#e0e0e0', + '#ffffff', + '#fddbc7', + '#f4a582', + '#d6604d', + '#b2182b', + '#67001f', + ], + }, + RdYlBu_reverse: { + 3: ['#91bfdb', '#ffffbf', '#fc8d59'], + 4: ['#2c7bb6', '#abd9e9', '#fdae61', '#d7191c'], + 5: ['#2c7bb6', '#abd9e9', '#ffffbf', '#fdae61', '#d7191c'], + 6: ['#4575b4', '#91bfdb', '#e0f3f8', '#fee090', '#fc8d59', '#d73027'], + 7: [ + '#4575b4', + '#91bfdb', + '#e0f3f8', + '#ffffbf', + '#fee090', + '#fc8d59', + '#d73027', + ], + 8: [ + '#4575b4', + '#74add1', + '#abd9e9', + '#e0f3f8', + '#fee090', + '#fdae61', + '#f46d43', + '#d73027', + ], + 9: [ + '#4575b4', + '#74add1', + '#abd9e9', + '#e0f3f8', + '#ffffbf', + '#fee090', + '#fdae61', + '#f46d43', + '#d73027', + ], + 10: [ + '#313695', + '#4575b4', + '#74add1', + '#abd9e9', + '#e0f3f8', + '#fee090', + '#fdae61', + '#f46d43', + '#d73027', + '#a50026', + ], + 11: [ + '#313695', + '#4575b4', + '#74add1', + '#abd9e9', + '#e0f3f8', + '#ffffbf', + '#fee090', + '#fdae61', + '#f46d43', + '#d73027', + '#a50026', + ], + }, + Spectral_reverse: { + 3: ['#99d594', '#ffffbf', '#fc8d59'], + 4: ['#2b83ba', '#abdda4', '#fdae61', '#d7191c'], + 5: ['#2b83ba', '#abdda4', '#ffffbf', '#fdae61', '#d7191c'], + 6: ['#3288bd', '#99d594', '#e6f598', '#fee08b', '#fc8d59', '#d53e4f'], + 7: [ + '#3288bd', + '#99d594', + '#e6f598', + '#ffffbf', + '#fee08b', + '#fc8d59', + '#d53e4f', + ], + 8: [ + '#3288bd', + '#66c2a5', + '#abdda4', + '#e6f598', + '#fee08b', + '#fdae61', + '#f46d43', + '#d53e4f', + ], + 9: [ + '#3288bd', + '#66c2a5', + '#abdda4', + '#e6f598', + '#ffffbf', + '#fee08b', + '#fdae61', + '#f46d43', + '#d53e4f', + ], + 10: [ + '#5e4fa2', + '#3288bd', + '#66c2a5', + '#abdda4', + '#e6f598', + '#fee08b', + '#fdae61', + '#f46d43', + '#d53e4f', + '#9e0142', + ], + 11: [ + '#5e4fa2', + '#3288bd', + '#66c2a5', + '#abdda4', + '#e6f598', + '#ffffbf', + '#fee08b', + '#fdae61', + '#f46d43', + '#d53e4f', + '#9e0142', + ], + }, + RdYlGn_reverse: { + 3: ['#91cf60', '#ffffbf', '#fc8d59'], + 4: ['#1a9641', '#a6d96a', '#fdae61', '#d7191c'], + 5: ['#1a9641', '#a6d96a', '#ffffbf', '#fdae61', '#d7191c'], + 6: ['#1a9850', '#91cf60', '#d9ef8b', '#fee08b', '#fc8d59', '#d73027'], + 7: [ + '#1a9850', + '#91cf60', + '#d9ef8b', + '#ffffbf', + '#fee08b', + '#fc8d59', + '#d73027', + ], + 8: [ + '#1a9850', + '#66bd63', + '#a6d96a', + '#d9ef8b', + '#fee08b', + '#fdae61', + '#f46d43', + '#d73027', + ], + 9: [ + '#1a9850', + '#66bd63', + '#a6d96a', + '#d9ef8b', + '#ffffbf', + '#fee08b', + '#fdae61', + '#f46d43', + '#d73027', + ], + 10: [ + '#006837', + '#1a9850', + '#66bd63', + '#a6d96a', + '#d9ef8b', + '#fee08b', + '#fdae61', + '#f46d43', + '#d73027', + '#a50026', + ], + 11: [ + '#006837', + '#1a9850', + '#66bd63', + '#a6d96a', + '#d9ef8b', + '#ffffbf', + '#fee08b', + '#fdae61', + '#f46d43', + '#d73027', + '#a50026', + ], + }, Vegetation: { 3: [ "#f1eda9", diff --git a/src/loaders/eventLoader.js b/src/loaders/eventLoader.js index da3b847c16..5b104e921a 100644 --- a/src/loaders/eventLoader.js +++ b/src/loaders/eventLoader.js @@ -161,6 +161,9 @@ const loadEventLayer = async ({ unclassifiedLegend: unclassifiedLegendFromConfig, noDataLegend: noDataLegendFromConfig, labelDataItem, + eventHeatmap: eventHeatmapFromConfig, + heatIntensity: heatIntensityFromConfig, + heatRadius: heatRadiusFromConfig, } = parseJsonConfig(config.config) if (countFeaturesWithoutCoordinates) { config.countFeaturesWithoutCoordinates = true @@ -202,6 +205,15 @@ const loadEventLayer = async ({ color: config.noDataColor, } } + if (eventHeatmapFromConfig !== undefined) { + config.eventHeatmap = eventHeatmapFromConfig + } + if (heatIntensityFromConfig !== undefined) { + config.heatIntensity = heatIntensityFromConfig + } + if (heatRadiusFromConfig !== undefined) { + config.heatRadius = heatRadiusFromConfig + } delete config.noDataColor delete config.config diff --git a/src/reducers/layerEdit.js b/src/reducers/layerEdit.js index 35ed32059d..63520bb8e7 100644 --- a/src/reducers/layerEdit.js +++ b/src/reducers/layerEdit.js @@ -365,6 +365,12 @@ const layerEdit = (state = null, action) => { return newState + case types.LAYER_EDIT_EVENT_HEATMAP_SET: + return { + ...state, + eventHeatmap: action.checked, + } + case types.LAYER_EDIT_EVENT_CLUSTERING_SET: return { ...state, diff --git a/src/reducers/map.js b/src/reducers/map.js index 7794693f07..4ddb2e8457 100644 --- a/src/reducers/map.js +++ b/src/reducers/map.js @@ -102,6 +102,26 @@ const layer = (state, action) => { opacity: action.opacity, } + case types.LAYER_CHANGE_INTENSITY: + if (state.id !== action.id) { + return state + } + + return { + ...state, + heatIntensity: action.heatIntensity, + } + + case types.LAYER_CHANGE_RADIUS: + if (state.id !== action.id) { + return state + } + + return { + ...state, + heatRadius: action.heatRadius, + } + case types.LAYER_LOADING_SET: if (state.id !== action.id) { return state @@ -295,6 +315,8 @@ const map = (state = defaultState, action) => { case types.LAYER_UPDATE: case types.LAYER_EDIT: case types.LAYER_CHANGE_OPACITY: + case types.LAYER_CHANGE_INTENSITY: + case types.LAYER_CHANGE_RADIUS: case types.LAYER_TOGGLE_VISIBILITY: case types.LAYER_TOGGLE_EXPAND: case types.DATA_FILTER_SET: diff --git a/src/util/colors.js b/src/util/colors.js index 810f5a4799..8e83f07061 100644 --- a/src/util/colors.js +++ b/src/util/colors.js @@ -53,13 +53,34 @@ export const colorScales = [ 'Set3', ] +export const heatScales = [ + 'YlOrBr', + 'Reds', + 'YlGn', + 'Greens', + 'Blues', + 'BuPu', + 'RdPu', + 'PuRd', + 'Greys', + 'PuOr_reverse', + 'BrBG_reverse', + 'PRGn_reverse', + 'PiYG_reverse', + 'RdBu_reverse', + 'RdGy_reverse', + 'RdYlBu_reverse', + 'Spectral_reverse', + 'RdYlGn_reverse', +] + // Returns a color brewer scale for a number of classes export const getColorPalette = (scale, classes) => colorbrewer[scale][classes] // Returns color scale name for a palette // join(',') is used to compare two arrays of colors -export const getColorScale = (palette) => - colorScales.find( +export const getColorScale = (palette, heat) => + (heat ? heatScales : colorScales).find( (name) => colorbrewer[name][palette.length].join(',') === palette.join(',') ) diff --git a/src/util/favorites.js b/src/util/favorites.js index 520c919873..05c17a6381 100644 --- a/src/util/favorites.js +++ b/src/util/favorites.js @@ -42,9 +42,12 @@ const validLayerProperties = [ 'endDate', 'eventCoordinateField', 'eventClustering', + 'eventHeatmap', 'eventPointColor', 'eventPointRadius', 'eventStatus', + 'heatIntensity', + 'heatRadius', 'featureStyle', // used by GEOJSON_URL_LAYER, stored in layer config 'filter', 'filters', @@ -180,6 +183,15 @@ const buildCommonLayerConfigData = (layer) => { if (layer.labelDataItem) { configData.labelDataItem = layer.labelDataItem } + if (layer.eventHeatmap !== undefined) { + configData.eventHeatmap = layer.eventHeatmap + } + if (layer.heatIntensity !== undefined) { + configData.heatIntensity = layer.heatIntensity + } + if (layer.heatRadius !== undefined) { + configData.heatRadius = layer.heatRadius + } return configData } @@ -194,6 +206,9 @@ const deleteCommonLayerConfigProps = (layer) => { delete layer.countFeaturesWithoutCoordinates delete layer.countEventsOutsideOrgUnits delete layer.labelDataItem + delete layer.eventHeatmap + delete layer.heatIntensity + delete layer.heatRadius } const buildEarthEngineLayerConfigData = (layer) => { diff --git a/src/util/getDefaultLayerTypes.js b/src/util/getDefaultLayerTypes.js index 2c7e85db82..6e5f530327 100644 --- a/src/util/getDefaultLayerTypes.js +++ b/src/util/getDefaultLayerTypes.js @@ -19,6 +19,8 @@ export const getDefaultLayerTypes = () => [ type: i18n.t('Events'), img: 'images/events.png', opacity: 0.8, + heatIntensity: 0.5, + heatRadius: 0.5, eventClustering: true, }, { diff --git a/yarn.lock b/yarn.lock index 90512c02d9..9ef55844c8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2371,10 +2371,9 @@ resolved "https://registry.yarnpkg.com/@dhis2/data-engine/-/data-engine-3.17.3.tgz#0347416e9919efbf4d9739c4141fa543f89669ad" integrity sha512-hLXt7LFrFitR7QgKfGQ3ComTLrY5IAdtERonhdo/SIrsRYWoeVaMiCOkUUzC48pEaeo1/BL5qwA7Tw7jZgROQw== -"@dhis2/maps-gl@^4.4.3": - version "4.4.3" - resolved "https://registry.yarnpkg.com/@dhis2/maps-gl/-/maps-gl-4.4.3.tgz#5584caee92c3fd164e3ab2cb67b78a428c7d1cb3" - integrity sha512-C2aDFNV3l3v5wbo0LJHr1lydQE6St04U11kCXtzU1WGqnEMEZKzuHyEoCfhIa/gas3e7HpNVfAbL1ZHI1OqDSw== +"@dhis2/maps-gl@git+https://github.com/d2-ci/maps-gl.git#a0c0a650843414776d42d94d38b476499823cffb": + version "4.2.8" + resolved "git+https://github.com/d2-ci/maps-gl.git#a0c0a650843414776d42d94d38b476499823cffb" dependencies: "@mapbox/sphericalmercator" "^1.2.0" "@turf/area" "^7.3.5"