mirror of
https://github.com/umap-project/umap.git
synced 2025-04-29 03:42:37 +02:00
Merge pull request #1953 from umap-project/categorized-layer
feat: add new type of layer Categorized
This commit is contained in:
commit
257d205690
6 changed files with 511 additions and 226 deletions
|
@ -496,8 +496,10 @@ input.switch:checked ~ label:after {
|
||||||
}
|
}
|
||||||
.button-bar.by3,
|
.button-bar.by3,
|
||||||
.button-bar.by5,
|
.button-bar.by5,
|
||||||
|
.button-bar.by6,
|
||||||
.umap-multiplechoice.by3,
|
.umap-multiplechoice.by3,
|
||||||
.umap-multiplechoice.by5 {
|
.umap-multiplechoice.by5,
|
||||||
|
.umap-multiplechoice.by6 {
|
||||||
grid-template-columns: 1fr 1fr 1fr;
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
}
|
}
|
||||||
.button-bar.by4,
|
.button-bar.by4,
|
||||||
|
|
|
@ -40,6 +40,10 @@ export const SCHEMA = {
|
||||||
label: translate('Do you want to display caption menus?'),
|
label: translate('Do you want to display caption menus?'),
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
categorized: {
|
||||||
|
type: Object,
|
||||||
|
impacts: ['data'],
|
||||||
|
},
|
||||||
color: {
|
color: {
|
||||||
type: String,
|
type: String,
|
||||||
impacts: ['data'],
|
impacts: ['data'],
|
||||||
|
|
|
@ -1,3 +1,153 @@
|
||||||
|
U.COLORS = [
|
||||||
|
'Black',
|
||||||
|
'Navy',
|
||||||
|
'DarkBlue',
|
||||||
|
'MediumBlue',
|
||||||
|
'Blue',
|
||||||
|
'DarkGreen',
|
||||||
|
'Green',
|
||||||
|
'Teal',
|
||||||
|
'DarkCyan',
|
||||||
|
'DeepSkyBlue',
|
||||||
|
'DarkTurquoise',
|
||||||
|
'MediumSpringGreen',
|
||||||
|
'Lime',
|
||||||
|
'SpringGreen',
|
||||||
|
'Aqua',
|
||||||
|
'Cyan',
|
||||||
|
'MidnightBlue',
|
||||||
|
'DodgerBlue',
|
||||||
|
'LightSeaGreen',
|
||||||
|
'ForestGreen',
|
||||||
|
'SeaGreen',
|
||||||
|
'DarkSlateGray',
|
||||||
|
'DarkSlateGrey',
|
||||||
|
'LimeGreen',
|
||||||
|
'MediumSeaGreen',
|
||||||
|
'Turquoise',
|
||||||
|
'RoyalBlue',
|
||||||
|
'SteelBlue',
|
||||||
|
'DarkSlateBlue',
|
||||||
|
'MediumTurquoise',
|
||||||
|
'Indigo',
|
||||||
|
'DarkOliveGreen',
|
||||||
|
'CadetBlue',
|
||||||
|
'CornflowerBlue',
|
||||||
|
'MediumAquaMarine',
|
||||||
|
'DimGray',
|
||||||
|
'DimGrey',
|
||||||
|
'SlateBlue',
|
||||||
|
'OliveDrab',
|
||||||
|
'SlateGray',
|
||||||
|
'SlateGrey',
|
||||||
|
'LightSlateGray',
|
||||||
|
'LightSlateGrey',
|
||||||
|
'MediumSlateBlue',
|
||||||
|
'LawnGreen',
|
||||||
|
'Chartreuse',
|
||||||
|
'Aquamarine',
|
||||||
|
'Maroon',
|
||||||
|
'Purple',
|
||||||
|
'Olive',
|
||||||
|
'Gray',
|
||||||
|
'Grey',
|
||||||
|
'SkyBlue',
|
||||||
|
'LightSkyBlue',
|
||||||
|
'BlueViolet',
|
||||||
|
'DarkRed',
|
||||||
|
'DarkMagenta',
|
||||||
|
'SaddleBrown',
|
||||||
|
'DarkSeaGreen',
|
||||||
|
'LightGreen',
|
||||||
|
'MediumPurple',
|
||||||
|
'DarkViolet',
|
||||||
|
'PaleGreen',
|
||||||
|
'DarkOrchid',
|
||||||
|
'YellowGreen',
|
||||||
|
'Sienna',
|
||||||
|
'Brown',
|
||||||
|
'DarkGray',
|
||||||
|
'DarkGrey',
|
||||||
|
'LightBlue',
|
||||||
|
'GreenYellow',
|
||||||
|
'PaleTurquoise',
|
||||||
|
'LightSteelBlue',
|
||||||
|
'PowderBlue',
|
||||||
|
'FireBrick',
|
||||||
|
'DarkGoldenRod',
|
||||||
|
'MediumOrchid',
|
||||||
|
'RosyBrown',
|
||||||
|
'DarkKhaki',
|
||||||
|
'Silver',
|
||||||
|
'MediumVioletRed',
|
||||||
|
'IndianRed',
|
||||||
|
'Peru',
|
||||||
|
'Chocolate',
|
||||||
|
'Tan',
|
||||||
|
'LightGray',
|
||||||
|
'LightGrey',
|
||||||
|
'Thistle',
|
||||||
|
'Orchid',
|
||||||
|
'GoldenRod',
|
||||||
|
'PaleVioletRed',
|
||||||
|
'Crimson',
|
||||||
|
'Gainsboro',
|
||||||
|
'Plum',
|
||||||
|
'BurlyWood',
|
||||||
|
'LightCyan',
|
||||||
|
'Lavender',
|
||||||
|
'DarkSalmon',
|
||||||
|
'Violet',
|
||||||
|
'PaleGoldenRod',
|
||||||
|
'LightCoral',
|
||||||
|
'Khaki',
|
||||||
|
'AliceBlue',
|
||||||
|
'HoneyDew',
|
||||||
|
'Azure',
|
||||||
|
'SandyBrown',
|
||||||
|
'Wheat',
|
||||||
|
'Beige',
|
||||||
|
'WhiteSmoke',
|
||||||
|
'MintCream',
|
||||||
|
'GhostWhite',
|
||||||
|
'Salmon',
|
||||||
|
'AntiqueWhite',
|
||||||
|
'Linen',
|
||||||
|
'LightGoldenRodYellow',
|
||||||
|
'OldLace',
|
||||||
|
'Red',
|
||||||
|
'Fuchsia',
|
||||||
|
'Magenta',
|
||||||
|
'DeepPink',
|
||||||
|
'OrangeRed',
|
||||||
|
'Tomato',
|
||||||
|
'HotPink',
|
||||||
|
'Coral',
|
||||||
|
'DarkOrange',
|
||||||
|
'LightSalmon',
|
||||||
|
'Orange',
|
||||||
|
'LightPink',
|
||||||
|
'Pink',
|
||||||
|
'Gold',
|
||||||
|
'PeachPuff',
|
||||||
|
'NavajoWhite',
|
||||||
|
'Moccasin',
|
||||||
|
'Bisque',
|
||||||
|
'MistyRose',
|
||||||
|
'BlanchedAlmond',
|
||||||
|
'PapayaWhip',
|
||||||
|
'LavenderBlush',
|
||||||
|
'SeaShell',
|
||||||
|
'Cornsilk',
|
||||||
|
'LemonChiffon',
|
||||||
|
'FloralWhite',
|
||||||
|
'Snow',
|
||||||
|
'Yellow',
|
||||||
|
'LightYellow',
|
||||||
|
'Ivory',
|
||||||
|
'White',
|
||||||
|
]
|
||||||
|
|
||||||
L.FormBuilder.Element.include({
|
L.FormBuilder.Element.include({
|
||||||
undefine: function () {
|
undefine: function () {
|
||||||
L.DomUtil.addClass(this.wrapper, 'undefined')
|
L.DomUtil.addClass(this.wrapper, 'undefined')
|
||||||
|
@ -115,156 +265,7 @@ L.FormBuilder.CheckBox.include({
|
||||||
})
|
})
|
||||||
|
|
||||||
L.FormBuilder.ColorPicker = L.FormBuilder.Input.extend({
|
L.FormBuilder.ColorPicker = L.FormBuilder.Input.extend({
|
||||||
colors: [
|
colors: U.COLORS,
|
||||||
'Black',
|
|
||||||
'Navy',
|
|
||||||
'DarkBlue',
|
|
||||||
'MediumBlue',
|
|
||||||
'Blue',
|
|
||||||
'DarkGreen',
|
|
||||||
'Green',
|
|
||||||
'Teal',
|
|
||||||
'DarkCyan',
|
|
||||||
'DeepSkyBlue',
|
|
||||||
'DarkTurquoise',
|
|
||||||
'MediumSpringGreen',
|
|
||||||
'Lime',
|
|
||||||
'SpringGreen',
|
|
||||||
'Aqua',
|
|
||||||
'Cyan',
|
|
||||||
'MidnightBlue',
|
|
||||||
'DodgerBlue',
|
|
||||||
'LightSeaGreen',
|
|
||||||
'ForestGreen',
|
|
||||||
'SeaGreen',
|
|
||||||
'DarkSlateGray',
|
|
||||||
'DarkSlateGrey',
|
|
||||||
'LimeGreen',
|
|
||||||
'MediumSeaGreen',
|
|
||||||
'Turquoise',
|
|
||||||
'RoyalBlue',
|
|
||||||
'SteelBlue',
|
|
||||||
'DarkSlateBlue',
|
|
||||||
'MediumTurquoise',
|
|
||||||
'Indigo',
|
|
||||||
'DarkOliveGreen',
|
|
||||||
'CadetBlue',
|
|
||||||
'CornflowerBlue',
|
|
||||||
'MediumAquaMarine',
|
|
||||||
'DimGray',
|
|
||||||
'DimGrey',
|
|
||||||
'SlateBlue',
|
|
||||||
'OliveDrab',
|
|
||||||
'SlateGray',
|
|
||||||
'SlateGrey',
|
|
||||||
'LightSlateGray',
|
|
||||||
'LightSlateGrey',
|
|
||||||
'MediumSlateBlue',
|
|
||||||
'LawnGreen',
|
|
||||||
'Chartreuse',
|
|
||||||
'Aquamarine',
|
|
||||||
'Maroon',
|
|
||||||
'Purple',
|
|
||||||
'Olive',
|
|
||||||
'Gray',
|
|
||||||
'Grey',
|
|
||||||
'SkyBlue',
|
|
||||||
'LightSkyBlue',
|
|
||||||
'BlueViolet',
|
|
||||||
'DarkRed',
|
|
||||||
'DarkMagenta',
|
|
||||||
'SaddleBrown',
|
|
||||||
'DarkSeaGreen',
|
|
||||||
'LightGreen',
|
|
||||||
'MediumPurple',
|
|
||||||
'DarkViolet',
|
|
||||||
'PaleGreen',
|
|
||||||
'DarkOrchid',
|
|
||||||
'YellowGreen',
|
|
||||||
'Sienna',
|
|
||||||
'Brown',
|
|
||||||
'DarkGray',
|
|
||||||
'DarkGrey',
|
|
||||||
'LightBlue',
|
|
||||||
'GreenYellow',
|
|
||||||
'PaleTurquoise',
|
|
||||||
'LightSteelBlue',
|
|
||||||
'PowderBlue',
|
|
||||||
'FireBrick',
|
|
||||||
'DarkGoldenRod',
|
|
||||||
'MediumOrchid',
|
|
||||||
'RosyBrown',
|
|
||||||
'DarkKhaki',
|
|
||||||
'Silver',
|
|
||||||
'MediumVioletRed',
|
|
||||||
'IndianRed',
|
|
||||||
'Peru',
|
|
||||||
'Chocolate',
|
|
||||||
'Tan',
|
|
||||||
'LightGray',
|
|
||||||
'LightGrey',
|
|
||||||
'Thistle',
|
|
||||||
'Orchid',
|
|
||||||
'GoldenRod',
|
|
||||||
'PaleVioletRed',
|
|
||||||
'Crimson',
|
|
||||||
'Gainsboro',
|
|
||||||
'Plum',
|
|
||||||
'BurlyWood',
|
|
||||||
'LightCyan',
|
|
||||||
'Lavender',
|
|
||||||
'DarkSalmon',
|
|
||||||
'Violet',
|
|
||||||
'PaleGoldenRod',
|
|
||||||
'LightCoral',
|
|
||||||
'Khaki',
|
|
||||||
'AliceBlue',
|
|
||||||
'HoneyDew',
|
|
||||||
'Azure',
|
|
||||||
'SandyBrown',
|
|
||||||
'Wheat',
|
|
||||||
'Beige',
|
|
||||||
'WhiteSmoke',
|
|
||||||
'MintCream',
|
|
||||||
'GhostWhite',
|
|
||||||
'Salmon',
|
|
||||||
'AntiqueWhite',
|
|
||||||
'Linen',
|
|
||||||
'LightGoldenRodYellow',
|
|
||||||
'OldLace',
|
|
||||||
'Red',
|
|
||||||
'Fuchsia',
|
|
||||||
'Magenta',
|
|
||||||
'DeepPink',
|
|
||||||
'OrangeRed',
|
|
||||||
'Tomato',
|
|
||||||
'HotPink',
|
|
||||||
'Coral',
|
|
||||||
'DarkOrange',
|
|
||||||
'LightSalmon',
|
|
||||||
'Orange',
|
|
||||||
'LightPink',
|
|
||||||
'Pink',
|
|
||||||
'Gold',
|
|
||||||
'PeachPuff',
|
|
||||||
'NavajoWhite',
|
|
||||||
'Moccasin',
|
|
||||||
'Bisque',
|
|
||||||
'MistyRose',
|
|
||||||
'BlanchedAlmond',
|
|
||||||
'PapayaWhip',
|
|
||||||
'LavenderBlush',
|
|
||||||
'SeaShell',
|
|
||||||
'Cornsilk',
|
|
||||||
'LemonChiffon',
|
|
||||||
'FloralWhite',
|
|
||||||
'Snow',
|
|
||||||
'Yellow',
|
|
||||||
'LightYellow',
|
|
||||||
'Ivory',
|
|
||||||
'White',
|
|
||||||
],
|
|
||||||
|
|
||||||
getParentNode: function () {
|
getParentNode: function () {
|
||||||
L.FormBuilder.CheckBox.prototype.getParentNode.call(this)
|
L.FormBuilder.CheckBox.prototype.getParentNode.call(this)
|
||||||
return this.quickContainer
|
return this.quickContainer
|
||||||
|
@ -346,6 +347,7 @@ L.FormBuilder.LayerTypeChooser = L.FormBuilder.Select.extend({
|
||||||
U.Layer.Cluster,
|
U.Layer.Cluster,
|
||||||
U.Layer.Heat,
|
U.Layer.Heat,
|
||||||
U.Layer.Choropleth,
|
U.Layer.Choropleth,
|
||||||
|
U.Layer.Categorized,
|
||||||
]
|
]
|
||||||
return layer_classes.map((class_) => [class_.TYPE, class_.NAME])
|
return layer_classes.map((class_) => [class_.TYPE, class_.NAME])
|
||||||
},
|
},
|
||||||
|
|
|
@ -126,7 +126,77 @@ U.Layer.Cluster = L.MarkerClusterGroup.extend({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
U.Layer.Choropleth = L.FeatureGroup.extend({
|
// Layer where each feature color is relative to the others,
|
||||||
|
// so we need all features before behing able to set one
|
||||||
|
// feature layer
|
||||||
|
U.RelativeColorLayer = L.FeatureGroup.extend({
|
||||||
|
initialize: function (datalayer) {
|
||||||
|
this.datalayer = datalayer
|
||||||
|
this.colorSchemes = Object.keys(colorbrewer)
|
||||||
|
.filter((k) => k !== 'schemeGroups')
|
||||||
|
.sort()
|
||||||
|
const key = this.getType().toLowerCase()
|
||||||
|
if (!U.Utils.isObject(this.datalayer.options[key])) {
|
||||||
|
this.datalayer.options[key] = {}
|
||||||
|
}
|
||||||
|
L.FeatureGroup.prototype.initialize.call(this, [], this.datalayer.options[key])
|
||||||
|
this.datalayer.onceDataLoaded(() => {
|
||||||
|
this.redraw()
|
||||||
|
this.datalayer.on('datachanged', this.redraw, this)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
redraw: function () {
|
||||||
|
this.compute()
|
||||||
|
if (this._map) this.eachLayer(this._map.addLayer, this._map)
|
||||||
|
},
|
||||||
|
|
||||||
|
getOption: function (option, feature) {
|
||||||
|
if (feature && option === feature.staticOptions.mainColor) {
|
||||||
|
return this.getColor(feature)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
addLayer: function (layer) {
|
||||||
|
// Do not add yet the layer to the map
|
||||||
|
// wait for datachanged event, so we can compute breaks only once
|
||||||
|
const id = this.getLayerId(layer)
|
||||||
|
this._layers[id] = layer
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
|
||||||
|
onAdd: function (map) {
|
||||||
|
this.compute()
|
||||||
|
L.FeatureGroup.prototype.onAdd.call(this, map)
|
||||||
|
},
|
||||||
|
|
||||||
|
getValues: function () {
|
||||||
|
const values = []
|
||||||
|
this.datalayer.eachLayer((layer) => {
|
||||||
|
const value = this._getValue(layer)
|
||||||
|
if (value !== undefined) values.push(value)
|
||||||
|
})
|
||||||
|
return values
|
||||||
|
},
|
||||||
|
|
||||||
|
renderLegend: function (container) {
|
||||||
|
const parent = L.DomUtil.create('ul', '', container)
|
||||||
|
const items = this.getLegendItems()
|
||||||
|
for (const [color, label] of items) {
|
||||||
|
const li = L.DomUtil.create('li', '', parent)
|
||||||
|
const colorEl = L.DomUtil.create('span', 'datalayer-color', li)
|
||||||
|
colorEl.style.backgroundColor = color
|
||||||
|
const labelEl = L.DomUtil.create('span', '', li)
|
||||||
|
labelEl.textContent = label
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getColorSchemes: function (classes) {
|
||||||
|
return this.colorSchemes.filter((scheme) => Boolean(colorbrewer[scheme][classes]))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
U.Layer.Choropleth = U.RelativeColorLayer.extend({
|
||||||
statics: {
|
statics: {
|
||||||
NAME: L._('Choropleth'),
|
NAME: L._('Choropleth'),
|
||||||
TYPE: 'Choropleth',
|
TYPE: 'Choropleth',
|
||||||
|
@ -147,42 +217,13 @@ U.Layer.Choropleth = L.FeatureGroup.extend({
|
||||||
manual: L._('Manual'),
|
manual: L._('Manual'),
|
||||||
},
|
},
|
||||||
|
|
||||||
initialize: function (datalayer) {
|
|
||||||
this.datalayer = datalayer
|
|
||||||
if (!U.Utils.isObject(this.datalayer.options.choropleth)) {
|
|
||||||
this.datalayer.options.choropleth = {}
|
|
||||||
}
|
|
||||||
L.FeatureGroup.prototype.initialize.call(
|
|
||||||
this,
|
|
||||||
[],
|
|
||||||
this.datalayer.options.choropleth
|
|
||||||
)
|
|
||||||
this.datalayer.onceDataLoaded(() => {
|
|
||||||
this.redraw()
|
|
||||||
this.datalayer.on('datachanged', this.redraw, this)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
redraw: function () {
|
|
||||||
this.computeBreaks()
|
|
||||||
if (this._map) this.eachLayer(this._map.addLayer, this._map)
|
|
||||||
},
|
|
||||||
|
|
||||||
_getValue: function (feature) {
|
_getValue: function (feature) {
|
||||||
const key = this.datalayer.options.choropleth.property || 'value'
|
const key = this.datalayer.options.choropleth.property || 'value'
|
||||||
return +feature.properties[key] // TODO: should we catch values non castable to int ?
|
const value = +feature.properties[key]
|
||||||
|
if (!Number.isNaN(value)) return value
|
||||||
},
|
},
|
||||||
|
|
||||||
getValues: function () {
|
compute: function () {
|
||||||
const values = []
|
|
||||||
this.datalayer.eachLayer((layer) => {
|
|
||||||
const value = this._getValue(layer)
|
|
||||||
if (!Number.isNaN(value)) values.push(value)
|
|
||||||
})
|
|
||||||
return values
|
|
||||||
},
|
|
||||||
|
|
||||||
computeBreaks: function () {
|
|
||||||
const values = this.getValues()
|
const values = this.getValues()
|
||||||
|
|
||||||
if (!values.length) {
|
if (!values.length) {
|
||||||
|
@ -224,7 +265,7 @@ U.Layer.Choropleth = L.FeatureGroup.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
getColor: function (feature) {
|
getColor: function (feature) {
|
||||||
if (!feature) return // FIXME shold not happen
|
if (!feature) return // FIXME should not happen
|
||||||
const featureValue = this._getValue(feature)
|
const featureValue = this._getValue(feature)
|
||||||
// Find the bucket/step/limit that this value is less than and give it that color
|
// Find the bucket/step/limit that this value is less than and give it that color
|
||||||
for (let i = 1; i < this.options.breaks.length; i++) {
|
for (let i = 1; i < this.options.breaks.length; i++) {
|
||||||
|
@ -234,25 +275,6 @@ U.Layer.Choropleth = L.FeatureGroup.extend({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getOption: function (option, feature) {
|
|
||||||
if (feature && option === feature.staticOptions.mainColor) {
|
|
||||||
return this.getColor(feature)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
addLayer: function (layer) {
|
|
||||||
// Do not add yet the layer to the map
|
|
||||||
// wait for datachanged event, so we want compute breaks once
|
|
||||||
const id = this.getLayerId(layer)
|
|
||||||
this._layers[id] = layer
|
|
||||||
return this
|
|
||||||
},
|
|
||||||
|
|
||||||
onAdd: function (map) {
|
|
||||||
this.computeBreaks()
|
|
||||||
L.FeatureGroup.prototype.onAdd.call(this, map)
|
|
||||||
},
|
|
||||||
|
|
||||||
onEdit: function (field, builder) {
|
onEdit: function (field, builder) {
|
||||||
// Only compute the breaks if we're dealing with choropleth
|
// Only compute the breaks if we're dealing with choropleth
|
||||||
if (!field.startsWith('options.choropleth')) return
|
if (!field.startsWith('options.choropleth')) return
|
||||||
|
@ -261,7 +283,7 @@ U.Layer.Choropleth = L.FeatureGroup.extend({
|
||||||
this.datalayer.options.choropleth.mode = 'manual'
|
this.datalayer.options.choropleth.mode = 'manual'
|
||||||
if (builder) builder.helpers['options.choropleth.mode'].fetch()
|
if (builder) builder.helpers['options.choropleth.mode'].fetch()
|
||||||
}
|
}
|
||||||
this.computeBreaks()
|
this.compute()
|
||||||
// If user changes the mode or the number of classes,
|
// If user changes the mode or the number of classes,
|
||||||
// then update the breaks input value
|
// then update the breaks input value
|
||||||
if (field === 'options.choropleth.mode' || field === 'options.choropleth.classes') {
|
if (field === 'options.choropleth.mode' || field === 'options.choropleth.classes') {
|
||||||
|
@ -270,10 +292,6 @@ U.Layer.Choropleth = L.FeatureGroup.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
getEditableOptions: function () {
|
getEditableOptions: function () {
|
||||||
const brewerSchemes = Object.keys(colorbrewer)
|
|
||||||
.filter((k) => k !== 'schemeGroups')
|
|
||||||
.sort()
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
[
|
[
|
||||||
'options.choropleth.property',
|
'options.choropleth.property',
|
||||||
|
@ -288,7 +306,7 @@ U.Layer.Choropleth = L.FeatureGroup.extend({
|
||||||
{
|
{
|
||||||
handler: 'Select',
|
handler: 'Select',
|
||||||
label: L._('Choropleth color palette'),
|
label: L._('Choropleth color palette'),
|
||||||
selectOptions: brewerSchemes,
|
selectOptions: this.colorSchemes,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
@ -324,20 +342,137 @@ U.Layer.Choropleth = L.FeatureGroup.extend({
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
renderLegend: function (container) {
|
getLegendItems: function () {
|
||||||
const parent = L.DomUtil.create('ul', '', container)
|
return this.options.breaks.slice(0, -1).map((el, index) => {
|
||||||
let li
|
const from = +this.options.breaks[index].toFixed(1)
|
||||||
let color
|
const to = +this.options.breaks[index + 1].toFixed(1)
|
||||||
let label
|
return [this.options.colors[index], `${from} - ${to}`]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
this.options.breaks.slice(0, -1).forEach((limit, index) => {
|
U.Layer.Categorized = U.RelativeColorLayer.extend({
|
||||||
li = L.DomUtil.create('li', '', parent)
|
statics: {
|
||||||
color = L.DomUtil.create('span', 'datalayer-color', li)
|
NAME: L._('Categorized'),
|
||||||
color.style.backgroundColor = this.options.colors[index]
|
TYPE: 'Categorized',
|
||||||
label = L.DomUtil.create('span', '', li)
|
},
|
||||||
label.textContent = `${+this.options.breaks[index].toFixed(
|
includes: [U.Layer],
|
||||||
1
|
MODES: {
|
||||||
)} - ${+this.options.breaks[index + 1].toFixed(1)}`
|
manual: L._('Manual'),
|
||||||
|
alpha: L._('Alphabetical'),
|
||||||
|
},
|
||||||
|
defaults: {
|
||||||
|
color: 'white',
|
||||||
|
fillColor: 'red',
|
||||||
|
fillOpacity: 0.7,
|
||||||
|
weight: 2,
|
||||||
|
},
|
||||||
|
|
||||||
|
_getValue: function (feature) {
|
||||||
|
const key =
|
||||||
|
this.datalayer.options.categorized.property || this.datalayer._propertiesIndex[0]
|
||||||
|
return feature.properties[key]
|
||||||
|
},
|
||||||
|
|
||||||
|
getColor: function (feature) {
|
||||||
|
if (!feature) return // FIXME should not happen
|
||||||
|
const featureValue = this._getValue(feature)
|
||||||
|
for (let i = 0; i < this.options.categories.length; i++) {
|
||||||
|
if (featureValue === this.options.categories[i]) {
|
||||||
|
return this.options.colors[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
compute: function () {
|
||||||
|
const values = this.getValues()
|
||||||
|
|
||||||
|
if (!values.length) {
|
||||||
|
this.options.categories = []
|
||||||
|
this.options.colors = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const mode = this.datalayer.options.categorized.mode
|
||||||
|
let categories = []
|
||||||
|
if (mode === 'manual') {
|
||||||
|
const manualCategories = this.datalayer.options.categorized.categories
|
||||||
|
if (manualCategories) {
|
||||||
|
categories = manualCategories.split(',')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
categories = values
|
||||||
|
.filter((val, idx, arr) => arr.indexOf(val) === idx)
|
||||||
|
.sort(U.Utils.naturalSort)
|
||||||
|
}
|
||||||
|
this.options.categories = categories
|
||||||
|
this.datalayer.options.categorized.categories = this.options.categories.join(',')
|
||||||
|
const fillColor = this.datalayer.getOption('fillColor') || this.defaults.fillColor
|
||||||
|
const colorScheme = this.datalayer.options.categorized.brewer
|
||||||
|
this._classes = this.options.categories.length
|
||||||
|
if (colorbrewer[colorScheme]?.[this._classes]) {
|
||||||
|
this.options.colors = colorbrewer[colorScheme][this._classes]
|
||||||
|
} else {
|
||||||
|
this.options.colors = colorbrewer?.Accent[this._classes] ? colorbrewer?.Accent[this._classes] : U.COLORS
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getEditableOptions: function () {
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'options.categorized.property',
|
||||||
|
{
|
||||||
|
handler: 'Select',
|
||||||
|
selectOptions: this.datalayer._propertiesIndex,
|
||||||
|
label: L._('Category property'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'options.categorized.brewer',
|
||||||
|
{
|
||||||
|
handler: 'Select',
|
||||||
|
label: L._('Color palette'),
|
||||||
|
selectOptions: this.getColorSchemes(this._classes),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'options.categorized.categories',
|
||||||
|
{
|
||||||
|
handler: 'BlurInput',
|
||||||
|
label: L._('Categories'),
|
||||||
|
helpText: L._('Comma separated list of categories.'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'options.categorized.mode',
|
||||||
|
{
|
||||||
|
handler: 'MultiChoice',
|
||||||
|
default: 'alpha',
|
||||||
|
choices: Object.entries(this.MODES),
|
||||||
|
label: L._('Categories mode'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
onEdit: function (field, builder) {
|
||||||
|
// Only compute the categories if we're dealing with categorized
|
||||||
|
if (!field.startsWith('options.categorized')) return
|
||||||
|
// If user touches the categories, then force manual mode
|
||||||
|
if (field === 'options.categorized.categories') {
|
||||||
|
this.datalayer.options.categorized.mode = 'manual'
|
||||||
|
if (builder) builder.helpers['options.categorized.mode'].fetch()
|
||||||
|
}
|
||||||
|
this.compute()
|
||||||
|
// If user changes the mode
|
||||||
|
// then update the categories input value
|
||||||
|
if (field === 'options.categorized.mode') {
|
||||||
|
if (builder) builder.helpers['options.categorized.categories'].fetch()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getLegendItems: function () {
|
||||||
|
return this.options.categories.map((limit, index) => {
|
||||||
|
return [this.options.colors[index], this.options.categories[index]]
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -614,9 +749,9 @@ U.DataLayer = L.Evented.extend({
|
||||||
this.resetLayer()
|
this.resetLayer()
|
||||||
}
|
}
|
||||||
this.hide()
|
this.hide()
|
||||||
fields.forEach((field) => {
|
for (const field of fields) {
|
||||||
this.layer.onEdit(field, builder)
|
this.layer.onEdit(field, builder)
|
||||||
})
|
}
|
||||||
this.redraw()
|
this.redraw()
|
||||||
this.show()
|
this.show()
|
||||||
break
|
break
|
||||||
|
@ -733,8 +868,8 @@ U.DataLayer = L.Evented.extend({
|
||||||
this.addData(geojson, sync)
|
this.addData(geojson, sync)
|
||||||
this._geojson = geojson
|
this._geojson = geojson
|
||||||
this._dataloaded = true
|
this._dataloaded = true
|
||||||
this.fire('dataloaded')
|
|
||||||
this.fire('datachanged')
|
this.fire('datachanged')
|
||||||
|
this.fire('dataloaded')
|
||||||
},
|
},
|
||||||
|
|
||||||
fromUmapGeoJSON: async function (geojson) {
|
fromUmapGeoJSON: async function (geojson) {
|
||||||
|
|
1
umap/tests/fixtures/categorized_highway.geojson
vendored
Normal file
1
umap/tests/fixtures/categorized_highway.geojson
vendored
Normal file
File diff suppressed because one or more lines are too long
141
umap/tests/integration/test_categorized_layer.py
Normal file
141
umap/tests/integration/test_categorized_layer.py
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from playwright.sync_api import expect
|
||||||
|
|
||||||
|
from ..base import DataLayerFactory
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
def test_basic_categorized_map_with_default_color(map, live_server, page):
|
||||||
|
path = Path(__file__).parent.parent / "fixtures/categorized_highway.geojson"
|
||||||
|
data = json.loads(path.read_text())
|
||||||
|
DataLayerFactory(data=data, map=map)
|
||||||
|
page.goto(f"{live_server.url}{map.get_absolute_url()}#13/48.4378/3.3043")
|
||||||
|
# residential
|
||||||
|
expect(page.locator("path[stroke='#7fc97f']")).to_have_count(5)
|
||||||
|
# secondary
|
||||||
|
expect(page.locator("path[stroke='#beaed4']")).to_have_count(1)
|
||||||
|
# service
|
||||||
|
expect(page.locator("path[stroke='#fdc086']")).to_have_count(2)
|
||||||
|
# tertiary
|
||||||
|
expect(page.locator("path[stroke='#ffff99']")).to_have_count(6)
|
||||||
|
# track
|
||||||
|
expect(page.locator("path[stroke='#386cb0']")).to_have_count(11)
|
||||||
|
# unclassified
|
||||||
|
expect(page.locator("path[stroke='#f0027f']")).to_have_count(7)
|
||||||
|
|
||||||
|
|
||||||
|
def test_basic_categorized_map_with_custom_brewer(openmap, live_server, page):
|
||||||
|
path = Path(__file__).parent.parent / "fixtures/categorized_highway.geojson"
|
||||||
|
data = json.loads(path.read_text())
|
||||||
|
|
||||||
|
# Change brewer at load
|
||||||
|
data["_umap_options"]["categorized"]["brewer"] = "Spectral"
|
||||||
|
DataLayerFactory(data=data, map=openmap)
|
||||||
|
|
||||||
|
page.goto(f"{live_server.url}{openmap.get_absolute_url()}#13/48.4378/3.3043")
|
||||||
|
# residential
|
||||||
|
expect(page.locator("path[stroke='#d53e4f']")).to_have_count(5)
|
||||||
|
# secondary
|
||||||
|
expect(page.locator("path[stroke='#fc8d59']")).to_have_count(1)
|
||||||
|
# service
|
||||||
|
expect(page.locator("path[stroke='#fee08b']")).to_have_count(2)
|
||||||
|
# tertiary
|
||||||
|
expect(page.locator("path[stroke='#e6f598']")).to_have_count(6)
|
||||||
|
# track
|
||||||
|
expect(page.locator("path[stroke='#99d594']")).to_have_count(11)
|
||||||
|
# unclassified
|
||||||
|
expect(page.locator("path[stroke='#3288bd']")).to_have_count(7)
|
||||||
|
|
||||||
|
# Now change brewer from UI
|
||||||
|
page.get_by_role("button", name="Edit").click()
|
||||||
|
page.get_by_role("link", name="Manage layers").click()
|
||||||
|
page.locator(".panel").get_by_title("Edit", exact=True).click()
|
||||||
|
page.get_by_text("Categorized: settings").click()
|
||||||
|
page.locator('select[name="brewer"]').select_option("Paired")
|
||||||
|
|
||||||
|
# residential
|
||||||
|
expect(page.locator("path[stroke='#a6cee3']")).to_have_count(5)
|
||||||
|
# secondary
|
||||||
|
expect(page.locator("path[stroke='#1f78b4']")).to_have_count(1)
|
||||||
|
# service
|
||||||
|
expect(page.locator("path[stroke='#b2df8a']")).to_have_count(2)
|
||||||
|
# tertiary
|
||||||
|
expect(page.locator("path[stroke='#33a02c']")).to_have_count(6)
|
||||||
|
# track
|
||||||
|
expect(page.locator("path[stroke='#fb9a99']")).to_have_count(11)
|
||||||
|
# unclassified
|
||||||
|
expect(page.locator("path[stroke='#e31a1c']")).to_have_count(7)
|
||||||
|
|
||||||
|
|
||||||
|
def test_basic_categorized_map_with_custom_categories(openmap, live_server, page):
|
||||||
|
path = Path(__file__).parent.parent / "fixtures/categorized_highway.geojson"
|
||||||
|
data = json.loads(path.read_text())
|
||||||
|
|
||||||
|
# Change categories at load
|
||||||
|
data["_umap_options"]["categorized"]["categories"] = (
|
||||||
|
"unclassified,track,service,residential,tertiary,secondary"
|
||||||
|
)
|
||||||
|
data["_umap_options"]["categorized"]["mode"] = "manual"
|
||||||
|
DataLayerFactory(data=data, map=openmap)
|
||||||
|
|
||||||
|
page.goto(f"{live_server.url}{openmap.get_absolute_url()}#13/48.4378/3.3043")
|
||||||
|
|
||||||
|
# unclassified
|
||||||
|
expect(page.locator("path[stroke='#7fc97f']")).to_have_count(7)
|
||||||
|
# track
|
||||||
|
expect(page.locator("path[stroke='#beaed4']")).to_have_count(11)
|
||||||
|
# service
|
||||||
|
expect(page.locator("path[stroke='#fdc086']")).to_have_count(2)
|
||||||
|
# residential
|
||||||
|
expect(page.locator("path[stroke='#ffff99']")).to_have_count(5)
|
||||||
|
# tertiary
|
||||||
|
expect(page.locator("path[stroke='#386cb0']")).to_have_count(6)
|
||||||
|
# secondary
|
||||||
|
expect(page.locator("path[stroke='#f0027f']")).to_have_count(1)
|
||||||
|
|
||||||
|
# Now change categories from UI
|
||||||
|
page.get_by_role("button", name="Edit").click()
|
||||||
|
page.get_by_role("link", name="Manage layers").click()
|
||||||
|
page.locator(".panel").get_by_title("Edit", exact=True).click()
|
||||||
|
page.get_by_text("Categorized: settings").click()
|
||||||
|
page.locator('input[name="categories"]').fill(
|
||||||
|
"secondary,tertiary,residential,service,track,unclassified"
|
||||||
|
)
|
||||||
|
page.locator('input[name="categories"]').blur()
|
||||||
|
|
||||||
|
# secondary
|
||||||
|
expect(page.locator("path[stroke='#7fc97f']")).to_have_count(1)
|
||||||
|
# tertiary
|
||||||
|
expect(page.locator("path[stroke='#beaed4']")).to_have_count(6)
|
||||||
|
# residential
|
||||||
|
expect(page.locator("path[stroke='#fdc086']")).to_have_count(5)
|
||||||
|
# service
|
||||||
|
expect(page.locator("path[stroke='#ffff99']")).to_have_count(2)
|
||||||
|
# track
|
||||||
|
expect(page.locator("path[stroke='#386cb0']")).to_have_count(11)
|
||||||
|
# unclassified
|
||||||
|
expect(page.locator("path[stroke='#f0027f']")).to_have_count(7)
|
||||||
|
|
||||||
|
# Now go back to automatic categories
|
||||||
|
page.get_by_role("button", name="Edit").click()
|
||||||
|
page.get_by_role("link", name="Manage layers").click()
|
||||||
|
page.locator(".panel").get_by_title("Edit", exact=True).click()
|
||||||
|
page.get_by_text("Categorized: settings").click()
|
||||||
|
page.get_by_text("Alphabetical").click()
|
||||||
|
|
||||||
|
# residential
|
||||||
|
expect(page.locator("path[stroke='#7fc97f']")).to_have_count(5)
|
||||||
|
# secondary
|
||||||
|
expect(page.locator("path[stroke='#beaed4']")).to_have_count(1)
|
||||||
|
# service
|
||||||
|
expect(page.locator("path[stroke='#fdc086']")).to_have_count(2)
|
||||||
|
# tertiary
|
||||||
|
expect(page.locator("path[stroke='#ffff99']")).to_have_count(6)
|
||||||
|
# track
|
||||||
|
expect(page.locator("path[stroke='#386cb0']")).to_have_count(11)
|
||||||
|
# unclassified
|
||||||
|
expect(page.locator("path[stroke='#f0027f']")).to_have_count(7)
|
Loading…
Reference in a new issue