diff --git a/examples/earthquakes.jGIS b/examples/earthquakes.jGIS index 091a8a9d..48aea8e1 100644 --- a/examples/earthquakes.jGIS +++ b/examples/earthquakes.jGIS @@ -1,7 +1,7 @@ { "layerTree": [ "0959c04f-a841-4fa2-8b44-d262e89e4c9a", - "6dc9af9d-206d-42b5-9889-09758e9934b9" + "b116b76f-e040-4908-9098-a6fbea7ca5bc" ], "layers": { "0959c04f-a841-4fa2-8b44-d262e89e4c9a": { @@ -12,113 +12,45 @@ "type": "RasterLayer", "visible": true }, - "6dc9af9d-206d-42b5-9889-09758e9934b9": { - "filters": { - "appliedFilters": [], - "logicalOp": "any" - }, + "b116b76f-e040-4908-9098-a6fbea7ca5bc": { "name": "earthquakes", "parameters": { "color": { "circle-fill-color": [ - "interpolate", - [ - "linear" - ], - [ - "get", - "mag" - ], - 0.95, - [ - 8.0, - 29.0, - 88.0, - 1.0 - ], - 1.159230769230769, + "case", [ - 23.0, - 41.0, - 118.0, - 1.0 - ], - 1.3684615384615386, - [ - 37.0, - 52.0, - 148.0, - 1.0 - ], - 1.577692307692308, - [ - 34.0, - 94.0, - 168.0, - 1.0 - ], - 1.7869230769230768, - [ - 32.0, - 120.0, - 180.0, - 1.0 - ], - 1.9961538461538462, - [ - 29.0, - 145.0, - 192.0, - 1.0 - ], - 2.2053846153846157, - [ - 65.0, - 182.0, - 196.0, - 1.0 - ], - 2.4146153846153844, - [ - 96.0, - 194.0, - 192.0, - 1.0 + "==", + [ + "get", + "tsunami" + ], + 0.0 ], - 2.623846153846154, [ - 127.0, - 205.0, - 187.0, + 125.0, + 0.0, + 179.0, 1.0 ], - 2.863076923076923, [ - 199.0, - 233.0, - 180.0, + "==", + [ + "get", + "tsunami" + ], 1.0 ], - 3.1246153846153852, [ - 218.0, - 241.0, - 199.0, - 1.0 - ], - 3.491538461538462, - [ - 237.0, - 248.0, - 217.0, + 147.0, + 255.0, + 0.0, 1.0 ], - 4.326153846153848, [ - 255.0, - 255.0, - 217.0, - 1.0 + 0.0, + 0.0, + 0.0, + 0.0 ] ], "circle-radius": [ @@ -130,20 +62,33 @@ "get", "mag" ], + 1.0, + 1.0, 2.0, - 5.0, + 2.0, + 3.0, + 3.0, + 4.0, 4.0, - 10.0, + 5.0, + 5.0, 6.0, - 15.0 + 6.0 ], - "circle-stroke-color": "#986a44", + "circle-stroke-color": "#3399CC", "circle-stroke-line-cap": "round", "circle-stroke-line-join": "round", "circle-stroke-width": 1.25 }, "opacity": 1.0, - "source": "4a74edbc-1939-40e3-a0ac-28b2e1d87846", + "source": "dc048820-75cd-4b8d-a1fb-91642901cd82", + "symbologyState": { + "colorRamp": "cool", + "mode": "", + "nClasses": "", + "renderType": "Categorized", + "value": "tsunami" + }, "type": "circle" }, "type": "VectorLayer", @@ -153,10 +98,10 @@ "options": { "bearing": 0.0, "extent": [ - -14115404.754324596, - -3578744.7791191125, - -9601917.6529872, - 9131405.218514971 + -14291047.530673811, + -3536164.7121253638, + -9426274.876637986, + 9088825.15152122 ], "latitude": 24.187972965810673, "longitude": -106.52816608439294, @@ -165,20 +110,6 @@ "zoom": 3.8783091860507373 }, "sources": { - "4a74edbc-1939-40e3-a0ac-28b2e1d87846": { - "name": "Custom GeoJSON Layer Source", - "parameters": { - "path": "eq.json" - }, - "type": "GeoJSONSource" - }, - "4aab05ec-5a57-454b-9ba5-31bcf272feda": { - "name": "Custom GeoJSON Layer Source", - "parameters": { - "path": "france_regions.json" - }, - "type": "GeoJSONSource" - }, "a7ed9785-8797-4d6d-a6a9-062ce78ba7ba": { "name": "OpenStreetMap.Mapnik", "parameters": { @@ -190,6 +121,13 @@ "urlParameters": {} }, "type": "RasterSource" + }, + "dc048820-75cd-4b8d-a1fb-91642901cd82": { + "name": "Custom GeoJSON Layer Source", + "parameters": { + "path": "eq.json" + }, + "type": "GeoJSONSource" } } } diff --git a/examples/geotiff.jGIS b/examples/geotiff.jGIS index 4fdb4c21..7570f37c 100644 --- a/examples/geotiff.jGIS +++ b/examples/geotiff.jGIS @@ -31,44 +31,122 @@ 0.0, 0.0 ], - 0.1, + 0.0, + [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + 0.07142857142857144, + [ + 17.0, + 17.0, + 23.0, + 1.0 + ], + 0.14285714285714288, + [ + 34.0, + 34.0, + 46.0, + 1.0 + ], + 0.21428571428571427, + [ + 50.0, + 50.0, + 70.0, + 1.0 + ], + 0.28571428571428575, + [ + 67.0, + 67.0, + 93.0, + 1.0 + ], + 0.3571428571428571, [ - 53.0, - 132.0, - 228.0, + 84.0, + 84.0, + 116.0, 1.0 ], - 0.25, + 0.42857142857142855, [ - 248.0, - 228.0, - 92.0, + 98.0, + 103.0, + 130.0, 1.0 ], 0.5, [ - 255.0, - 190.0, - 111.0, + 112.0, + 123.0, + 144.0, + 1.0 + ], + 0.5714285714285714, + [ + 127.0, + 142.0, + 158.0, + 1.0 + ], + 0.6428571428571429, + [ + 141.0, + 161.0, + 172.0, + 1.0 + ], + 0.7142857142857142, + [ + 155.0, + 181.0, + 186.0, 1.0 ], - 0.75, + 0.7857142857142858, [ - 143.0, - 240.0, - 164.0, + 169.0, + 200.0, + 200.0, + 1.0 + ], + 0.8571428571428571, + [ + 198.0, + 218.0, + 218.0, + 1.0 + ], + 0.9285714285714286, + [ + 226.0, + 237.0, + 237.0, 1.0 ], 1.0, [ - 153.0, - 193.0, - 241.0, + 255.0, + 255.0, + 255.0, 1.0 ] ], "opacity": 1.0, - "source": "8b1d4258-5d46-48da-b466-496d376b593d" + "source": "8b1d4258-5d46-48da-b466-496d376b593d", + "symbologyState": { + "band": 1.0, + "colorRamp": "bone", + "interpolation": "linear", + "mode": "equal interval", + "nClasses": "15", + "renderType": "Singleband Pseudocolor" + } }, "type": "WebGlLayer", "visible": true @@ -77,16 +155,16 @@ "options": { "bearing": 0.0, "extent": [ - -14740045.41309709, - 2843576.998577497, - -8156951.805400478, - 5992856.553536485 + -13920582.07909406, + -1339375.3727731649, + -9144100.770363271, + 11083665.925811738 ], - "latitude": 36.849981896612846, - "longitude": -102.84361280909266, + "latitude": 40.042672545275906, + "longitude": -103.59678563518457, "pitch": 0.0, "projection": "EPSG:3857", - "zoom": 4.597387349849267 + "zoom": 3.901573011026123 }, "sources": { "8b1d4258-5d46-48da-b466-496d376b593d": { @@ -95,8 +173,8 @@ "normalize": true, "urls": [ { - "max": 3000.0, - "min": 1000.0, + "max": 25000.0, + "min": 2000.0, "url": "https://s2downloads.eox.at/demo/EOxCloudless/2020/rgbnir/s2cloudless2020-16bits_sinlge-file_z0-4.tif" } ], diff --git a/packages/base/src/classificationModes.ts b/packages/base/src/classificationModes.ts index 66b375d7..81f35d9a 100644 --- a/packages/base/src/classificationModes.ts +++ b/packages/base/src/classificationModes.ts @@ -1,7 +1,7 @@ // Adapted from https://github.com/qgis/QGIS/blob/master/src/core/classification/ import { Pool, fromUrl, TypedArray } from 'geotiff'; -import { InterpolationType } from './dialogs/components/symbology/SingleBandPseudoColor'; +import { InterpolationType } from './dialogs/symbology/tiff_layer/types/SingleBandPseudoColor'; export namespace VectorClassifications { export const calculateQuantileBreaks = ( diff --git a/packages/base/src/commands.ts b/packages/base/src/commands.ts index d676d8a5..46a54dea 100644 --- a/packages/base/src/commands.ts +++ b/packages/base/src/commands.ts @@ -17,7 +17,7 @@ import { ITranslator } from '@jupyterlab/translation'; import { CommandIDs, icons } from './constants'; import { CreationFormDialog } from './dialogs/formdialog'; import { LayerBrowserWidget } from './dialogs/layerBrowserDialog'; -import { SymbologyWidget } from './dialogs/symbologyDialog'; +import { SymbologyWidget } from './dialogs/symbology/symbologyDialog'; import { JupyterGISWidget } from './widget'; interface ICreateEntry { diff --git a/packages/base/src/dialogs/symbology/classificationModes.ts b/packages/base/src/dialogs/symbology/classificationModes.ts new file mode 100644 index 00000000..dea63d92 --- /dev/null +++ b/packages/base/src/dialogs/symbology/classificationModes.ts @@ -0,0 +1,436 @@ +// Adapted from https://github.com/qgis/QGIS/blob/master/src/core/classification/ + +import { Pool, fromUrl, TypedArray } from 'geotiff'; +import { InterpolationType } from './tiff_layer/types/SingleBandPseudoColor'; + +export namespace VectorClassifications { + export const calculateQuantileBreaks = ( + values: number[], + nClasses: number + ) => { + // q-th quantile of a data set: + // value where q fraction of data is below and (1-q) fraction is above this value + // Xq = (1 - r) * X_NI1 + r * X_NI2 + // NI1 = (int) (q * (n+1)) + // NI2 = NI1 + 1 + // r = q * (n+1) - (int) (q * (n+1)) + // (indices of X: 1...n) + + const sortedValues = [...values].sort((a, b) => a - b); + + const breaks = []; + + if (!sortedValues) { + return []; + } + + const n = sortedValues.length; + + let xq: number = n > 0 ? sortedValues[0] : 0; + + for (let i = 1; i < nClasses; i++) { + if (n > 1) { + const q = i / nClasses; + const a = q * (n - 1); + const aa = Math.floor(a); + + const r = a - aa; + xq = (1 - r) * sortedValues[aa] + r * sortedValues[aa + 1]; + } + breaks.push(xq); + } + + breaks.push(sortedValues[n - 1]); + + return breaks; + }; + + export const calculateEqualIntervalBreaks = ( + values: number[], + nClasses: number + ) => { + const minimum = Math.min(...values); + const maximum = Math.max(...values); + + const breaks: number[] = []; + const step = (maximum - minimum) / nClasses; + + let value = minimum; + + for (let i = 0; i < nClasses; i++) { + value += step; + breaks.push(value); + } + + breaks[nClasses - 1] = maximum; + + return breaks; + }; + + export const calculateJenksBreaks = (values: number[], nClasses: number) => { + const maximum = Math.max(...values); + + if (values.length === 0) { + return []; + } + + if (nClasses <= 1) { + return [maximum]; + } + + if (nClasses >= values.length) { + return values; + } + + const sample = [...values].sort((a, b) => a - b); + const n = sample.length; + + const matrixOne = Array.from({ length: n + 1 }, () => + Array(nClasses + 1).fill(0) + ); + const matrixTwo = Array.from({ length: n + 1 }, () => + Array(nClasses + 1).fill(Number.MAX_VALUE) + ); + + for (let i = 1; i <= nClasses; i++) { + matrixOne[0][i] = 1; + matrixOne[1][i] = 1; + matrixTwo[0][i] = 0.0; + + for (let j = 2; j <= n; j++) { + matrixTwo[j][i] = Number.MAX_VALUE; + } + } + + for (let l = 2; l <= n; l++) { + let s1 = 0.0; + let s2 = 0.0; + let w = 0; + let v = 0.0; + + for (let m = 1; m <= l; m++) { + const i3 = l - m + 1; + + const val = sample[i3 - 1]; + + s2 += val * val; + s1 += val; + w++; + + v = s2 - (s1 * s1) / w; + const i4 = i3 - 1; + if (i4 !== 0) { + for (let j = 2; j <= nClasses; j++) { + if (matrixTwo[l][j] >= v + matrixTwo[i4][j - 1]) { + matrixOne[l][j] = i4; + matrixTwo[l][j] = v + matrixTwo[i4][j - 1]; + } + } + } + } + matrixOne[l][1] = 1; + matrixTwo[l][1] = v; + } + + const breaks = Array(nClasses); + breaks[nClasses - 1] = sample[n - 1]; + + for (let j = nClasses, k = n; j >= 2; j--) { + const id = matrixOne[k][j] - 1; + breaks[j - 2] = sample[id]; + k = matrixOne[k][j] - 1; + } + + return breaks; + }; + + export const calculatePrettyBreaks = (values: number[], nClasses: number) => { + const minimum = Math.min(...values); + const maximum = Math.max(...values); + + const breaks = []; + + if (nClasses < 1) { + breaks.push(maximum); + return breaks; + } + + const minimumCount = Math.floor(nClasses / 3); + const shrink = 0.75; + const highBias = 1.5; + const adjustBias = 0.5 + 1.5 * highBias; + const divisions = nClasses; + const h = highBias; + let cell; + let small = false; + const dx = maximum - minimum; + + let U; + cell = Math.max(Math.abs(minimum), Math.abs(maximum)); + if (adjustBias >= 1.5 * h + 0.5) { + U = 1 + 1.0 / (1 + h); + } else { + U = 1 + 1.5 / (1 + adjustBias); + } + small = dx < cell * U * Math.max(1, divisions) * 1e-7 * 3.0; + + if (small) { + if (cell > 10) { + cell = 9 + cell / 10; + cell = cell * shrink; + } + if (minimumCount > 1) { + cell = cell / minimumCount; + } + } else { + cell = dx; + if (divisions > 1) { + cell = cell / divisions; + } + } + if (cell < 20 * 1e-7) { + cell = 20 * 1e-7; + } + + const base = Math.pow(10.0, Math.floor(Math.log10(cell))); + let unit = base; + if (2 * base - cell < h * (cell - unit)) { + unit = 2.0 * base; + if (5 * base - cell < adjustBias * (cell - unit)) { + unit = 5.0 * base; + if (10.0 * base - cell < h * (cell - unit)) { + unit = 10.0 * base; + } + } + } + + let start = Math.floor(minimum / unit + 1e-7); + let end = Math.ceil(maximum / unit - 1e-7); + + while (start * unit > minimum + 1e-7 * unit) { + start = start - 1; + } + while (end * unit < maximum - 1e-7 * unit) { + end = end + 1; + } + + let k = Math.floor(0.5 + end - start); + if (k < minimumCount) { + k = minimumCount - k; + if (start >= 0) { + end = end + k / 2; + start = start - k / 2 + (k % 2); + } else { + start = start - k / 2; + end = end + k / 2 + (k % 2); + } + } + + const minimumBreak = start * unit; + const count = end - start; + + for (let i = 1; i < count + 1; i++) { + breaks.push(minimumBreak + i * unit); + } + + if (breaks.length === 0) { + return breaks; + } + + if (breaks[0] < minimum) { + breaks[0] = minimum; + } + if (breaks[breaks.length - 1] > maximum) { + breaks[breaks.length - 1] = maximum; + } + + if (minimum < 0.0 && maximum > 0.0) { + const breaksMinusZero = breaks.map(b => b - 0.0); + + let posOfMin = 0; + for (let i = 1; i < breaks.length; i++) { + if ( + Math.abs(breaksMinusZero[i]) < Math.abs(breaksMinusZero[posOfMin]) + ) { + posOfMin = i; + } + } + + breaks[posOfMin] = 0.0; // Set the closest break to zero + } + + return breaks; + }; + + export const calculateLogarithmicBreaks = ( + values: number[], + nClasses: number + ) => { + const minimum = Math.min(...values); + const maximum = Math.max(...values); + + let positiveMinimum = Number.MAX_VALUE; + + let breaks = []; + + positiveMinimum = minimum; + + const actualLogMin = Math.log10(positiveMinimum); + let logMin = Math.floor(actualLogMin); + const logMax = Math.ceil(Math.log10(maximum)); + + let prettyBreaks = calculatePrettyBreaks([logMin, logMax], nClasses); + + while (prettyBreaks.length > 0 && prettyBreaks[0] < actualLogMin) { + logMin += 1.0; + prettyBreaks = calculatePrettyBreaks([logMin, logMax], nClasses); + } + + breaks = prettyBreaks; + + for (let i = 0; i < breaks.length; i++) { + breaks[i] = Math.pow(10, breaks[i]); + } + + return breaks; + }; +} + +export namespace GeoTiffClassifications { + export const classifyQuantileBreaks = async ( + nClasses: number, + bandNumber: number, + url: string, + colorRampType: string + ) => { + const breaks: number[] = []; + const isDiscrete = colorRampType === 'discrete'; + + const pool = new Pool(); + const tiff = await fromUrl(url); + const image = await tiff.getImage(); + const values = await image.readRasters({ pool }); + + // Band numbers are 1 indexed + const bandValues = values[bandNumber - 1] as TypedArray; + + const bandSortedValues = bandValues + .filter(value => value !== 0) + .sort((a, b) => a - b); + + pool.destroy(); + + if (!bandSortedValues) { + return []; + } + + // Adapted from https://github.com/GeoTIFF/geoblaze/blob/master/src/histogram/histogram.core.js#L64 + // iterate through values and use a counter to + // decide when to set up the next bin. + let numValuesInCurrentBin; + let valuesPerBin; + let startIndex; + + if (isDiscrete) { + valuesPerBin = bandSortedValues.length / nClasses; + numValuesInCurrentBin = 0; + startIndex = 0; + } else { + valuesPerBin = bandSortedValues.length / (nClasses - 1); + breaks.push(1); + numValuesInCurrentBin = 1; + startIndex = 1; + } + + for (let i = startIndex; i < bandSortedValues.length; i++) { + if (numValuesInCurrentBin + 1 < valuesPerBin) { + numValuesInCurrentBin++; + } else { + breaks.push(bandSortedValues[i] as number); + numValuesInCurrentBin = 0; + } + } + + if (breaks.length !== nClasses) { + //TODO: This should be set based on the type of bandSortedValues I think + breaks.push(65535); + } + + return breaks; + }; + + export const classifyContinuousBreaks = ( + nClasses: number, + minimumValue: number, + maximumValue: number, + colorRampType: InterpolationType + ) => { + const min = minimumValue; + const max = maximumValue; + + if (min > max) { + return []; + } + + const isDiscrete = colorRampType === 'discrete'; + + const breaks: number[] = []; + + const numberOfEntries = nClasses; + if (isDiscrete) { + const intervalDiff = + ((max - min) * (numberOfEntries - 1)) / numberOfEntries; + + for (let i = 1; i < numberOfEntries; i++) { + const val = i / numberOfEntries; + breaks.push(min + val * intervalDiff); + } + breaks.push(max); + } else { + for (let i = 0; i <= numberOfEntries; i++) { + if (i === 26) { + continue; + } + const val = i / numberOfEntries; + breaks.push(min + val * (max - min)); + } + } + + return breaks; + }; + + export const classifyEqualIntervalBreaks = ( + nClasses: number, + minimumValue: number, + maximumValue: number, + colorRampType: InterpolationType + ) => { + const min = minimumValue; + const max = maximumValue; + + if (min > max) { + return []; + } + + const isDiscrete = colorRampType === 'discrete'; + + const breaks: number[] = []; + + if (isDiscrete) { + const intervalDiff = (max - min) / nClasses; + + for (let i = 1; i < nClasses; i++) { + breaks.push(min + i * intervalDiff); + } + breaks.push(max); + } else { + const intervalDiff = (max - min) / (nClasses - 1); + + for (let i = 0; i < nClasses; i++) { + breaks.push(min + i * intervalDiff); + } + } + + return breaks; + }; +} diff --git a/packages/base/src/dialogs/components/symbology/CanvasSelectComponent.tsx b/packages/base/src/dialogs/symbology/components/color_ramp/CanvasSelectComponent.tsx similarity index 100% rename from packages/base/src/dialogs/components/symbology/CanvasSelectComponent.tsx rename to packages/base/src/dialogs/symbology/components/color_ramp/CanvasSelectComponent.tsx diff --git a/packages/base/src/dialogs/components/symbology/ColorRamp.tsx b/packages/base/src/dialogs/symbology/components/color_ramp/ColorRamp.tsx similarity index 70% rename from packages/base/src/dialogs/components/symbology/ColorRamp.tsx rename to packages/base/src/dialogs/symbology/components/color_ramp/ColorRamp.tsx index ddd82cfa..6b2e65a6 100644 --- a/packages/base/src/dialogs/components/symbology/ColorRamp.tsx +++ b/packages/base/src/dialogs/symbology/components/color_ramp/ColorRamp.tsx @@ -3,6 +3,7 @@ import React, { useEffect, useState } from 'react'; import CanvasSelectComponent from './CanvasSelectComponent'; import { faSpinner } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import ModeSelectRow from './ModeSelectRow'; import { IDict } from '@jupytergis/schema'; interface IColorRampProps { @@ -14,6 +15,7 @@ interface IColorRampProps { selectedRamp: string, setIsLoading: (isLoading: boolean) => void ) => void; + showModeRow: boolean; } export type ColorRampOptions = { @@ -25,7 +27,8 @@ export type ColorRampOptions = { const ColorRamp = ({ layerParams, modeOptions, - classifyFunc + classifyFunc, + showModeRow }: IColorRampProps) => { const [selectedRamp, setSelectedRamp] = useState(''); const [selectedMode, setSelectedMode] = useState(''); @@ -44,7 +47,6 @@ const ColorRamp = ({ singleBandMode = layerParams.symbologyState.mode; colorRamp = layerParams.symbologyState.colorRamp; } - setNumberOfShades(nClasses ? nClasses : '9'); setSelectedMode(singleBandMode ? singleBandMode : 'equal interval'); setSelectedRamp(colorRamp ? colorRamp : 'cool'); @@ -59,36 +61,15 @@ const ColorRamp = ({ setSelected={setSelectedRamp} /> -
-
- - setNumberOfShades(event.target.value)} - /> -
-
- - -
-
+ {showModeRow && ( + + )} {isLoading ? ( ) : ( diff --git a/packages/base/src/dialogs/components/symbology/ColorRampEntry.tsx b/packages/base/src/dialogs/symbology/components/color_ramp/ColorRampEntry.tsx similarity index 100% rename from packages/base/src/dialogs/components/symbology/ColorRampEntry.tsx rename to packages/base/src/dialogs/symbology/components/color_ramp/ColorRampEntry.tsx diff --git a/packages/base/src/dialogs/symbology/components/color_ramp/ModeSelectRow.tsx b/packages/base/src/dialogs/symbology/components/color_ramp/ModeSelectRow.tsx new file mode 100644 index 00000000..a1926368 --- /dev/null +++ b/packages/base/src/dialogs/symbology/components/color_ramp/ModeSelectRow.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +interface IModeSelectRowProps { + numberOfShades: string; + setNumberOfShades: (value: string) => void; + selectedMode: string; + setSelectedMode: (value: string) => void; + modeOptions: string[]; +} +const ModeSelectRow = ({ + numberOfShades, + setNumberOfShades, + selectedMode, + setSelectedMode, + modeOptions +}: IModeSelectRowProps) => { + return ( +
+
+ + setNumberOfShades(event.target.value)} + disabled={selectedMode === 'continuous'} + /> +
+
+ + +
+
+ ); +}; + +export default ModeSelectRow; diff --git a/packages/base/src/dialogs/symbology/components/color_stops/StopContainer.tsx b/packages/base/src/dialogs/symbology/components/color_stops/StopContainer.tsx new file mode 100644 index 00000000..660a7d9e --- /dev/null +++ b/packages/base/src/dialogs/symbology/components/color_stops/StopContainer.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import { Button } from '@jupyterlab/ui-components'; +import { IStopRow } from '../../symbologyDialog'; +import StopRow from './StopRow'; + +interface IStopContainerProps { + selectedMethod: string; + stopRows: IStopRow[]; + setStopRows: (stops: IStopRow[]) => void; +} + +const StopContainer = ({ + selectedMethod, + stopRows, + setStopRows +}: IStopContainerProps) => { + const addStopRow = () => { + setStopRows([ + { + stop: 0, + output: [0, 0, 0, 1] + }, + ...stopRows + ]); + }; + + const deleteStopRow = (index: number) => { + const newFilters = [...stopRows]; + newFilters.splice(index, 1); + + setStopRows(newFilters); + }; + + return ( + <> +
+
+ Value + Output Value +
+ {stopRows.map((stop, index) => ( + deleteStopRow(index)} + useNumber={selectedMethod === 'radius' ? true : false} + /> + ))} +
+
+ +
+ + ); +}; + +export default StopContainer; diff --git a/packages/base/src/dialogs/components/symbology/StopRow.tsx b/packages/base/src/dialogs/symbology/components/color_stops/StopRow.tsx similarity index 100% rename from packages/base/src/dialogs/components/symbology/StopRow.tsx rename to packages/base/src/dialogs/symbology/components/color_stops/StopRow.tsx diff --git a/packages/base/src/dialogs/symbology/hooks/useGetProperties.ts b/packages/base/src/dialogs/symbology/hooks/useGetProperties.ts new file mode 100644 index 00000000..8bf21d78 --- /dev/null +++ b/packages/base/src/dialogs/symbology/hooks/useGetProperties.ts @@ -0,0 +1,72 @@ +// import { GeoJSONFeature } from 'geojson'; + +import { GeoJSONFeature1, IJupyterGISModel } from '@jupytergis/schema'; +import { useEffect, useState } from 'react'; + +interface IUseGetPropertiesProps { + layerId?: string; + model: IJupyterGISModel; +} + +interface IUseGetPropertiesResult { + featureProps: Record>; + isLoading: boolean; + error?: Error; +} + +export const useGetProperties = ({ + layerId, + model +}: IUseGetPropertiesProps): IUseGetPropertiesResult => { + const [featureProps, setFeatureProps] = useState({}); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(undefined); + + const getProperties = async () => { + if (!layerId) { + return; + } + + try { + const layer = model.getLayer(layerId); + const source = model.getSource(layer?.parameters?.source); + + if (!source) { + throw new Error('Source not found'); + } + + const data = await model.readGeoJSON(source.parameters?.path); + + if (!data) { + throw new Error('Failed to read GeoJSON data'); + } + + const result: Record> = {}; + + data.features.forEach((feature: GeoJSONFeature1) => { + if (feature.properties) { + Object.entries(feature.properties).forEach(([key, value]) => { + if (typeof value !== 'string') { + if (!(key in result)) { + result[key] = new Set(); + } + result[key].add(value); + } + }); + } + }); + + setFeatureProps(result); + setIsLoading(false); + } catch (err) { + setError(err as Error); + setIsLoading(false); + } + }; + + useEffect(() => { + getProperties(); + }, [model, layerId]); + + return { featureProps, isLoading, error }; +}; diff --git a/packages/base/src/dialogs/symbologyDialog.tsx b/packages/base/src/dialogs/symbology/symbologyDialog.tsx similarity index 95% rename from packages/base/src/dialogs/symbologyDialog.tsx rename to packages/base/src/dialogs/symbology/symbologyDialog.tsx index 7bf54dba..0dac696f 100644 --- a/packages/base/src/dialogs/symbologyDialog.tsx +++ b/packages/base/src/dialogs/symbology/symbologyDialog.tsx @@ -5,8 +5,8 @@ import { IStateDB } from '@jupyterlab/statedb'; import { PromiseDelegate } from '@lumino/coreutils'; import { Signal } from '@lumino/signaling'; import React, { useEffect, useState } from 'react'; -import BandRendering from './components/symbology/BandRendering'; -import VectorRendering from './components/symbology/VectorRendering'; +import TiffRendering from './tiff_layer/TiffRendering'; +import VectorRendering from './vector_layer/VectorRendering'; export interface ISymbologyDialogProps { context: DocumentRegistry.IContext; @@ -87,7 +87,7 @@ const SymbologyDialog = ({ break; case 'WebGlLayer': LayerSymbology = ( - { + // This it to parse a color object on the layer + if (!layer.parameters?.color) { + return []; + } + + const color = layer.parameters.color; + + // If color is a string we don't need to parse + if (typeof color === 'string') { + return []; + } + + const prefix = layer.parameters.type === 'circle' ? 'circle-' : ''; + + if (!color[`${prefix}fill-color`]) { + return []; + } + + const valueColorPairs: IStopRow[] = []; + + // So if it's not a string then it's an array and we parse + // Color[0] is the operator used for the color expression + switch (color[`${prefix}fill-color`][0]) { + case 'interpolate': + // First element is interpolate for linear selection + // Second element is type of interpolation (ie linear) + // Third is input value that stop values are compared with + // Fourth and on is value:color pairs + for (let i = 3; i < color[`${prefix}fill-color`].length; i += 2) { + const obj: IStopRow = { + stop: color[`${prefix}fill-color`][i], + output: color[`${prefix}fill-color`][i + 1] + }; + valueColorPairs.push(obj); + } + break; + case 'case': + for (let i = 1; i < color[`${prefix}fill-color`].length - 1; i += 2) { + const obj: IStopRow = { + stop: color[`${prefix}fill-color`][i][2], + output: color[`${prefix}fill-color`][i + 1] + }; + valueColorPairs.push(obj); + } + break; + } + + return valueColorPairs; + }; + + export const buildRadiusInfo = (layer: IJGISLayer) => { + if (!layer.parameters?.color) { + return []; + } + + const color = layer.parameters.color; + + // If color is a string we don't need to parse + if (typeof color === 'string') { + return []; + } + + const stopOutputPairs: IStopRow[] = []; + + for (let i = 3; i < color['circle-radius'].length; i += 2) { + const obj: IStopRow = { + stop: color['circle-radius'][i], + output: color['circle-radius'][i + 1] + }; + stopOutputPairs.push(obj); + } + + return stopOutputPairs; + }; +} + +export namespace Utils { + export const getValueColorPairs = ( + stops: number[], + selectedRamp: string, + nClasses: number + ) => { + let colorMap = colormap({ + colormap: selectedRamp, + nshades: nClasses > 9 ? nClasses : 9, + format: 'rgba' + }); + + const valueColorPairs: IStopRow[] = []; + + // colormap requires 9 classes to generate the ramp + // so we do some tomfoolery to make it work with less than 9 stops + if (nClasses < 9) { + const midIndex = Math.floor(nClasses / 2); + + // Get the first n/2 elements from the second array + const firstPart = colorMap.slice(0, midIndex); + + // Get the last n/2 elements from the second array + const secondPart = colorMap.slice( + colorMap.length - (stops.length - firstPart.length) + ); + + // Create the new array by combining the first and last parts + colorMap = firstPart.concat(secondPart); + } + + for (let i = 0; i < nClasses; i++) { + valueColorPairs.push({ stop: stops[i], output: colorMap[i] }); + } + + return valueColorPairs; + }; +} diff --git a/packages/base/src/dialogs/components/symbology/BandRendering.tsx b/packages/base/src/dialogs/symbology/tiff_layer/TiffRendering.tsx similarity index 89% rename from packages/base/src/dialogs/components/symbology/BandRendering.tsx rename to packages/base/src/dialogs/symbology/tiff_layer/TiffRendering.tsx index 4bf0ff81..9b3be7cf 100644 --- a/packages/base/src/dialogs/components/symbology/BandRendering.tsx +++ b/packages/base/src/dialogs/symbology/tiff_layer/TiffRendering.tsx @@ -1,8 +1,8 @@ import React, { useEffect, useState } from 'react'; -import { ISymbologyDialogProps } from '../../symbologyDialog'; -import SingleBandPseudoColor from './SingleBandPseudoColor'; +import { ISymbologyDialogProps } from '../symbologyDialog'; +import SingleBandPseudoColor from './types/SingleBandPseudoColor'; -const BandRendering = ({ +const TiffRendering = ({ context, state, okSignalPromise, @@ -64,4 +64,4 @@ const BandRendering = ({ ); }; -export default BandRendering; +export default TiffRendering; diff --git a/packages/base/src/dialogs/components/symbology/BandRow.tsx b/packages/base/src/dialogs/symbology/tiff_layer/components/BandRow.tsx similarity index 97% rename from packages/base/src/dialogs/components/symbology/BandRow.tsx rename to packages/base/src/dialogs/symbology/tiff_layer/components/BandRow.tsx index c887c58a..50aeef8b 100644 --- a/packages/base/src/dialogs/components/symbology/BandRow.tsx +++ b/packages/base/src/dialogs/symbology/tiff_layer/components/BandRow.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { IBandRow } from './SingleBandPseudoColor'; +import { IBandRow } from '../types/SingleBandPseudoColor'; const BandRow = ({ index, diff --git a/packages/base/src/dialogs/components/symbology/SingleBandPseudoColor.tsx b/packages/base/src/dialogs/symbology/tiff_layer/types/SingleBandPseudoColor.tsx similarity index 94% rename from packages/base/src/dialogs/components/symbology/SingleBandPseudoColor.tsx rename to packages/base/src/dialogs/symbology/tiff_layer/types/SingleBandPseudoColor.tsx index 80fbd6a6..73b58c19 100644 --- a/packages/base/src/dialogs/components/symbology/SingleBandPseudoColor.tsx +++ b/packages/base/src/dialogs/symbology/tiff_layer/types/SingleBandPseudoColor.tsx @@ -1,17 +1,19 @@ import { IDict, IWebGlLayer } from '@jupytergis/schema'; import { Button } from '@jupyterlab/ui-components'; import { ReadonlyJSONObject } from '@lumino/coreutils'; -import colormap from 'colormap'; import { ExpressionValue } from 'ol/expr/expression'; import React, { useEffect, useRef, useState } from 'react'; -import { GeoTiffClassifications } from '../../../classificationModes'; -import { GlobalStateDbManager } from '../../../store'; +import { GeoTiffClassifications } from '../../classificationModes'; +import { GlobalStateDbManager } from '../../../../store'; import { IStopRow, ISymbologyDialogProps } from '../../symbologyDialog'; -import BandRow from './BandRow'; -import ColorRamp, { ColorRampOptions } from './ColorRamp'; -import StopRow from './StopRow'; -import { getGdal } from '../../../gdal'; -import { Spinner } from '../../../mainview/spinner'; +import BandRow from '../components/BandRow'; +import ColorRamp, { + ColorRampOptions +} from '../../components/color_ramp/ColorRamp'; +import StopRow from '../../components/color_stops/StopRow'; +import { Utils } from '../../symbologyUtils'; +import { getGdal } from '../../../../gdal'; +import { Spinner } from '../../../../mainview/spinner'; export interface IBandRow { band: number; @@ -120,13 +122,8 @@ const SingleBandPseudoColor = ({ setLayerState(layerState); const layerParams = layer.parameters as IWebGlLayer; - const band = layerParams.symbologyState?.band - ? layerParams.symbologyState.band - : 1; - - const interpolation = layerParams.symbologyState?.interpolation - ? layerParams.symbologyState.interpolation - : 'linear'; + const band = layerParams.symbologyState?.band ?? 1; + const interpolation = layerParams.symbologyState?.interpolation ?? 'linear'; setSelectedBand(band); setSelectedFunction(interpolation); @@ -235,7 +232,7 @@ const SingleBandPseudoColor = ({ setStopRows(valueColorPairs); }; - const handleOk = async () => { + const handleOk = () => { // Update source const bandRow = bandRowsRef.current[selectedBand - 1]; if (!bandRow) { @@ -377,17 +374,6 @@ const SingleBandPseudoColor = ({ const source = context.model.getSource(layer?.parameters?.source); const sourceInfo = source?.parameters?.urls[0]; const nClasses = selectedMode === 'continuous' ? 52 : +numberOfShades; - const colorMap = colormap({ - colormap: selectedRamp, - nshades: nClasses, - format: 'rgba' - }); - - if (!sourceInfo.url) { - return; - } - - const valueColorPairs: IStopRow[] = []; setIsLoading(true); switch (selectedMode) { @@ -421,9 +407,11 @@ const SingleBandPseudoColor = ({ } setIsLoading(false); - for (let i = 0; i < stops.length; i++) { - valueColorPairs.push({ stop: stops[i], output: colorMap[i] }); - } + const valueColorPairs = Utils.getValueColorPairs( + stops, + selectedRamp, + nClasses + ); setStopRows(valueColorPairs); }; @@ -496,6 +484,7 @@ const SingleBandPseudoColor = ({ layerParams={layer.parameters} modeOptions={modeOptions} classifyFunc={buildColorInfoFromClassification} + showModeRow={true} /> )}
diff --git a/packages/base/src/dialogs/components/symbology/VectorRendering.tsx b/packages/base/src/dialogs/symbology/vector_layer/VectorRendering.tsx similarity index 77% rename from packages/base/src/dialogs/components/symbology/VectorRendering.tsx rename to packages/base/src/dialogs/symbology/vector_layer/VectorRendering.tsx index 21936307..af544197 100644 --- a/packages/base/src/dialogs/components/symbology/VectorRendering.tsx +++ b/packages/base/src/dialogs/symbology/vector_layer/VectorRendering.tsx @@ -1,7 +1,8 @@ import React, { useEffect, useState } from 'react'; -import { ISymbologyDialogProps } from '../../symbologyDialog'; -import Graduated from './Graduated'; -import SimpleSymbol from './SimpleSymbol'; +import { ISymbologyDialogProps } from '../symbologyDialog'; +import Graduated from './types/Graduated'; +import SimpleSymbol from './types/SimpleSymbol'; +import Categorized from './types/Categorized'; const VectorRendering = ({ context, @@ -27,11 +28,11 @@ const VectorRendering = ({ } useEffect(() => { - const renderType = layer.parameters?.symbologyState.renderType; + const renderType = layer.parameters?.symbologyState?.renderType; setSelectedRenderType(renderType ?? 'Single Symbol'); if (layer.type === 'VectorLayer') { - const options = ['Single Symbol', 'Graduated']; + const options = ['Single Symbol', 'Graduated', 'Categorized']; setRenderTypeOptions(options); } }, []); @@ -60,6 +61,17 @@ const VectorRendering = ({ /> ); break; + case 'Categorized': + RenderComponent = ( + + ); + break; default: RenderComponent =
Render Type Not Implemented (yet)
; } diff --git a/packages/base/src/dialogs/symbology/vector_layer/components/ValueSelect.tsx b/packages/base/src/dialogs/symbology/vector_layer/components/ValueSelect.tsx new file mode 100644 index 00000000..abd0c437 --- /dev/null +++ b/packages/base/src/dialogs/symbology/vector_layer/components/ValueSelect.tsx @@ -0,0 +1,37 @@ +import React from 'react'; + +interface IValueSelectProps { + featureProperties: any; + selectedValue: string; + setSelectedValue: (value: string) => void; +} + +const ValueSelect = ({ + featureProperties, + selectedValue, + setSelectedValue +}: IValueSelectProps) => { + return ( +
+ + +
+ ); +}; + +export default ValueSelect; diff --git a/packages/base/src/dialogs/symbology/vector_layer/types/Categorized.tsx b/packages/base/src/dialogs/symbology/vector_layer/types/Categorized.tsx new file mode 100644 index 00000000..2da45411 --- /dev/null +++ b/packages/base/src/dialogs/symbology/vector_layer/types/Categorized.tsx @@ -0,0 +1,156 @@ +import React, { useEffect, useRef, useState } from 'react'; +import ValueSelect from '../components/ValueSelect'; +import { IStopRow, ISymbologyDialogProps } from '../../symbologyDialog'; +import { useGetProperties } from '../../hooks/useGetProperties'; +import StopContainer from '../../components/color_stops/StopContainer'; +import { Utils, VectorUtils } from '../../symbologyUtils'; +import ColorRamp from '../../components/color_ramp/ColorRamp'; +import { ReadonlyJSONObject } from '@lumino/coreutils'; +import { ExpressionValue } from 'ol/expr/expression'; +import { IVectorLayer } from '@jupytergis/schema'; + +const Categorized = ({ + context, + state, + okSignalPromise, + cancel, + layerId +}: ISymbologyDialogProps) => { + const selectedValueRef = useRef(); + const stopRowsRef = useRef(); + const colorRampOptionsRef = useRef(); + + const [selectedValue, setSelectedValue] = useState(''); + const [stopRows, setStopRows] = useState([]); + const [colorRampOptions, setColorRampOptions] = useState< + ReadonlyJSONObject | undefined + >(); + + if (!layerId) { + return; + } + const layer = context.model.getLayer(layerId); + if (!layer?.parameters) { + return; + } + const { featureProps } = useGetProperties({ + layerId, + model: context.model + }); + + useEffect(() => { + const valueColorPairs = VectorUtils.buildColorInfo(layer); + + setStopRows(valueColorPairs); + + okSignalPromise.promise.then(okSignal => { + okSignal.connect(handleOk, this); + }); + + return () => { + okSignalPromise.promise.then(okSignal => { + okSignal.disconnect(handleOk, this); + }); + }; + }, []); + + useEffect(() => { + populateOptions(); + }, [featureProps]); + + useEffect(() => { + selectedValueRef.current = selectedValue; + stopRowsRef.current = stopRows; + colorRampOptionsRef.current = colorRampOptions; + }, [selectedValue, stopRows, colorRampOptions]); + + const populateOptions = async () => { + const layerParams = layer.parameters as IVectorLayer; + const value = + layerParams.symbologyState?.value ?? Object.keys(featureProps)[0]; + + setSelectedValue(value); + }; + + const buildColorInfoFromClassification = ( + selectedMode: string, + numberOfShades: string, + selectedRamp: string, + setIsLoading: (isLoading: boolean) => void + ) => { + setColorRampOptions({ + selectedFunction: '', + selectedRamp, + numberOfShades: '', + selectedMode: '' + }); + + const stops = Array.from(featureProps[selectedValue]).sort((a, b) => a - b); + + const valueColorPairs = Utils.getValueColorPairs( + stops, + selectedRamp, + stops.length + ); + + setStopRows(valueColorPairs); + }; + + const handleOk = () => { + if (!layer.parameters) { + return; + } + + const colorExpr: ExpressionValue[] = []; + colorExpr.push('case'); + + stopRowsRef.current?.map(stop => { + colorExpr.push(['==', ['get', selectedValueRef.current], stop.stop]); + colorExpr.push(stop.output); + }); + + // fallback value + colorExpr.push([0, 0, 0, 0.0]); + + const newStyle = { ...layer.parameters.color }; + newStyle['circle-fill-color'] = colorExpr; + + const symbologyState = { + renderType: 'Categorized', + value: selectedValueRef.current, + colorRamp: colorRampOptionsRef.current?.selectedRamp, + nClasses: colorRampOptionsRef.current?.numberOfShades, + mode: colorRampOptionsRef.current?.selectedMode + }; + + layer.parameters.symbologyState = symbologyState; + layer.parameters.color = newStyle; + + context.model.sharedModel.updateLayer(layerId, layer); + cancel(); + }; + + return ( +
+ + + + +
+ ); +}; + +export default Categorized; diff --git a/packages/base/src/dialogs/components/symbology/Graduated.tsx b/packages/base/src/dialogs/symbology/vector_layer/types/Graduated.tsx similarity index 52% rename from packages/base/src/dialogs/components/symbology/Graduated.tsx rename to packages/base/src/dialogs/symbology/vector_layer/types/Graduated.tsx index 86eac81d..6d38728b 100644 --- a/packages/base/src/dialogs/components/symbology/Graduated.tsx +++ b/packages/base/src/dialogs/symbology/vector_layer/types/Graduated.tsx @@ -1,12 +1,15 @@ -import { GeoJSONFeature1, IVectorLayer } from '@jupytergis/schema'; -import { Button } from '@jupyterlab/ui-components'; -import colormap from 'colormap'; import { ExpressionValue } from 'ol/expr/expression'; import React, { useEffect, useRef, useState } from 'react'; -import { VectorClassifications } from '../../../classificationModes'; +import { VectorClassifications } from '../../classificationModes'; import { IStopRow, ISymbologyDialogProps } from '../../symbologyDialog'; -import ColorRamp, { ColorRampOptions } from './ColorRamp'; -import StopRow from './StopRow'; +import ColorRamp, { + ColorRampOptions +} from '../../components/color_ramp/ColorRamp'; +import ValueSelect from '../components/ValueSelect'; +import StopContainer from '../../components/color_stops/StopContainer'; +import { useGetProperties } from '../../hooks/useGetProperties'; +import { Utils, VectorUtils } from '../../symbologyUtils'; +import { IVectorLayer } from '@jupytergis/schema'; const Graduated = ({ context, @@ -29,10 +32,10 @@ const Graduated = ({ const colorRampOptionsRef = useRef(); const [selectedValue, setSelectedValue] = useState(''); - const [featureProperties, setFeatureProperties] = useState({}); const [selectedMethod, setSelectedMethod] = useState('color'); const [stopRows, setStopRows] = useState([]); const [methodOptions, setMethodOptions] = useState(['color']); + const [colorRampOptions, setColorRampOptions] = useState< ColorRampOptions | undefined >(); @@ -45,38 +48,25 @@ const Graduated = ({ return; } - useEffect(() => { - const getProperties = async () => { - if (!layerId) { - return; - } - const model = context.model; - const layer = model.getLayer(layerId); - const source = model.getSource(layer?.parameters?.source); - - if (!source) { - return; - } - - const data = await model.readGeoJSON(source.parameters?.path); - const featureProps: any = {}; + const { featureProps } = useGetProperties({ + layerId, + model: context.model + }); - data?.features.forEach((feature: GeoJSONFeature1) => { - feature.properties && - Object.entries(feature.properties).forEach(([key, value]) => { - if (!(key in featureProps)) { - featureProps[key] = new Set(); - } + useEffect(() => { + let stopOutputPairs: IStopRow[] = []; + const layerParams = layer.parameters as IVectorLayer; + const method = layerParams.symbologyState?.method ?? 'color'; - featureProps[key].add(value); - }); + if (method === 'color') { + stopOutputPairs = VectorUtils.buildColorInfo(layer); + } - setFeatureProperties(featureProps); - }); - }; + if (method === 'radius') { + stopOutputPairs = VectorUtils.buildRadiusInfo(layer); + } - getProperties(); - buildColorInfo(); + setStopRows(stopOutputPairs); okSignalPromise.promise.then(okSignal => { okSignal.connect(handleOk, this); @@ -98,49 +88,7 @@ const Graduated = ({ useEffect(() => { populateOptions(); - }, [featureProperties]); - - const buildColorInfo = () => { - // This it to parse a color object on the layer - if (!layer.parameters?.color) { - return; - } - - const color = layer.parameters.color; - - // If color is a string we don't need to parse - if (typeof color === 'string') { - return; - } - - const prefix = layer.parameters.type === 'circle' ? 'circle-' : ''; - if (!color[`${prefix}fill-color`]) { - return; - } - - const valueColorPairs: IStopRow[] = []; - - // So if it's not a string then it's an array and we parse - // Color[0] is the operator used for the color expression - switch (color[`${prefix}fill-color`][0]) { - case 'interpolate': { - // First element is interpolate for linear selection - // Second element is type of interpolation (ie linear) - // Third is input value that stop values are compared with - // Fourth and on is value:color pairs - for (let i = 3; i < color[`${prefix}fill-color`].length; i += 2) { - const obj: IStopRow = { - stop: color[`${prefix}fill-color`][i], - output: color[`${prefix}fill-color`][i + 1] - }; - valueColorPairs.push(obj); - } - break; - } - } - - setStopRows(valueColorPairs); - }; + }, [featureProps]); const populateOptions = async () => { // Set up method options @@ -150,13 +98,9 @@ const Graduated = ({ } const layerParams = layer.parameters as IVectorLayer; - const value = layerParams.symbologyState?.value - ? layerParams.symbologyState.value - : Object.keys(featureProperties)[0]; - - const method = layerParams.symbologyState?.method - ? layerParams.symbologyState.method - : 'color'; + const value = + layerParams.symbologyState?.value ?? Object.keys(featureProps)[0]; + const method = layerParams.symbologyState?.method ?? 'color'; setSelectedValue(value); setSelectedMethod(method); @@ -215,23 +159,6 @@ const Graduated = ({ cancel(); }; - const addStopRow = () => { - setStopRows([ - { - stop: 0, - output: [0, 0, 0, 1] - }, - ...stopRows - ]); - }; - - const deleteStopRow = (index: number) => { - const newFilters = [...stopRows]; - newFilters.splice(index, 1); - - setStopRows(newFilters); - }; - const buildColorInfoFromClassification = ( selectedMode: string, numberOfShades: string, @@ -245,7 +172,7 @@ const Graduated = ({ let stops; - const values = featureProperties[selectedValue]; + const values = Array.from(featureProps[selectedValue]); switch (selectedMode) { case 'quantile': @@ -283,42 +210,29 @@ const Graduated = ({ return; } - const colorMap = colormap({ - colormap: selectedRamp, - nshades: +numberOfShades, - format: 'rgba' - }); - - const valueColorPairs: IStopRow[] = []; - - for (let i = 0; i < +numberOfShades; i++) { - valueColorPairs.push({ stop: stops[i], output: colorMap[i] }); + let stopOutputPairs = []; + if (selectedMethod === 'radius') { + for (let i = 0; i < +numberOfShades; i++) { + stopOutputPairs.push({ stop: stops[i], output: stops[i] }); + } + } else { + stopOutputPairs = Utils.getValueColorPairs( + stops, + selectedRamp, + +numberOfShades + ); } - setStopRows(valueColorPairs); + setStopRows(stopOutputPairs); }; return (
-
- - -
+