-
Notifications
You must be signed in to change notification settings - Fork 410
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
* #9092 Styling classification for WFS and Vector layers
- Loading branch information
1 parent
be05b0e
commit d834240
Showing
9 changed files
with
344 additions
and
33 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -244,12 +244,12 @@ | |
"react-copy-to-clipboard": "5.0.0", | ||
"react-data-grid": "5.0.4", | ||
"react-data-grid-addons": "5.0.4", | ||
"react-draft-wysiwyg": "npm:@geosolutions/[email protected]", | ||
"react-dnd": "2.6.0", | ||
"react-dnd-html5-backend": "2.6.0", | ||
"react-dnd-test-backend": "2.6.0", | ||
"react-dock": "0.2.4", | ||
"react-dom": "16.10.1", | ||
"react-draft-wysiwyg": "npm:@geosolutions/[email protected]", | ||
"react-draggable": "2.2.6", | ||
"react-dropzone": "3.13.1", | ||
"react-error-boundary": "1.2.5", | ||
|
@@ -292,6 +292,7 @@ | |
"rxjs": "5.1.1", | ||
"screenfull": "4.0.0", | ||
"shpjs": "3.4.2", | ||
"simple-statistics": "7.8.3", | ||
"stickybits": "3.6.6", | ||
"stream": "0.0.2", | ||
"tinycolor2": "1.4.1", | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
/* | ||
* Copyright 2023, GeoSolutions Sas. | ||
* All rights reserved. | ||
* | ||
* This source code is licensed under the BSD-style license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
*/ | ||
|
||
import uniq from 'lodash/uniq'; | ||
import isNil from 'lodash/isNil'; | ||
import chroma from 'chroma-js'; | ||
import { defaultClassificationColors } from './SLDService'; | ||
|
||
const getSimpleStatistics = () => import('simple-statistics').then(mod => mod); | ||
|
||
const getColorClasses = ({ ramp, intervals, reverse }) => { | ||
const scale = defaultClassificationColors[ramp] || ramp; | ||
const colorClasses = chroma.scale(scale).colors(intervals); | ||
return reverse ? [...colorClasses].reverse() : colorClasses; | ||
}; | ||
/** | ||
* Classify an array of features with quantile method | ||
* @param {object} features array of GeoJSON features | ||
* @param {object} params parameters to compute the classification | ||
* @param {string} params.attribute the name of the attribute to use for classification | ||
* @param {number} params.intervals number of expected classes | ||
* @param {string} params.ramp the identifier of the color ramp | ||
* @param {boolean} params.reverse reverse the ramp color classification | ||
* @returns {promise} return classification object | ||
*/ | ||
const quantile = (features, params) => getSimpleStatistics().then(({ quantileSorted }) => { | ||
const values = features.map(feature => feature?.properties?.[params.attribute]).filter(value => !isNil(value)).sort((a, b) => a - b); | ||
const intervals = params.intervals; | ||
const classes = [...[...new Array(intervals).keys()].map((n) => n / intervals), 1].map((p) => quantileSorted(values, p)); | ||
const colors = getColorClasses({ ...params, intervals }); | ||
return { | ||
data: { | ||
classification: classes.reduce((acc, min, idx) => { | ||
const max = classes[idx + 1]; | ||
if (max !== undefined) { | ||
const color = colors[idx]; | ||
return [ ...acc, { color, min, max }]; | ||
} | ||
return acc; | ||
}, []) | ||
} | ||
}; | ||
}); | ||
/** | ||
* Classify an array of features with jenks method | ||
* @param {object} features array of GeoJSON features | ||
* @param {object} params parameters to compute the classification | ||
* @param {string} params.attribute the name of the attribute to use for classification | ||
* @param {number} params.intervals number of expected classes | ||
* @param {string} params.ramp the identifier of the color ramp | ||
* @param {boolean} params.reverse reverse the ramp color classification | ||
* @returns {promise} return classification object | ||
*/ | ||
const jenks = (features, params) => getSimpleStatistics().then(({ jenks: jenksMethod }) => { | ||
const values = features.map(feature => feature?.properties?.[params.attribute]).filter(value => !isNil(value)).sort((a, b) => a - b); | ||
const paramIntervals = params.intervals; | ||
const intervals = paramIntervals > values.length ? values.length : paramIntervals; | ||
const classes = jenksMethod(values, intervals); | ||
const colors = getColorClasses({ ...params, intervals }); | ||
return { | ||
data: { | ||
classification: classes.reduce((acc, min, idx) => { | ||
const max = classes[idx + 1]; | ||
if (max !== undefined) { | ||
const color = colors[idx]; | ||
return [ ...acc, { color, min, max }]; | ||
} | ||
return acc; | ||
}, []) | ||
} | ||
}; | ||
}); | ||
/** | ||
* Classify an array of features with equal interval method | ||
* @param {object} features array of GeoJSON features | ||
* @param {object} params parameters to compute the classification | ||
* @param {string} params.attribute the name of the attribute to use for classification | ||
* @param {number} params.intervals number of expected classes | ||
* @param {string} params.ramp the identifier of the color ramp | ||
* @param {boolean} params.reverse reverse the ramp color classification | ||
* @returns {promise} return classification object | ||
*/ | ||
const equalInterval = (features, params) => getSimpleStatistics().then(({ equalIntervalBreaks }) => { | ||
const values = features.map(feature => feature?.properties?.[params.attribute]).filter(value => !isNil(value)).sort((a, b) => a - b); | ||
const classes = equalIntervalBreaks(values, params.intervals); | ||
const colors = getColorClasses(params); | ||
return { | ||
data: { | ||
classification: classes.reduce((acc, min, idx) => { | ||
const max = classes[idx + 1]; | ||
if (max !== undefined) { | ||
const color = colors[idx]; | ||
return [ ...acc, { color, min, max }]; | ||
} | ||
return acc; | ||
}, []) | ||
} | ||
}; | ||
}); | ||
/** | ||
* Classify an array of features with unique interval method | ||
* @param {object} features array of GeoJSON features | ||
* @param {object} params parameters to compute the classification | ||
* @param {string} params.attribute the name of the attribute to use for classification | ||
* @param {string} params.ramp the identifier of the color ramp | ||
* @param {boolean} params.reverse reverse the ramp color classification | ||
* @returns {promise} return classification object | ||
*/ | ||
const uniqueInterval = (features, params) => { | ||
const classes = uniq(features.map(feature => feature?.properties?.[params.attribute])).sort((a, b) => a > b ? 1 : -1); | ||
const colors = getColorClasses({ ...params, intervals: classes.length }); | ||
return Promise.resolve({ | ||
data: { | ||
classification: classes.map((value, idx) => { | ||
return { | ||
color: colors[idx], | ||
unique: value | ||
}; | ||
}) | ||
} | ||
}); | ||
}; | ||
|
||
const methods = { | ||
quantile, | ||
jenks, | ||
equalInterval, | ||
uniqueInterval | ||
}; | ||
/** | ||
* Classify a GeoJSON feature collection | ||
* @param {object} geojson a GeoJSON feature collection | ||
* @param {object} params parameters to compute the classification | ||
* @param {string} params.method classification methods, one of: `quantile`, `jenks`, `equalInterval` or `uniqueInterval` | ||
* @param {string} params.attribute the name of the attribute to use for classification | ||
* @param {number} params.intervals number of expected classes | ||
* @param {string} params.ramp the identifier of the color ramp | ||
* @param {boolean} params.reverse reverse the ramp color classification | ||
* @returns {promise} return classification object | ||
*/ | ||
export const classifyGeoJSON = (geojson, params) => { | ||
const features = geojson.type === 'FeatureCollection' | ||
? geojson.features | ||
: []; | ||
return methods[params.method](features, params); | ||
}; | ||
|
||
export const availableMethods = Object.keys(methods); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
/* | ||
* Copyright 2023, GeoSolutions Sas. | ||
* All rights reserved. | ||
* | ||
* This source code is licensed under the BSD-style license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
*/ | ||
|
||
import expect from 'expect'; | ||
import shuffle from 'lodash/shuffle'; | ||
import { classifyGeoJSON } from '../GeoJSONClassification'; | ||
|
||
describe('GeoJSONClassification APIs', () => { | ||
const geojson = { | ||
type: 'FeatureCollection', | ||
features: shuffle([...new Array(50).keys()]).map(value => ({ type: 'Feature', properties: { value, category: `category-${value % 2}` }, geometry: null })) | ||
}; | ||
it('classify GeoJSON with quantile method', (done) => { | ||
classifyGeoJSON(geojson, { attribute: 'value', method: 'quantile', ramp: 'viridis', intervals: 5 }) | ||
.then(({ data }) => { | ||
expect(data.classification).toEqual([ | ||
{ color: '#440154', min: 0, max: 9.5 }, | ||
{ color: '#3f4a8a', min: 9.5, max: 19.5 }, | ||
{ color: '#26838f', min: 19.5, max: 29.5 }, | ||
{ color: '#6cce5a', min: 29.5, max: 39.5 }, | ||
{ color: '#fee825', min: 39.5, max: 49 } | ||
]); | ||
done(); | ||
}) | ||
.catch(done); | ||
}); | ||
it('classify GeoJSON with jenks method', (done) => { | ||
classifyGeoJSON(geojson, { attribute: 'value', method: 'jenks', ramp: 'viridis', intervals: 5 }) | ||
.then(({ data }) => { | ||
expect(data.classification).toEqual([ | ||
{ color: '#440154', min: 0, max: 10 }, | ||
{ color: '#3f4a8a', min: 10, max: 20 }, | ||
{ color: '#26838f', min: 20, max: 30 }, | ||
{ color: '#6cce5a', min: 30, max: 40 }, | ||
{ color: '#fee825', min: 40, max: 49 } | ||
]); | ||
done(); | ||
}) | ||
.catch(done); | ||
}); | ||
it('classify GeoJSON with equalInterval method', (done) => { | ||
classifyGeoJSON(geojson, { attribute: 'value', method: 'equalInterval', ramp: 'viridis', intervals: 5 }) | ||
.then(({ data }) => { | ||
expect(data.classification).toEqual([ | ||
{ color: '#440154', min: 0, max: 9.8 }, | ||
{ color: '#3f4a8a', min: 9.8, max: 19.6 }, | ||
{ color: '#26838f', min: 19.6, max: 29.400000000000002 }, | ||
{ color: '#6cce5a', min: 29.400000000000002, max: 39.2 }, | ||
{ color: '#fee825', min: 39.2, max: 49 } | ||
]); | ||
done(); | ||
}) | ||
.catch(done); | ||
}); | ||
it('classify GeoJSON with uniqueInterval method', (done) => { | ||
classifyGeoJSON(geojson, { attribute: 'category', method: 'uniqueInterval', ramp: 'viridis' }) | ||
.then(({ data }) => { | ||
expect(data.classification).toEqual([ | ||
{ color: '#440154', unique: 'category-0' }, | ||
{ color: '#fee825', unique: 'category-1' } | ||
]); | ||
done(); | ||
}) | ||
.catch(done); | ||
}); | ||
it('classify GeoJSON with uniqueInterval method and reverse equal to true', (done) => { | ||
classifyGeoJSON(geojson, { attribute: 'category', method: 'uniqueInterval', ramp: 'viridis', reverse: true }) | ||
.then(({ data }) => { | ||
expect(data.classification).toEqual([ | ||
{ color: '#fee825', unique: 'category-0' }, | ||
{ color: '#440154', unique: 'category-1' } | ||
]); | ||
done(); | ||
}) | ||
.catch(done); | ||
}); | ||
}); |
Oops, something went wrong.