[Maps] heatmap color ramp select and legend details (#37187)

* [Maps] heatmap color ramp select and legend details

* heatmap style editor

* add tests for color_utils

* add snapshot test for HeatmapStyleEditor

* change label to Color range

* update jest snapshot for label text change
This commit is contained in:
Nathan Reese 2019-05-29 17:36:19 -06:00 committed by GitHub
parent 6276a9b4af
commit c8177e07cb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 590 additions and 166 deletions

View file

@ -1,3 +1,2 @@
@import './components/index';
@import './icons/color_gradient';
@import './layers/index';

View file

@ -1,38 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { vislibColorMaps } from 'ui/vislib/components/color/colormaps';
import { getLegendColors } from 'ui/vis/map/color_util';
import classNames from 'classnames';
const GRADIENT_INTERVALS = 7;
const COLOR_KEYS = Object.keys(vislibColorMaps);
export const ColorGradient = ({ color, className }) => {
if (!color || !COLOR_KEYS.includes(color)) {
return null;
}
const classes = classNames('mapColorGradient', className);
const rgbColorStrings = getLegendColors(vislibColorMaps[color].value, GRADIENT_INTERVALS);
const background = getLinearGradient(rgbColorStrings, GRADIENT_INTERVALS);
return (
<div
className={classes}
style={{ background }}
/>
);
};
function getLinearGradient(colorStrings, intervals) {
let linearGradient = `linear-gradient(to right, ${colorStrings[0]} 0%,`;
for (let i = 1; i < intervals - 1; i++) {
linearGradient = `${linearGradient} ${colorStrings[i]} \
${Math.floor(100 * i / (intervals - 1))}%,`;
}
return `${linearGradient} ${colorStrings.pop()} 100%)`;
}

View file

@ -62,7 +62,6 @@ export class HeatmapLayer extends AbstractLayer {
data: { 'type': 'FeatureCollection', 'features': [] }
});
mbMap.addLayer({
id: mbLayerId,
type: 'heatmap',
@ -210,4 +209,13 @@ export class HeatmapLayer extends AbstractLayer {
return super.getCustomIconAndTooltipContent();
}
hasLegendDetails() {
return true;
}
getLegendDetails() {
const label = _.get(this._source.getMetricFields(), '[0].propertyLabel', '');
return this._style.getLegendDetails(label);
}
}

View file

@ -1,2 +1,3 @@
@import './components/color_gradient';
@import './components/static_dynamic_style_row';
@import './components/vector/color/static_color_selection';

View file

@ -0,0 +1,66 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { vislibColorMaps } from 'ui/vislib/components/color/colormaps';
import { getLegendColors, getColor } from 'ui/vis/map/color_util';
import { ColorGradient } from './components/color_gradient';
import chroma from 'chroma-js';
const GRADIENT_INTERVALS = 8;
function getColorRamp(colorRampName) {
const colorRamp = vislibColorMaps[colorRampName];
if (!colorRamp) {
throw new Error(`${colorRampName} not found. Expected one of following values: ${Object.keys(vislibColorMaps)}`);
}
return colorRamp;
}
export function getRGBColorRangeStrings(colorRampName, numberColors = GRADIENT_INTERVALS) {
const colorRamp = getColorRamp(colorRampName);
return getLegendColors(colorRamp.value, numberColors);
}
export function getHexColorRangeStrings(colorRampName, numberColors = GRADIENT_INTERVALS) {
return getRGBColorRangeStrings(colorRampName, numberColors)
.map(rgbColor => chroma(rgbColor).hex());
}
export function getColorRampCenterColor(colorRampName) {
const colorRamp = getColorRamp(colorRampName);
const centerIndex = Math.floor(colorRamp.value.length / 2);
return getColor(colorRamp.value, centerIndex);
}
// Returns an array of color stops
// [ stop_input_1: number, stop_output_1: color, stop_input_n: number, stop_output_n: color ]
export function getColorRampStops(colorRampName, numberColors = GRADIENT_INTERVALS) {
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 ];
}, []);
}
export const COLOR_GRADIENTS = Object.keys(vislibColorMaps).map(colorRampName => ({
value: colorRampName,
text: colorRampName,
inputDisplay: <ColorGradient colorRampName={colorRampName}/>
}));
export const COLOR_RAMP_NAMES = Object.keys(vislibColorMaps);
export function getLinearGradient(colorStrings) {
const intervals = colorStrings.length;
let linearGradient = `linear-gradient(to right, ${colorStrings[0]} 0%,`;
for (let i = 1; i < intervals - 1; i++) {
linearGradient = `${linearGradient} ${colorStrings[i]} \
${Math.floor(100 * i / (intervals - 1))}%,`;
}
return `${linearGradient} ${colorStrings[colorStrings.length - 1]} 100%)`;
}

