Skip to content

Commit

Permalink
(WIP) Add Bubbles layer type
Browse files Browse the repository at this point in the history
fix #1399

This is a very first look. But it's not as simple as I thought,
because:
- as for a heatmap, we need a special representation (each feature
  should be a circle, not a normal icon nor a polygon)
- as for a choropleth, we want to dynamically take control over the
  rendering (we need all the features to be able to compute the bubbles
  sizes)
- as for a normal layer, we want all the features to react to interaction,
  specifically click to open a popup, but also tooltip, highlight (?), etc.

In this first implementation, I've created a new type of layer, that creates
a CircleMarker for each feature, but doing so we lose all the interactions.
I've tried to set a sort of proxy so that a click on the circle will fire
a click on the feature, but as this feature is actually not on the map, this
does not work naturally.
Also, at this point, the styling is only done once, so editing layer's style
will not be updated live in the map.

A few more thinking is needed, mainly to decide how to handle the circle: should
it be a "proxy layer" as now, or should we try to transform a feature on the fly
(make sure it is a marker of type circle, but as for now our circle are html, not
svg…), and in this case, should we magically handle polygons and polylines (taking
their center), or only markers (which could be easier, and possibly enough) ?
  • Loading branch information
yohanboniface committed Nov 28, 2023
1 parent b716530 commit f30ee0f
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 0 deletions.
1 change: 1 addition & 0 deletions umap/static/umap/js/umap.forms.js
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@ L.FormBuilder.LayerTypeChooser = L.FormBuilder.Select.extend({
['Cluster', L._('Clustered')],
['Heat', L._('Heatmap')],
['Choropleth', L._('Choropleth')],
['Bubble', L._('Bubbles')],
],
})

Expand Down
117 changes: 117 additions & 0 deletions umap/static/umap/js/umap.layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,123 @@ L.U.Layer.Cluster = L.MarkerClusterGroup.extend({
},
})

L.U.Layer.Bubble = L.FeatureGroup.extend({
_type: 'Bubble',
includes: [L.U.Layer],
canBrowse: true,

initialize: function (datalayer) {
this.datalayer = datalayer
if (!L.Util.isObject(this.datalayer.options.bubbles)) {
this.datalayer.options.bubbles = {
minSize: 1,
maxSize: 30
}
}
L.FeatureGroup.prototype.initialize.call(this)
this.datalayer.onceDataLoaded(() => {
this.redraw()
this.datalayer.on('datachanged', this.redraw, this)
})
},

redraw: function () {
this.computeMinMax()
if (this._map) {
this.eachLayer((circle) => {
circle.setRadius(this.computeRadius(circle.options.value))
this._map.addLayer(circle)
})
}
},

computeRadius: function (value) {
const minSize = this.datalayer.options.bubbles.minSize || 1,
maxSize = this.datalayer.options.bubbles.maxSize || 30
return minSize + ((maxSize - minSize) / (this.options.max - this.options.min)) * Math.sqrt(value)
},

_getValue: function (feature) {
const key = this.datalayer.options.bubbles.property || 'value'
return +feature.properties[key] // TODO: should we catch values non castable to int ?
},

computeMinMax: function () {
const values = []
this.datalayer.eachLayer((layer) => {
let value = this._getValue(layer)
if (!isNaN(value)) values.push(Math.sqrt(value))
})
if (!values.length) {
this.options.min = 0
this.options.max = 0
return
}
this.options.min = Math.min(...values)
this.options.max = Math.max(...values)
},

addLayer: function (layer) {
// Do not add yet the layer to the map
// wait for datachanged event, so we can compute breaks once
const value = this._getValue(layer)
if (isNaN(value)) return this
const circle = L.circleMarker(layer.getCenter(), {
value: value,
radius: this.computeRadius(value),
weight: this.datalayer.getOption('weight'),
color: this.datalayer.getOption('color'),
fillColor: this.datalayer.getOption('fillColor'),
})
let id = this.getLayerId(circle)
this._layers[id] = circle
// Needed for the popup to open on the map
layer._map = this._map
circle.on('click', layer.view, layer)
return this
},

onAdd: function (map) {
this.computeMinMax()
L.FeatureGroup.prototype.onAdd.call(this, map)
},

getEditableOptions: function () {
return [
[
'options.bubbles.property',
{
handler: 'Select',
selectOptions: this.datalayer._propertiesIndex,
label: L._('Bubbles map property value'),
},
],
[
'options.bubbles.minSize',
{
handler: 'Range',
min: 1,
max: 10,
step: 1,
label: L._('Mininum size of the bubble'),
helpText: L._('A size in pixel'),
},
],
[
'options.bubbles.maxSize',
{
handler: 'Range',
min: 30,
max: 100,
step: 5,
label: L._('Maximum size of the bubble'),
helpText: L._('A size in pixel'),
},
],
]
},
})

L.U.Layer.Choropleth = L.FeatureGroup.extend({
_type: 'Choropleth',
includes: [L.U.Layer],
Expand Down

0 comments on commit f30ee0f

Please sign in to comment.