From 88d41fa352552fa63940de39418dd85553c63d20 Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Mon, 23 Mar 2020 16:09:09 -0400 Subject: [PATCH] [Maps] Remove client-side scaling of ordinal values (#58528) This removes the rescaling of ordinal values to the [0,1] domain, and modifies the creation of the mapbox-style rules to use the actual RangeStyleMeta-data. This is an important prerequisite for Maps handling tile vector sources. For these sources, Maps does not have access to the raw underlying GeoJson and needs to use the stylemeta directly. --- .../maps/public/layers/heatmap_layer.js | 2 +- .../maps/public/layers/styles/color_utils.js | 25 ++- .../public/layers/styles/color_utils.test.js | 22 +-- .../layers/styles/heatmap/heatmap_style.js | 7 +- .../properties/dynamic_color_property.js | 80 ++++++---- .../properties/dynamic_color_property.test.js | 143 +++++++++++------- .../properties/dynamic_icon_property.js | 4 +- .../dynamic_orientation_property.js | 4 - .../properties/dynamic_size_property.js | 51 ++++--- .../properties/dynamic_style_property.d.ts | 2 - .../properties/dynamic_style_property.js | 34 +---- .../properties/dynamic_text_property.js | 4 - .../properties/label_border_size_property.js | 30 ++-- .../public/layers/styles/vector/style_util.js | 36 +++-- .../layers/styles/vector/style_util.test.js | 28 +--- .../layers/styles/vector/vector_style.js | 14 +- .../functional/apps/maps/mapbox_styles.js | 101 +++++++++---- 17 files changed, 335 insertions(+), 252 deletions(-) diff --git a/x-pack/legacy/plugins/maps/public/layers/heatmap_layer.js b/x-pack/legacy/plugins/maps/public/layers/heatmap_layer.js index ef78b5afe3a3a..22f7a92c17c51 100644 --- a/x-pack/legacy/plugins/maps/public/layers/heatmap_layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/heatmap_layer.js @@ -72,7 +72,7 @@ export class HeatmapLayer extends VectorLayer { const propertyKey = this._getPropKeyOfSelectedMetric(); const dataBoundToMap = AbstractLayer.getBoundDataForSource(mbMap, this.getId()); if (featureCollection !== dataBoundToMap) { - let max = 0; + let max = 1; //max will be at least one, since counts or sums will be at least one. for (let i = 0; i < featureCollection.features.length; i++) { max = Math.max(featureCollection.features[i].properties[propertyKey], max); } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/color_utils.js b/x-pack/legacy/plugins/maps/public/layers/styles/color_utils.js index a619eaba21aef..09c7d76db1691 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/color_utils.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/color_utils.js @@ -78,17 +78,26 @@ export function getColorRampCenterColor(colorRampName) { // Returns an array of color stops // [ stop_input_1: number, stop_output_1: color, stop_input_n: number, stop_output_n: color ] -export function getOrdinalColorRampStops(colorRampName, numberColors = GRADIENT_INTERVALS) { +export function getOrdinalColorRampStops(colorRampName, min, max) { if (!colorRampName) { return null; } - return getHexColorRangeStrings(colorRampName, numberColors).reduce( - (accu, stopColor, idx, srcArr) => { - const stopNumber = idx / srcArr.length; // number between 0 and 1, increasing as index increases - return [...accu, stopNumber, stopColor]; - }, - [] - ); + + if (min > max) { + return null; + } + + const hexColors = getHexColorRangeStrings(colorRampName, GRADIENT_INTERVALS); + if (max === min) { + //just return single stop value + return [max, hexColors[hexColors.length - 1]]; + } + + const delta = max - min; + return hexColors.reduce((accu, stopColor, idx, srcArr) => { + const stopNumber = min + (delta * idx) / srcArr.length; + return [...accu, stopNumber, stopColor]; + }, []); } export const COLOR_GRADIENTS = Object.keys(vislibColorMaps).map(colorRampName => ({ diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/color_utils.test.js b/x-pack/legacy/plugins/maps/public/layers/styles/color_utils.test.js index 5a8289ba903f3..9a5ece01d5206 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/color_utils.test.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/color_utils.test.js @@ -60,26 +60,30 @@ describe('getColorRampCenterColor', () => { }); describe('getColorRampStops', () => { - it('Should create color stops for color ramp', () => { - expect(getOrdinalColorRampStops('Blues')).toEqual([ + it('Should create color stops for custom range', () => { + expect(getOrdinalColorRampStops('Blues', 0, 1000)).toEqual([ 0, '#f7faff', - 0.125, + 125, '#ddeaf7', - 0.25, + 250, '#c5daee', - 0.375, + 375, '#9dc9e0', - 0.5, + 500, '#6aadd5', - 0.625, + 625, '#4191c5', - 0.75, + 750, '#2070b4', - 0.875, + 875, '#072f6b', ]); }); + + it('Should snap to end of color stops for identical range', () => { + expect(getOrdinalColorRampStops('Blues', 23, 23)).toEqual([23, '#072f6b']); + }); }); describe('getLinearGradient', () => { diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/heatmap_style.js b/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/heatmap_style.js index dc3cfc3ffbdb8..d769fe0da9ec2 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/heatmap_style.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/heatmap_style.js @@ -14,6 +14,11 @@ import { getOrdinalColorRampStops } from '../color_utils'; import { i18n } from '@kbn/i18n'; import { EuiIcon } from '@elastic/eui'; +//The heatmap range chosen hear runs from 0 to 1. It is arbitrary. +//Weighting is on the raw count/sum values. +const MIN_RANGE = 0; +const MAX_RANGE = 1; + export class HeatmapStyle extends AbstractStyle { static type = LAYER_STYLE_TYPE.HEATMAP; @@ -80,7 +85,7 @@ export class HeatmapStyle extends AbstractStyle { const { colorRampName } = this._descriptor; if (colorRampName && colorRampName !== DEFAULT_HEATMAP_COLOR_RAMP_NAME) { - const colorStops = getOrdinalColorRampStops(colorRampName); + const colorStops = getOrdinalColorRampStops(colorRampName, MIN_RANGE, MAX_RANGE); mbMap.setPaintProperty(layerId, 'heatmap-color', [ 'interpolate', ['linear'], diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.js index 417426f12fc98..146bc40aa8531 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.js @@ -5,8 +5,7 @@ */ import { DynamicStyleProperty } from './dynamic_style_property'; -import _ from 'lodash'; -import { getComputedFieldName, getOtherCategoryLabel } from '../style_util'; +import { getOtherCategoryLabel, makeMbClampedNumberExpression } from '../style_util'; import { getOrdinalColorRampStops, getColorPalette } from '../../color_utils'; import { ColorGradient } from '../../components/color_gradient'; import React from 'react'; @@ -23,6 +22,7 @@ import { COLOR_MAP_TYPE } from '../../../../../common/constants'; import { isCategoricalStopsInvalid } from '../components/color/color_stops_utils'; const EMPTY_STOPS = { stops: [], defaultColor: null }; +const RGBA_0000 = 'rgba(0,0,0,0)'; export class DynamicColorProperty extends DynamicStyleProperty { syncCircleColorWithMb(mbLayerId, mbMap, alpha) { @@ -70,6 +70,17 @@ export class DynamicColorProperty extends DynamicStyleProperty { mbMap.setPaintProperty(mbLayerId, 'text-halo-color', color); } + supportsFieldMeta() { + if (!this.isComplete() || !this._field.supportsFieldMeta()) { + return false; + } + + return ( + (this.isCategorical() && !this._options.useCustomColorPalette) || + (this.isOrdinal() && !this._options.useCustomColorRamp) + ); + } + isOrdinal() { return ( typeof this._options.type === 'undefined' || this._options.type === COLOR_MAP_TYPE.ORDINAL @@ -80,28 +91,20 @@ export class DynamicColorProperty extends DynamicStyleProperty { return this._options.type === COLOR_MAP_TYPE.CATEGORICAL; } - isCustomOrdinalColorRamp() { - return this._options.useCustomColorRamp; - } - supportsMbFeatureState() { return true; } - isOrdinalScaled() { - return this.isOrdinal() && !this.isCustomOrdinalColorRamp(); - } - isOrdinalRanged() { - return this.isOrdinal() && !this.isCustomOrdinalColorRamp(); + return this.isOrdinal() && !this._options.useCustomColorRamp; } hasOrdinalBreaks() { - return (this.isOrdinal() && this.isCustomOrdinalColorRamp()) || this.isCategorical(); + return (this.isOrdinal() && this._options.useCustomColorRamp) || this.isCategorical(); } _getMbColor() { - if (!_.get(this._options, 'field.name')) { + if (!this._field || !this._field.getName()) { return null; } @@ -111,7 +114,7 @@ export class DynamicColorProperty extends DynamicStyleProperty { } _getOrdinalColorMbExpression() { - const targetName = getComputedFieldName(this._styleName, this._options.field.name); + const targetName = this._field.getName(); if (this._options.useCustomColorRamp) { if (!this._options.customColorRamp || !this._options.customColorRamp.length) { // custom color ramp config is not complete @@ -122,27 +125,44 @@ export class DynamicColorProperty extends DynamicStyleProperty { return [...accumulatedStops, nextStop.stop, nextStop.color]; }, []); const firstStopValue = colorStops[0]; - const lessThenFirstStopValue = firstStopValue - 1; + const lessThanFirstStopValue = firstStopValue - 1; return [ 'step', - ['coalesce', ['feature-state', targetName], lessThenFirstStopValue], - 'rgba(0,0,0,0)', // MB will assign the base value to any features that is below the first stop value + ['coalesce', ['feature-state', targetName], lessThanFirstStopValue], + RGBA_0000, // MB will assign the base value to any features that is below the first stop value ...colorStops, ]; - } + } else { + const rangeFieldMeta = this.getRangeFieldMeta(); + if (!rangeFieldMeta) { + return null; + } - const colorStops = getOrdinalColorRampStops(this._options.color); - if (!colorStops) { - return null; + const colorStops = getOrdinalColorRampStops( + this._options.color, + rangeFieldMeta.min, + rangeFieldMeta.max + ); + if (!colorStops) { + return null; + } + + const lessThanFirstStopValue = rangeFieldMeta.min - 1; + return [ + 'interpolate', + ['linear'], + makeMbClampedNumberExpression({ + minValue: rangeFieldMeta.min, + maxValue: rangeFieldMeta.max, + lookupFunction: 'feature-state', + fallback: lessThanFirstStopValue, + fieldName: targetName, + }), + lessThanFirstStopValue, + RGBA_0000, + ...colorStops, + ]; } - return [ - 'interpolate', - ['linear'], - ['coalesce', ['feature-state', targetName], -1], - -1, - 'rgba(0,0,0,0)', - ...colorStops, - ]; } _getColorPaletteStops() { @@ -220,7 +240,7 @@ export class DynamicColorProperty extends DynamicStyleProperty { } mbStops.push(defaultColor); //last color is default color - return ['match', ['to-string', ['get', this._options.field.name]], ...mbStops]; + return ['match', ['to-string', ['get', this._field.getName()]], ...mbStops]; } renderRangeLegendHeader() { diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.test.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.test.js index 755fc72d52798..b19c25b369848 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.test.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.test.js @@ -75,16 +75,10 @@ class MockLayer { } } -const makeProperty = options => { - return new DynamicColorProperty( - options, - VECTOR_STYLES.LINE_COLOR, - mockField, - new MockLayer(), - () => { - return x => x + '_format'; - } - ); +const makeProperty = (options, field = mockField) => { + return new DynamicColorProperty(options, VECTOR_STYLES.LINE_COLOR, field, new MockLayer(), () => { + return x => x + '_format'; + }); }; const defaultLegendParams = { @@ -236,7 +230,72 @@ test('Should pluck the categorical style-meta from fieldmeta', async () => { }); }); -describe('get mapbox color expression', () => { +describe('supportsFieldMeta', () => { + test('should support it when field does for ordinals', () => { + const dynamicStyleOptions = { + type: COLOR_MAP_TYPE.ORDINAL, + }; + const styleProp = makeProperty(dynamicStyleOptions); + + expect(styleProp.supportsFieldMeta()).toEqual(true); + }); + + test('should support it when field does for categories', () => { + const dynamicStyleOptions = { + type: COLOR_MAP_TYPE.CATEGORICAL, + }; + const styleProp = makeProperty(dynamicStyleOptions); + + expect(styleProp.supportsFieldMeta()).toEqual(true); + }); + + test('should not support it when field does not', () => { + const field = Object.create(mockField); + field.supportsFieldMeta = function() { + return false; + }; + + const dynamicStyleOptions = { + type: COLOR_MAP_TYPE.ORDINAL, + }; + const styleProp = makeProperty(dynamicStyleOptions, field); + + expect(styleProp.supportsFieldMeta()).toEqual(false); + }); + + test('should not support it when field config not complete', () => { + const dynamicStyleOptions = { + type: COLOR_MAP_TYPE.ORDINAL, + }; + const styleProp = makeProperty(dynamicStyleOptions, null); + + expect(styleProp.supportsFieldMeta()).toEqual(false); + }); + + test('should not support it when using custom ramp for ordinals', () => { + const dynamicStyleOptions = { + type: COLOR_MAP_TYPE.ORDINAL, + useCustomColorRamp: true, + customColorRamp: [], + }; + const styleProp = makeProperty(dynamicStyleOptions); + + expect(styleProp.supportsFieldMeta()).toEqual(false); + }); + + test('should not support it when using custom palette for categories', () => { + const dynamicStyleOptions = { + type: COLOR_MAP_TYPE.CATEGORICAL, + useCustomColorPalette: true, + customColorPalette: [], + }; + const styleProp = makeProperty(dynamicStyleOptions); + + expect(styleProp.supportsFieldMeta()).toEqual(false); + }); +}); + +describe('get mapbox color expression (via internal _getMbColor)', () => { describe('ordinal color ramp', () => { test('should return null when field is not provided', async () => { const dynamicStyleOptions = { @@ -259,44 +318,46 @@ describe('get mapbox color expression', () => { test('should return null when color ramp is not provided', async () => { const dynamicStyleOptions = { type: COLOR_MAP_TYPE.ORDINAL, - field: { - name: 'myField', - }, }; const colorProperty = makeProperty(dynamicStyleOptions); expect(colorProperty._getMbColor()).toBeNull(); }); - test('should return mapbox expression for color ramp', async () => { const dynamicStyleOptions = { type: COLOR_MAP_TYPE.ORDINAL, - field: { - name: 'myField', - }, color: 'Blues', }; const colorProperty = makeProperty(dynamicStyleOptions); expect(colorProperty._getMbColor()).toEqual([ 'interpolate', ['linear'], - ['coalesce', ['feature-state', '__kbn__dynamic__myField__lineColor'], -1], + [ + 'coalesce', + [ + 'case', + ['==', ['feature-state', 'foobar'], null], + -1, + ['max', ['min', ['to-number', ['feature-state', 'foobar']], 100], 0], + ], + -1, + ], -1, 'rgba(0,0,0,0)', 0, '#f7faff', - 0.125, + 12.5, '#ddeaf7', - 0.25, + 25, '#c5daee', - 0.375, + 37.5, '#9dc9e0', - 0.5, + 50, '#6aadd5', - 0.625, + 62.5, '#4191c5', - 0.75, + 75, '#2070b4', - 0.875, + 87.5, '#072f6b', ]); }); @@ -306,9 +367,6 @@ describe('get mapbox color expression', () => { test('should return null when customColorRamp is not provided', async () => { const dynamicStyleOptions = { type: COLOR_MAP_TYPE.ORDINAL, - field: { - name: 'myField', - }, useCustomColorRamp: true, }; const colorProperty = makeProperty(dynamicStyleOptions); @@ -318,9 +376,6 @@ describe('get mapbox color expression', () => { test('should return null when customColorRamp is empty', async () => { const dynamicStyleOptions = { type: COLOR_MAP_TYPE.ORDINAL, - field: { - name: 'myField', - }, useCustomColorRamp: true, customColorRamp: [], }; @@ -331,9 +386,6 @@ describe('get mapbox color expression', () => { test('should return mapbox expression for custom color ramp', async () => { const dynamicStyleOptions = { type: COLOR_MAP_TYPE.ORDINAL, - field: { - name: 'myField', - }, useCustomColorRamp: true, customColorRamp: [ { stop: 10, color: '#f7faff' }, @@ -343,7 +395,7 @@ describe('get mapbox color expression', () => { const colorProperty = makeProperty(dynamicStyleOptions); expect(colorProperty._getMbColor()).toEqual([ 'step', - ['coalesce', ['feature-state', '__kbn__dynamic__myField__lineColor'], 9], + ['coalesce', ['feature-state', 'foobar'], 9], 'rgba(0,0,0,0)', 10, '#f7faff', @@ -376,9 +428,6 @@ describe('get mapbox color expression', () => { test('should return null when color palette is not provided', async () => { const dynamicStyleOptions = { type: COLOR_MAP_TYPE.CATEGORICAL, - field: { - name: 'myField', - }, }; const colorProperty = makeProperty(dynamicStyleOptions); expect(colorProperty._getMbColor()).toBeNull(); @@ -387,15 +436,12 @@ describe('get mapbox color expression', () => { test('should return mapbox expression for color palette', async () => { const dynamicStyleOptions = { type: COLOR_MAP_TYPE.CATEGORICAL, - field: { - name: 'myField', - }, colorCategory: 'palette_0', }; const colorProperty = makeProperty(dynamicStyleOptions); expect(colorProperty._getMbColor()).toEqual([ 'match', - ['to-string', ['get', 'myField']], + ['to-string', ['get', 'foobar']], 'US', '#54B399', 'CN', @@ -409,9 +455,6 @@ describe('get mapbox color expression', () => { test('should return null when customColorPalette is not provided', async () => { const dynamicStyleOptions = { type: COLOR_MAP_TYPE.CATEGORICAL, - field: { - name: 'myField', - }, useCustomColorPalette: true, }; const colorProperty = makeProperty(dynamicStyleOptions); @@ -421,9 +464,6 @@ describe('get mapbox color expression', () => { test('should return null when customColorPalette is empty', async () => { const dynamicStyleOptions = { type: COLOR_MAP_TYPE.CATEGORICAL, - field: { - name: 'myField', - }, useCustomColorPalette: true, customColorPalette: [], }; @@ -434,9 +474,6 @@ describe('get mapbox color expression', () => { test('should return mapbox expression for custom color palette', async () => { const dynamicStyleOptions = { type: COLOR_MAP_TYPE.CATEGORICAL, - field: { - name: 'myField', - }, useCustomColorPalette: true, customColorPalette: [ { stop: null, color: '#f7faff' }, @@ -446,7 +483,7 @@ describe('get mapbox color expression', () => { const colorProperty = makeProperty(dynamicStyleOptions); expect(colorProperty._getMbColor()).toEqual([ 'match', - ['to-string', ['get', 'myField']], + ['to-string', ['get', 'foobar']], 'MX', '#072f6b', '#f7faff', diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_icon_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_icon_property.js index c492efbdf4ba3..05e2ad06842ce 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_icon_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_icon_property.js @@ -81,7 +81,7 @@ export class DynamicIconProperty extends DynamicStyleProperty { mbStops.push(getMakiIconId(style, iconPixelSize)); }); mbStops.push(getMakiIconId(fallback, iconPixelSize)); //last item is fallback style for anything that does not match provided stops - return ['match', ['to-string', ['get', this._options.field.name]], ...mbStops]; + return ['match', ['to-string', ['get', this._field.getName()]], ...mbStops]; } _getMbIconAnchorExpression() { @@ -98,7 +98,7 @@ export class DynamicIconProperty extends DynamicStyleProperty { mbStops.push(getMakiSymbolAnchor(style)); }); mbStops.push(getMakiSymbolAnchor(fallback)); //last item is fallback style for anything that does not match provided stops - return ['match', ['to-string', ['get', this._options.field.name]], ...mbStops]; + return ['match', ['to-string', ['get', this._field.getName()]], ...mbStops]; } _isIconDynamicConfigComplete() { diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_orientation_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_orientation_property.js index 96408b3d2229e..ae4d935e2457b 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_orientation_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_orientation_property.js @@ -25,8 +25,4 @@ export class DynamicOrientationProperty extends DynamicStyleProperty { supportsMbFeatureState() { return false; } - - isOrdinalScaled() { - return false; - } } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_size_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_size_property.js index 7626f8c9b4158..8b3f670bfa528 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_size_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_size_property.js @@ -5,7 +5,7 @@ */ import { DynamicStyleProperty } from './dynamic_style_property'; - +import { makeMbClampedNumberExpression } from '../style_util'; import { HALF_LARGE_MAKI_ICON_SIZE, LARGE_MAKI_ICON_SIZE, @@ -74,17 +74,24 @@ export class DynamicSizeProperty extends DynamicStyleProperty { } syncIconSizeWithMb(symbolLayerId, mbMap) { - if (this._isSizeDynamicConfigComplete(this._options)) { + const rangeFieldMeta = this.getRangeFieldMeta(); + if (this._isSizeDynamicConfigComplete(this._options) && rangeFieldMeta) { const halfIconPixels = this.getIconPixelSize() / 2; - const targetName = this.getComputedFieldName(); + const targetName = this.getFieldName(); // Using property state instead of feature-state because layout properties do not support feature-state mbMap.setLayoutProperty(symbolLayerId, 'icon-size', [ 'interpolate', ['linear'], - ['coalesce', ['get', targetName], 0], - 0, + makeMbClampedNumberExpression({ + minValue: rangeFieldMeta.min, + maxValue: rangeFieldMeta.max, + fallback: 0, + lookupFunction: 'get', + fieldName: targetName, + }), + rangeFieldMeta.min, this._options.minSize / halfIconPixels, - 1, + rangeFieldMeta.max, this._options.maxSize / halfIconPixels, ]); } else { @@ -113,25 +120,35 @@ export class DynamicSizeProperty extends DynamicStyleProperty { } getMbSizeExpression() { - if (this._isSizeDynamicConfigComplete(this._options)) { - return this._getMbDataDrivenSize({ - targetName: this.getComputedFieldName(), - minSize: this._options.minSize, - maxSize: this._options.maxSize, - }); + const rangeFieldMeta = this.getRangeFieldMeta(); + if (!this._isSizeDynamicConfigComplete(this._options) || !rangeFieldMeta) { + return null; } - return null; + + return this._getMbDataDrivenSize({ + targetName: this.getFieldName(), + minSize: this._options.minSize, + maxSize: this._options.maxSize, + minValue: rangeFieldMeta.min, + maxValue: rangeFieldMeta.max, + }); } - _getMbDataDrivenSize({ targetName, minSize, maxSize }) { + _getMbDataDrivenSize({ targetName, minSize, maxSize, minValue, maxValue }) { const lookup = this.supportsMbFeatureState() ? 'feature-state' : 'get'; return [ 'interpolate', ['linear'], - ['coalesce', [lookup, targetName], 0], - 0, + makeMbClampedNumberExpression({ + lookupFunction: lookup, + maxValue, + minValue, + fieldName: targetName, + fallback: 0, + }), + minValue, minSize, - 1, + maxValue, maxSize, ]; } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.d.ts b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.d.ts index 25063944b8891..a83dd55c0c175 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.d.ts +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.d.ts @@ -12,8 +12,6 @@ import { DynamicStylePropertyOptions, } from '../../../../../common/style_property_descriptor_types'; import { IField } from '../../../fields/field'; -import { IVectorLayer } from '../../../vector_layer'; -import { IVectorSource } from '../../../sources/vector_source'; import { CategoryFieldMeta, RangeFieldMeta } from '../../../../../common/descriptor_types'; export interface IDynamicStyleProperty extends IStyleProperty { diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js index 68e06bacfa7b7..ea521f8749d80 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js @@ -13,7 +13,6 @@ import { SOURCE_META_ID_ORIGIN, FIELD_ORIGIN, } from '../../../../../common/constants'; -import { scaleValue, getComputedFieldName } from '../style_util'; import React from 'react'; import { OrdinalLegend } from './components/ordinal_legend'; import { CategoricalLegend } from './components/categorical_legend'; @@ -109,13 +108,6 @@ export class DynamicStyleProperty extends AbstractStyleProperty { return this._field ? this._field.getName() : ''; } - getComputedFieldName() { - if (!this.isComplete()) { - return null; - } - return getComputedFieldName(this._styleName, this.getField().getName()); - } - isDynamic() { return true; } @@ -150,13 +142,7 @@ export class DynamicStyleProperty extends AbstractStyleProperty { } supportsFieldMeta() { - if (this.isOrdinal()) { - return this.isComplete() && this.isOrdinalScaled() && this._field.supportsFieldMeta(); - } else if (this.isCategorical()) { - return this.isComplete() && this._field.supportsFieldMeta(); - } else { - return false; - } + return this.isComplete() && this._field.supportsFieldMeta(); } async getFieldMetaRequest() { @@ -173,10 +159,6 @@ export class DynamicStyleProperty extends AbstractStyleProperty { return true; } - isOrdinalScaled() { - return true; - } - getFieldMetaOptions() { return _.get(this.getOptions(), 'fieldMetaOptions', {}); } @@ -296,19 +278,9 @@ export class DynamicStyleProperty extends AbstractStyleProperty { } } - getMbValue(value) { - if (!this.isOrdinal()) { - return this.formatField(value); - } - + getNumericalMbFeatureStateValue(value) { const valueAsFloat = parseFloat(value); - if (this.isOrdinalScaled()) { - return scaleValue(valueAsFloat, this.getRangeFieldMeta()); - } - if (isNaN(valueAsFloat)) { - return 0; - } - return valueAsFloat; + return isNaN(valueAsFloat) ? null : valueAsFloat; } renderBreakedLegend() { diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_text_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_text_property.js index c561ec128dec5..de868f3f92650 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_text_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_text_property.js @@ -28,8 +28,4 @@ export class DynamicTextProperty extends DynamicStyleProperty { supportsMbFeatureState() { return false; } - - isOrdinalScaled() { - return false; - } } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/label_border_size_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/label_border_size_property.js index 7119b659c1232..3016b15d0a05c 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/label_border_size_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/label_border_size_property.js @@ -31,21 +31,27 @@ export class LabelBorderSizeProperty extends AbstractStyleProperty { } syncLabelBorderSizeWithMb(mbLayerId, mbMap) { - const widthRatio = getWidthRatio(this.getOptions().size); - if (this.getOptions().size === LABEL_BORDER_SIZES.NONE) { mbMap.setPaintProperty(mbLayerId, 'text-halo-width', 0); - } else if (this._labelSizeProperty.isDynamic() && this._labelSizeProperty.isComplete()) { + return; + } + + const widthRatio = getWidthRatio(this.getOptions().size); + + if (this._labelSizeProperty.isDynamic() && this._labelSizeProperty.isComplete()) { const labelSizeExpression = this._labelSizeProperty.getMbSizeExpression(); - mbMap.setPaintProperty(mbLayerId, 'text-halo-width', [ - 'max', - ['*', labelSizeExpression, widthRatio], - 1, - ]); - } else { - const labelSize = _.get(this._labelSizeProperty.getOptions(), 'size', DEFAULT_LABEL_SIZE); - const labelBorderSize = Math.max(labelSize * widthRatio, 1); - mbMap.setPaintProperty(mbLayerId, 'text-halo-width', labelBorderSize); + if (labelSizeExpression) { + mbMap.setPaintProperty(mbLayerId, 'text-halo-width', [ + 'max', + ['*', labelSizeExpression, widthRatio], + 1, + ]); + return; + } } + + const labelSize = _.get(this._labelSizeProperty.getOptions(), 'size', DEFAULT_LABEL_SIZE); + const labelBorderSize = Math.max(labelSize * widthRatio, 1); + mbMap.setPaintProperty(mbLayerId, 'text-halo-width', labelBorderSize); } } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/style_util.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/style_util.js index 2859b8c0e5a56..0820568468439 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/style_util.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/style_util.js @@ -34,22 +34,6 @@ export function isOnlySingleFeatureType(featureType, supportedFeatures, hasFeatu }, true); } -export function scaleValue(value, range) { - if (isNaN(value) || !range) { - return -1; //Nothing to scale, put outside scaled range - } - - if (range.delta === 0 || value >= range.max) { - return 1; //snap to end of scaled range - } - - if (value <= range.min) { - return 0; //snap to beginning of scaled range - } - - return (value - range.min) / range.delta; -} - export function assignCategoriesToPalette({ categories, paletteValues }) { const stops = []; let fallback = null; @@ -70,3 +54,23 @@ export function assignCategoriesToPalette({ categories, paletteValues }) { fallback, }; } + +export function makeMbClampedNumberExpression({ + lookupFunction, + fieldName, + minValue, + maxValue, + fallback, +}) { + const clamp = ['max', ['min', ['to-number', [lookupFunction, fieldName]], maxValue], minValue]; + return [ + 'coalesce', + [ + 'case', + ['==', [lookupFunction, fieldName], null], + minValue - 1, //== does a JS-y like check where returns true for null and undefined + clamp, + ], + fallback, + ]; +} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/style_util.test.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/style_util.test.js index 2be31c0107193..76bbfc84e3892 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/style_util.test.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/style_util.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isOnlySingleFeatureType, scaleValue, assignCategoriesToPalette } from './style_util'; +import { isOnlySingleFeatureType, assignCategoriesToPalette } from './style_util'; import { VECTOR_SHAPE_TYPES } from '../../sources/vector_feature_types'; describe('isOnlySingleFeatureType', () => { @@ -62,32 +62,6 @@ describe('isOnlySingleFeatureType', () => { }); }); -describe('scaleValue', () => { - test('Should scale value between 0 and 1', () => { - expect(scaleValue(5, { min: 0, max: 10, delta: 10 })).toBe(0.5); - }); - - test('Should snap value less then range min to 0', () => { - expect(scaleValue(-1, { min: 0, max: 10, delta: 10 })).toBe(0); - }); - - test('Should snap value greater then range max to 1', () => { - expect(scaleValue(11, { min: 0, max: 10, delta: 10 })).toBe(1); - }); - - test('Should snap value to 1 when tere is not range delta', () => { - expect(scaleValue(10, { min: 10, max: 10, delta: 0 })).toBe(1); - }); - - test('Should put value as -1 when value is not provided', () => { - expect(scaleValue(undefined, { min: 0, max: 10, delta: 10 })).toBe(-1); - }); - - test('Should put value as -1 when range is not provided', () => { - expect(scaleValue(5, undefined)).toBe(-1); - }); -}); - describe('assignCategoriesToPalette', () => { test('Categories and palette values have same length', () => { const categories = [{ key: 'alpah' }, { key: 'bravo' }, { key: 'charlie' }, { key: 'delta' }]; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js index b5a0b51727936..ae5d148e43cfd 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js @@ -503,11 +503,19 @@ export class VectorStyle extends AbstractStyle { const dynamicStyleProp = dynamicStyleProps[j]; const name = dynamicStyleProp.getField().getName(); const computedName = getComputedFieldName(dynamicStyleProp.getStyleName(), name); - const styleValue = dynamicStyleProp.getMbValue(feature.properties[name]); + const rawValue = feature.properties[name]; if (dynamicStyleProp.supportsMbFeatureState()) { - tmpFeatureState[computedName] = styleValue; + tmpFeatureState[name] = dynamicStyleProp.getNumericalMbFeatureStateValue(rawValue); //the same value will be potentially overridden multiple times, if the name remains identical } else { - feature.properties[computedName] = styleValue; + //in practice, a new system property will only be created for: + // - label text: this requires the value to be formatted first. + // - icon orientation: this is a lay-out property which do not support feature-state (but we're still coercing to a number) + + const formattedValue = dynamicStyleProp.isOrdinal() + ? dynamicStyleProp.getNumericalMbFeatureStateValue(rawValue) + : dynamicStyleProp.formatField(rawValue); + + feature.properties[computedName] = formattedValue; } } tmpFeatureIdentifier.source = mbSourceId; diff --git a/x-pack/test/functional/apps/maps/mapbox_styles.js b/x-pack/test/functional/apps/maps/mapbox_styles.js index d3280bae8582e..508a019db1764 100644 --- a/x-pack/test/functional/apps/maps/mapbox_styles.js +++ b/x-pack/test/functional/apps/maps/mapbox_styles.js @@ -16,9 +16,7 @@ export const MAPBOX_STYLES = { ['==', ['get', '__kbn_isvisibleduetojoin__'], true], ['any', ['==', ['geometry-type'], 'Point'], ['==', ['geometry-type'], 'MultiPoint']], ], - layout: { - visibility: 'visible', - }, + layout: { visibility: 'visible' }, paint: { 'circle-color': [ 'interpolate', @@ -26,32 +24,53 @@ export const MAPBOX_STYLES = { [ 'coalesce', [ - 'feature-state', - '__kbn__dynamic____kbnjoin__max_of_prop1_groupby_meta_for_geo_shapes*.shape_name__fillColor', + 'case', + [ + '==', + ['feature-state', '__kbnjoin__max_of_prop1_groupby_meta_for_geo_shapes*.shape_name'], + null, + ], + 2, + [ + 'max', + [ + 'min', + [ + 'to-number', + [ + 'feature-state', + '__kbnjoin__max_of_prop1_groupby_meta_for_geo_shapes*.shape_name', + ], + ], + 12, + ], + 3, + ], ], - -1, + 2, ], - -1, + 2, 'rgba(0,0,0,0)', - 0, + 3, '#f7faff', - 0.125, + 4.125, '#ddeaf7', - 0.25, + 5.25, '#c5daee', - 0.375, + 6.375, '#9dc9e0', - 0.5, + 7.5, '#6aadd5', - 0.625, + 8.625, '#4191c5', - 0.75, + 9.75, '#2070b4', - 0.875, + 10.875, '#072f6b', ], - 'circle-opacity': 0.75, // Obtained dynamically - /* 'circle-stroke-color': '' */ 'circle-stroke-opacity': 0.75, + 'circle-opacity': 0.75, + 'circle-stroke-color': '#41937c', + 'circle-stroke-opacity': 0.75, 'circle-stroke-width': 1, 'circle-radius': 10, }, @@ -67,9 +86,7 @@ export const MAPBOX_STYLES = { ['==', ['get', '__kbn_isvisibleduetojoin__'], true], ['any', ['==', ['geometry-type'], 'Polygon'], ['==', ['geometry-type'], 'MultiPolygon']], ], - layout: { - visibility: 'visible', - }, + layout: { visibility: 'visible' }, paint: { 'fill-color': [ 'interpolate', @@ -77,28 +94,48 @@ export const MAPBOX_STYLES = { [ 'coalesce', [ - 'feature-state', - '__kbn__dynamic____kbnjoin__max_of_prop1_groupby_meta_for_geo_shapes*.shape_name__fillColor', + 'case', + [ + '==', + ['feature-state', '__kbnjoin__max_of_prop1_groupby_meta_for_geo_shapes*.shape_name'], + null, + ], + 2, + [ + 'max', + [ + 'min', + [ + 'to-number', + [ + 'feature-state', + '__kbnjoin__max_of_prop1_groupby_meta_for_geo_shapes*.shape_name', + ], + ], + 12, + ], + 3, + ], ], - -1, + 2, ], - -1, + 2, 'rgba(0,0,0,0)', - 0, + 3, '#f7faff', - 0.125, + 4.125, '#ddeaf7', - 0.25, + 5.25, '#c5daee', - 0.375, + 6.375, '#9dc9e0', - 0.5, + 7.5, '#6aadd5', - 0.625, + 8.625, '#4191c5', - 0.75, + 9.75, '#2070b4', - 0.875, + 10.875, '#072f6b', ], 'fill-opacity': 0.75,