View file

@ -0,0 +1,91 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
COLOR_GRADIENTS,
getColorRampCenterColor,
getColorRampStops,
getHexColorRangeStrings,
getLinearGradient,
getRGBColorRangeStrings,
} from './color_utils';
describe('COLOR_GRADIENTS', () => {
it('Should contain EuiSuperSelect options list of color ramps', () => {
expect(COLOR_GRADIENTS.length).toBe(6);
const colorGradientOption = COLOR_GRADIENTS[0];
expect(colorGradientOption.text).toBe('Blues');
expect(colorGradientOption.value).toBe('Blues');
});
});
describe('getRGBColorRangeStrings', () => {
it('Should create RGB color ramp', () => {
expect(getRGBColorRangeStrings('Blues')).toEqual([
'rgb(247,250,255)',
'rgb(221,234,247)',
'rgb(197,218,238)',
'rgb(157,201,224)',
'rgb(106,173,213)',
'rgb(65,145,197)',
'rgb(32,112,180)',
'rgb(7,47,107)'
]);
});
});
describe('getHexColorRangeStrings', () => {
it('Should create HEX color ramp', () => {
expect(getHexColorRangeStrings('Blues')).toEqual([
'#f7faff',
'#ddeaf7',
'#c5daee',
'#9dc9e0',
'#6aadd5',
'#4191c5',
'#2070b4',
'#072f6b'
]);
});
});
describe('getColorRampCenterColor', () => {
it('Should get center color from color ramp', () => {
expect(getColorRampCenterColor('Blues')).toBe('rgb(106,173,213)');
});
});
describe('getColorRampStops', () => {
it('Should create color stops for color ramp', () => {
expect(getColorRampStops('Blues')).toEqual([
0, '#f7faff',
0.125, '#ddeaf7',
0.25, '#c5daee',
0.375, '#9dc9e0',
0.5, '#6aadd5',
0.625, '#4191c5',
0.75, '#2070b4',
0.875, '#072f6b'
]);
});
});
describe('getLinearGradient', () => {
it('Should create linear gradient from color ramp', () => {
const colorRamp = [
'rgb(247,250,255)',
'rgb(221,234,247)',
'rgb(197,218,238)',
'rgb(157,201,224)',
'rgb(106,173,213)',
'rgb(65,145,197)',
'rgb(32,112,180)',
'rgb(7,47,107)'
];
// eslint-disable-next-line max-len
expect(getLinearGradient(colorRamp)).toBe('linear-gradient(to right, rgb(247,250,255) 0%, rgb(221,234,247) 14%, rgb(197,218,238) 28%, rgb(157,201,224) 42%, rgb(106,173,213) 57%, rgb(65,145,197) 71%, rgb(32,112,180) 85%, rgb(7,47,107) 100%)');
});
});

View file

@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { COLOR_RAMP_NAMES, getRGBColorRangeStrings, getLinearGradient } from '../color_utils';
import classNames from 'classnames';
export const ColorGradient = ({ colorRamp, colorRampName, className }) => {
if (!colorRamp && (!colorRampName || !COLOR_RAMP_NAMES.includes(colorRampName))) {
return null;
}
const classes = classNames('mapColorGradient', className);
const rgbColorStrings = colorRampName
? getRGBColorRangeStrings(colorRampName)
: colorRamp;
const background = getLinearGradient(rgbColorStrings);
return (
<div
className={classes}
style={{ background }}
/>
);
};

View file

