mirror of
https://github.com/umap-project/umap.git
synced 2025-04-28 19:42:36 +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.by5,
|
||||
.button-bar.by6,
|
||||
.umap-multiplechoice.by3,
|
||||
.umap-multiplechoice.by5 {
|
||||
.umap-multiplechoice.by5,
|
||||
.umap-multiplechoice.by6 {
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
}
|
||||
.button-bar.by4,
|
||||
|
|
|
@ -40,6 +40,10 @@ export const SCHEMA = {
|
|||
label: translate('Do you want to display caption menus?'),
|
||||
default: true,
|
||||
},
|
||||
categorized: {
|
||||
type: Object,
|
||||
impacts: ['data'],
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
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({
|
||||
undefine: function () {
|
||||
L.DomUtil.addClass(this.wrapper, 'undefined')
|
||||
|
@ -115,156 +265,7 @@ L.FormBuilder.CheckBox.include({
|
|||
})
|
||||
|
||||
L.FormBuilder.ColorPicker = L.FormBuilder.Input.extend({
|
||||
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',
|
||||
],
|
||||
|
||||
colors: U.COLORS,
|
||||
getParentNode: function () {
|
||||
L.FormBuilder.CheckBox.prototype.getParentNode.call(this)
|
||||
return this.quickContainer
|
||||
|
@ -346,6 +347,7 @@ L.FormBuilder.LayerTypeChooser = L.FormBuilder.Select.extend({
|
|||
U.Layer.Cluster,
|
||||
U.Layer.Heat,
|
||||
U.Layer.Choropleth,
|
||||
U.Layer.Categorized,
|
||||
]
|
||||
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: {
|
||||
NAME: L._('Choropleth'),
|
||||
TYPE: 'Choropleth',
|
||||
|
@ -147,42 +217,13 @@ U.Layer.Choropleth = L.FeatureGroup.extend({
|
|||
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) {
|
||||
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 () {
|
||||
const values = []
|
||||
this.datalayer.eachLayer((layer) => {
|
||||
const value = this._getValue(layer)
|
||||
if (!Number.isNaN(value)) values.push(value)
|
||||
})
|
||||
return values
|
||||
},
|
||||
|
||||
computeBreaks: function () {
|
||||
compute: function () {
|
||||
const values = this.getValues()
|
||||
|
||||
if (!values.length) {
|
||||
|
@ -224,7 +265,7 @@ U.Layer.Choropleth = L.FeatureGroup.extend({
|
|||
},
|
||||
|
||||
getColor: function (feature) {
|
||||
if (!feature) return // FIXME shold not happen
|
||||
if (!feature) return // FIXME should not happen
|
||||
const featureValue = this._getValue(feature)
|
||||
// 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++) {
|
||||
|
@ -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) {
|
||||
// Only compute the breaks if we're dealing with choropleth
|
||||
if (!field.startsWith('options.choropleth')) return
|
||||
|
@ -261,7 +283,7 @@ U.Layer.Choropleth = L.FeatureGroup.extend({
|
|||
this.datalayer.options.choropleth.mode = 'manual'
|
||||
if (builder) builder.helpers['options.choropleth.mode'].fetch()
|
||||
}
|
||||
this.computeBreaks()
|
||||
this.compute()
|
||||
// If user changes the mode or the number of classes,
|
||||
// then update the breaks input value
|
||||
if (field === 'options.choropleth.mode' || field === 'options.choropleth.classes') {
|
||||
|
@ -270,10 +292,6 @@ U.Layer.Choropleth = L.FeatureGroup.extend({
|
|||
},
|
||||
|
||||
getEditableOptions: function () {
|
||||
const brewerSchemes = Object.keys(colorbrewer)
|
||||
.filter((k) => k !== 'schemeGroups')
|
||||
.sort()
|
||||
|
||||
return [
|
||||
[
|
||||
'options.choropleth.property',
|
||||
|
@ -288,7 +306,7 @@ U.Layer.Choropleth = L.FeatureGroup.extend({
|
|||
{
|
||||
handler: 'Select',
|
||||
label: L._('Choropleth color palette'),
|
||||
selectOptions: brewerSchemes,
|
||||
selectOptions: this.colorSchemes,
|
||||
},
|
||||
],
|
||||
[
|
||||
|
@ -324,20 +342,137 @@ U.Layer.Choropleth = L.FeatureGroup.extend({
|
|||
]
|
||||
},
|
||||
|
||||
renderLegend: function (container) {
|
||||
const parent = L.DomUtil.create('ul', '', container)
|
||||
let li
|
||||
let color
|
||||
let label
|
||||
getLegendItems: function () {
|
||||
return this.options.breaks.slice(0, -1).map((el, index) => {
|
||||
const from = +this.options.breaks[index].toFixed(1)
|
||||
const to = +this.options.breaks[index + 1].toFixed(1)
|
||||
return [this.options.colors[index], `${from} - ${to}`]
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
this.options.breaks.slice(0, -1).forEach((limit, index) => {
|
||||
li = L.DomUtil.create('li', '', parent)
|
||||
color = L.DomUtil.create('span', 'datalayer-color', li)
|
||||
color.style.backgroundColor = this.options.colors[index]
|
||||
label = L.DomUtil.create('span', '', li)
|
||||
label.textContent = `${+this.options.breaks[index].toFixed(
|
||||
1
|
||||
)} - ${+this.options.breaks[index + 1].toFixed(1)}`
|
||||
U.Layer.Categorized = U.RelativeColorLayer.extend({
|
||||
statics: {
|
||||
NAME: L._('Categorized'),
|
||||
TYPE: 'Categorized',
|
||||
},
|
||||
includes: [U.Layer],
|
||||
MODES: {
|
||||
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.hide()
|
||||
fields.forEach((field) => {
|
||||
for (const field of fields) {
|
||||
this.layer.onEdit(field, builder)
|
||||
})
|
||||
}
|
||||
this.redraw()
|
||||
this.show()
|
||||
break
|
||||
|
@ -733,8 +868,8 @@ U.DataLayer = L.Evented.extend({
|
|||
this.addData(geojson, sync)
|
||||
this._geojson = geojson
|
||||
this._dataloaded = true
|
||||
this.fire('dataloaded')
|
||||
this.fire('datachanged')
|
||||
this.fire('dataloaded')
|
||||
},
|
||||
|
||||
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