@ -0,0 +1,81 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`HeatmapStyleEditor is rendered 1`] = `
<EuiFormRow
describedByIds={Array []}
fullWidth={false}
hasEmptyLabelSpace={false}
label="Color range"
labelType="label"
>
<EuiSuperSelect
compressed={false}
fullWidth={false}
hasDividers={true}
isInvalid={false}
onChange={[Function]}
options={
Array [
Object {
"inputDisplay": <ColorGradient
colorRamp={
Array [
"rgb(65, 105, 225)",
"rgb(0, 256, 256)",
"rgb(0, 256, 0)",
"rgb(256, 256, 0)",
"rgb(256, 0, 0)",
]
}
/>,
"text": "theclassic",
"value": "theclassic",
},
Object {
"inputDisplay": <ColorGradient
colorRampName="Blues"
/>,
"text": "Blues",
"value": "Blues",
},
Object {
"inputDisplay": <ColorGradient
colorRampName="Greens"
/>,
"text": "Greens",
"value": "Greens",
},
Object {
"inputDisplay": <ColorGradient
colorRampName="Greys"
/>,
"text": "Greys",
"value": "Greys",
},
Object {
"inputDisplay": <ColorGradient
colorRampName="Reds"
/>,
"text": "Reds",
"value": "Reds",
},
Object {
"inputDisplay": <ColorGradient
colorRampName="Yellow to Red"
/>,
"text": "Yellow to Red",
"value": "Yellow to Red",
},
Object {
"inputDisplay": <ColorGradient
colorRampName="Green to Red"
/>,
"text": "Green to Red",
"value": "Green to Red",
},
]
}
valueOfSelected="Blues"
/>
</EuiFormRow>
`;

View file

@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
// Color stops from default Mapbox heatmap-color
export const DEFAULT_RGB_HEATMAP_COLOR_RAMP = [
'rgb(65, 105, 225)', // royalblue
'rgb(0, 256, 256)', // cyan
'rgb(0, 256, 0)', // lime
'rgb(256, 256, 0)', // yellow
'rgb(256, 0, 0)', // red
];
export const DEFAULT_HEATMAP_COLOR_RAMP_NAME = 'theclassic';
export const HEATMAP_COLOR_RAMP_LABEL = i18n.translate('xpack.maps.heatmap.colorRampLabel', {
defaultMessage: 'Color range'
});

View file

@ -0,0 +1,45 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { EuiFormRow, EuiSuperSelect } from '@elastic/eui';
import { COLOR_GRADIENTS } from '../../color_utils';
import { ColorGradient } from '../color_gradient';
import {
DEFAULT_RGB_HEATMAP_COLOR_RAMP,
DEFAULT_HEATMAP_COLOR_RAMP_NAME,
HEATMAP_COLOR_RAMP_LABEL
} from './heatmap_constants';
export function HeatmapStyleEditor({ colorRampName, onHeatmapColorChange }) {
const onColorRampChange = (selectedColorRampName) => {
onHeatmapColorChange({
colorRampName: selectedColorRampName
});
};
const colorRampOptions = [
{
value: DEFAULT_HEATMAP_COLOR_RAMP_NAME,
text: DEFAULT_HEATMAP_COLOR_RAMP_NAME,
inputDisplay: <ColorGradient colorRamp={DEFAULT_RGB_HEATMAP_COLOR_RAMP}/>
},
...COLOR_GRADIENTS
];
return (
<EuiFormRow label={HEATMAP_COLOR_RAMP_LABEL}>
<EuiSuperSelect
options={colorRampOptions}
onChange={onColorRampChange}
valueOfSelected={colorRampName}
hasDividers={true}
/>
</EuiFormRow>
);
}

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { HeatmapStyleEditor } from './heatmap_style_editor';
describe('HeatmapStyleEditor', () => {
test('is rendered', () => {
const component = shallow(
<HeatmapStyleEditor
colorRampName="Blues"
onHeatmapColorChange={() => {}}
/>
);
expect(component)
.toMatchSnapshot();
});
});

View file

@ -0,0 +1,40 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import { ColorGradient } from '../../color_gradient';
import { StyleLegendRow } from '../../style_legend_row';
import {
DEFAULT_RGB_HEATMAP_COLOR_RAMP,
DEFAULT_HEATMAP_COLOR_RAMP_NAME,
HEATMAP_COLOR_RAMP_LABEL
} from '../heatmap_constants';
export function HeatmapLegend({ colorRampName, label }) {
const header = colorRampName === DEFAULT_HEATMAP_COLOR_RAMP_NAME
? <ColorGradient colorRamp={DEFAULT_RGB_HEATMAP_COLOR_RAMP}/>
: <ColorGradient colorRampName={colorRampName}/>;
return (
<StyleLegendRow
header={header}
minLabel={
i18n.translate('xpack.maps.heatmapLegend.coldLabel', {
defaultMessage: 'cold'
})
}
maxLabel={
i18n.translate('xpack.maps.heatmapLegend.hotLabel', {
defaultMessage: 'hot'
})
}
propertyLabel={HEATMAP_COLOR_RAMP_LABEL}
fieldLabel={label}
/>
);
}

View file

@ -0,0 +1,66 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import {
EuiFlexGroup,
EuiFlexItem,
EuiText,
EuiSpacer,
EuiToolTip,
} from '@elastic/eui';
export function StyleLegendRow({ header, minLabel, maxLabel, propertyLabel, fieldLabel }) {
return (
<div>
<EuiSpacer size="xs"/>
{header}
<EuiFlexGroup gutterSize="xs" justifyContent="spaceBetween">
<EuiFlexItem grow={true}>
<EuiText size="xs">
<small>{minLabel}</small>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiToolTip
position="top"
title={propertyLabel}
content={fieldLabel}
>
<EuiText
className="eui-textTruncate"
size="xs"
style={{ maxWidth: '180px' }}
>
<small><strong>{fieldLabel}</strong></small>
</EuiText>
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem grow={true}>
<EuiText textAlign="right" size="xs">
<small>{maxLabel}</small>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</div>
);
}
StyleLegendRow.propTypes = {
header: PropTypes.node.isRequired,
minLabel: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]).isRequired,
maxLabel: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]).isRequired,
propertyLabel: PropTypes.string.isRequired,
fieldLabel: PropTypes.string.isRequired,
};

View file

@ -8,14 +8,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { EuiSuperSelect } from '@elastic/eui';
import { vislibColorMaps } from 'ui/vislib/components/color/colormaps';
import { ColorGradient } from '../../../../../icons/color_gradient';
export const COLOR_GRADIENTS = Object.keys(vislibColorMaps).map(colorKey => ({
value: colorKey,
text: colorKey,
inputDisplay: <ColorGradient color={colorKey}/>
}));
import { COLOR_GRADIENTS } from '../../../color_utils';
export function ColorRampSelect({ color, onChange }) {
const onColorRampChange = (selectedColorRampString) => {

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
export const LineIcon = ({ style }) => (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<line x1="0" y1="6" x2="16" y2="6" style={style} />
</svg>
);

View file

@ -4,10 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
export const FillableCircle = ({ style }) => (
export const PointIcon = ({ style }) => (
<svg
className="euiIcon euiIcon--medium mapFillableCircle"
xmlns="http://www.w3.org/2000/svg"
@ -43,20 +42,3 @@ export const FillableCircle = ({ style }) => (
</g>
</svg>
);
export const FillableRectangle = ({ style }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
>
<rect width="15" height="15" x=".5" y=".5" style={style} rx="4"/>
</svg>
);
export const ColorableLine = ({ style }) => (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<line x1="0" y1="6" x2="16" y2="6" style={style} />
</svg>
);

View file

@ -0,0 +1,18 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
export const PolygonIcon = ({ style }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
>
<rect width="15" height="15" x=".5" y=".5" style={style} rx="4"/>
</svg>
);

View file

@ -10,17 +10,11 @@ import PropTypes from 'prop-types';
import { styleOptionShapes, rangeShape } from '../style_option_shapes';
import { VectorStyle } from '../../../vector_style';
import { ColorGradient } from '../../../../../icons/color_gradient';
import { FillableCircle } from '../../../../../icons/additional_layer_icons';
import { ColorGradient } from '../../color_gradient';
import { PointIcon } from './point_icon';
import { getVectorStyleLabel } from '../get_vector_style_label';
import {
EuiFlexGroup,
EuiFlexItem,
EuiText,
EuiSpacer,
EuiToolTip,
EuiHorizontalRule,
} from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui';
import { StyleLegendRow } from '../../style_legend_row';
function getLineWidthIcons() {
const defaultStyle = {
@ -29,9 +23,9 @@ function getLineWidthIcons() {
width: '12px',
};
return [
<FillableCircle style={{ ...defaultStyle, strokeWidth: '1px' }}/>,
<FillableCircle style={{ ...defaultStyle, strokeWidth: '2px' }}/>,
<FillableCircle style={{ ...defaultStyle, strokeWidth: '3px' }}/>,
<PointIcon style={{ ...defaultStyle, strokeWidth: '1px' }}/>,
<PointIcon style={{ ...defaultStyle, strokeWidth: '2px' }}/>,
<PointIcon style={{ ...defaultStyle, strokeWidth: '3px' }}/>,
];
}
@ -42,9 +36,9 @@ function getSymbolSizeIcons() {
fill: 'grey',
};
return [
<FillableCircle style={{ ...defaultStyle, width: '4px' }}/>,
<FillableCircle style={{ ...defaultStyle, width: '8px' }}/>,
<FillableCircle style={{ ...defaultStyle, width: '12px' }}/>,
<PointIcon style={{ ...defaultStyle, width: '4px' }}/>,
<PointIcon style={{ ...defaultStyle, width: '8px' }}/>,
<PointIcon style={{ ...defaultStyle, width: '12px' }}/>,
];
}
@ -84,7 +78,7 @@ export function StylePropertyLegendRow({ name, type, options, range }) {
let header;
if (options.color) {
header = <ColorGradient color={options.color}/>;
header = <ColorGradient colorRampName={options.color}/>;
} else if (name === 'lineWidth') {
header = renderHeaderWithIcons(getLineWidthIcons());
} else if (name === 'iconSize') {
@ -92,37 +86,13 @@ export function StylePropertyLegendRow({ name, type, options, range }) {
}
return (
<div>
<EuiSpacer size="xs"/>
{header}
<EuiFlexGroup gutterSize="xs" justifyContent="spaceBetween">
<EuiFlexItem grow={true}>
<EuiText size="xs">
<small>{_.get(range, 'min', '')}</small>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiToolTip
position="top"
title={getVectorStyleLabel(name)}
content={options.field.label}
>
<EuiText
className="eui-textTruncate"
size="xs"
style={{ maxWidth: '180px' }}
>
<small><strong>{options.field.label}</strong></small>
</EuiText>
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem grow={true}>
<EuiText textAlign="right" size="xs">
<small>{_.get(range, 'max', '')}</small>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</div>
<StyleLegendRow
header={header}
minLabel={_.get(range, 'min', '')}
maxLabel={_.get(range, 'max', '')}
propertyLabel={getVectorStyleLabel(name)}
fieldLabel={options.field.label}
/>
);
}

View file

@ -8,9 +8,11 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { dynamicColorShape, staticColorShape } from '../style_option_shapes';
import { ColorableLine, FillableCircle, FillableRectangle } from '../../../../../icons/additional_layer_icons';
import { PointIcon } from './point_icon';
import { LineIcon } from './line_icon';
import { PolygonIcon } from './polygon_icon';
import { VectorStyle } from '../../../vector_style';
import { getColorRampCenterColor } from '../../../../../utils/color_utils';
import { getColorRampCenterColor } from '../../../color_utils';
export class VectorIcon extends Component {
@ -50,7 +52,7 @@ export class VectorIcon extends Component {
strokeWidth: '4px',
};
return (
<ColorableLine style={style}/>
<LineIcon style={style}/>
);
}
@ -61,8 +63,8 @@ export class VectorIcon extends Component {
};
return this.state.isPointsOnly
? <FillableCircle style={style}/>
: <FillableRectangle style={style}/>;
? <PointIcon style={style}/>
: <PolygonIcon style={style}/>;
}
}

View file

@ -4,22 +4,28 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { GRID_RESOLUTION } from '../grid_resolution';
import { AbstractStyle } from './abstract_style';
import { HeatmapStyleEditor } from './components/heatmap/heatmap_style_editor';
import { HeatmapLegend } from './components/heatmap/legend/heatmap_legend';
import { DEFAULT_HEATMAP_COLOR_RAMP_NAME } from './components/heatmap/heatmap_constants';
import { getColorRampStops } from './color_utils';
import { i18n } from '@kbn/i18n';
export class HeatmapStyle extends AbstractStyle {
static type = 'HEATMAP';
constructor() {
constructor(descriptor = {}) {
super();
this._descriptor = HeatmapStyle.createDescriptor();
this._descriptor = HeatmapStyle.createDescriptor(descriptor.colorRampName);
}
static createDescriptor() {
static createDescriptor(colorRampName) {
return {
type: HeatmapStyle.type,
colorRampName: colorRampName ? colorRampName : DEFAULT_HEATMAP_COLOR_RAMP_NAME,
};
}
@ -29,6 +35,29 @@ export class HeatmapStyle extends AbstractStyle {
});
}
renderEditor({ onStyleDescriptorChange }) {
const onHeatmapColorChange = ({ colorRampName }) => {
const styleDescriptor = HeatmapStyle.createDescriptor(colorRampName);
onStyleDescriptorChange(styleDescriptor);
};
return (
<HeatmapStyleEditor
colorRampName={this._descriptor.colorRampName}
onHeatmapColorChange={onHeatmapColorChange}
/>
);
}
getLegendDetails(label) {
return (
<HeatmapLegend
colorRampName={this._descriptor.colorRampName}
label={label}
/>
);
}
setMBPaintProperties({ mbMap, layerId, propertyName, resolution }) {
let radius;
if (resolution === GRID_RESOLUTION.COARSE) {
@ -49,7 +78,29 @@ export class HeatmapStyle extends AbstractStyle {
type: 'identity',
property: propertyName
});
const { colorRampName } = this._descriptor;
if (colorRampName && colorRampName !== DEFAULT_HEATMAP_COLOR_RAMP_NAME) {
const colorStops = getColorRampStops(colorRampName);
mbMap.setPaintProperty(layerId, 'heatmap-color', [
'interpolate',
['linear'],
['heatmap-density'],
0, 'rgba(0, 0, 255, 0)',
...colorStops.slice(2) // remove first stop from colorStops to avoid conflict with transparent stop at zero
]);
} else {
mbMap.setPaintProperty(layerId, 'heatmap-color', [
'interpolate',
['linear'],
['heatmap-density'],
0, 'rgba(0, 0, 255, 0)',
0.1, 'royalblue',
0.3, 'cyan',
0.5, 'lime',
0.7, 'yellow',
1, 'red'
]);
}
}
}

View file

@ -7,7 +7,7 @@
import _ from 'lodash';
import React from 'react';
import { i18n } from '@kbn/i18n';
import { getHexColorRangeStrings } from '../../utils/color_utils';
import { getColorRampStops } from './color_utils';
import { VectorStyleEditor } from './components/vector/vector_style_editor';
import { getDefaultStaticProperties } from './vector_style_defaults';
import { AbstractStyle } from './abstract_style';
@ -372,18 +372,14 @@ export class VectorStyle extends AbstractStyle {
}
_getMBDataDrivenColor({ fieldName, color }) {
const colorRange = getHexColorRangeStrings(color, 8)
.reduce((accu, curColor, idx, srcArr) => {
accu = [ ...accu, idx / srcArr.length, curColor ];
return accu;
}, []);
const colorStops = getColorRampStops(color);
const targetName = VectorStyle.getComputedFieldName(fieldName);
return [
'interpolate',
['linear'],
['coalesce', ['feature-state', targetName], -1],
-1, 'rgba(0,0,0,0)',
...colorRange
...colorStops
];
}

View file

@ -5,7 +5,7 @@
*/
import { VectorStyle } from './vector_style';
import { COLOR_GRADIENTS } from './components/vector/color/color_ramp_select';
import { COLOR_GRADIENTS } from './color_utils';
const DEFAULT_COLORS = ['#e6194b', '#3cb44b', '#ffe119', '#f58231', '#911eb4'];

View file

@ -1,33 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { vislibColorMaps } from 'ui/vislib/components/color/colormaps';
import { getLegendColors, getColor } from 'ui/vis/map/color_util';
import chroma from 'chroma-js';
function getColorRamp(colorRampName) {
const colorRamp = vislibColorMaps[colorRampName];
if (!colorRamp) {
throw new Error(`${colorRampName} not found. Expected one of following values: ${Object.keys(vislibColorMaps)}`);
}
return colorRamp;
}
export function getRGBColorRangeStrings(colorRampName, numberColors) {
const colorRamp = getColorRamp(colorRampName);
return getLegendColors(colorRamp.value, numberColors);
}
export function getHexColorRangeStrings(colorRampName, numberColors) {
return getRGBColorRangeStrings(colorRampName, numberColors)
.map(rgbColor => chroma(rgbColor).hex());
}
export function getColorRampCenterColor(colorRampName) {
const colorRamp = getColorRamp(colorRampName);
const centerIndex = Math.floor(colorRamp.value.length / 2);
return getColor(colorRamp.value, centerIndex);
}