[Maps] display ranged-data with bands (#60570) (#67497)

This commit is contained in:
Thomas Neirynck 2020-05-27 13:24:52 -04:00 committed by GitHub
parent 78ee77fb75
commit deab3ca318
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 960 additions and 562 deletions

View file

@ -30,6 +30,7 @@ export interface IVectorLayer extends ILayer {
getJoins(): IJoin[];
getValidJoins(): IJoin[];
getSource(): IVectorSource;
getStyle(): IVectorStyle;
}
export class VectorLayer extends AbstractLayer implements IVectorLayer {
@ -73,4 +74,5 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
_setMbPointsProperties(mbMap: unknown, mvtSourceLayer?: string): void;
_setMbLinePolygonProperties(mbMap: unknown, mvtSourceLayer?: string): void;
getSource(): IVectorSource;
getStyle(): IVectorStyle;
}

View file

@ -2,3 +2,5 @@
@import 'vector/components/style_prop_editor';
@import 'vector/components/color/color_stops';
@import 'vector/components/symbol/icon_select';
@import 'vector/components/legend/category';
@import 'vector/components/legend/vector_legend';

View file

@ -11,7 +11,7 @@ import { euiPaletteColorBlind } from '@elastic/eui/lib/services';
import { ColorGradient } from './components/color_gradient';
import { vislibColorMaps } from '../../../../../../src/plugins/charts/public';
const GRADIENT_INTERVALS = 8;
export const GRADIENT_INTERVALS = 8;
export const DEFAULT_FILL_COLORS = euiPaletteColorBlind();
export const DEFAULT_LINE_COLORS = [
@ -73,7 +73,7 @@ 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, min, max) {
export function getOrdinalMbColorRampStops(colorRampName, min, max, numberColors) {
if (!colorRampName) {
return null;
}
@ -82,7 +82,7 @@ export function getOrdinalColorRampStops(colorRampName, min, max) {
return null;
}
const hexColors = getHexColorRangeStrings(colorRampName, GRADIENT_INTERVALS);
const hexColors = getHexColorRangeStrings(colorRampName, numberColors);
if (max === min) {
//just return single stop value
return [max, hexColors[hexColors.length - 1]];

View file

@ -3,11 +3,10 @@
* 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,
getOrdinalColorRampStops,
getOrdinalMbColorRampStops,
getHexColorRangeStrings,
getLinearGradient,
getRGBColorRangeStrings,
@ -25,7 +24,7 @@ describe('COLOR_GRADIENTS', () => {
describe('getRGBColorRangeStrings', () => {
it('Should create RGB color ramp', () => {
expect(getRGBColorRangeStrings('Blues')).toEqual([
expect(getRGBColorRangeStrings('Blues', 8)).toEqual([
'rgb(247,250,255)',
'rgb(221,234,247)',
'rgb(197,218,238)',
@ -61,7 +60,7 @@ describe('getColorRampCenterColor', () => {
describe('getColorRampStops', () => {
it('Should create color stops for custom range', () => {
expect(getOrdinalColorRampStops('Blues', 0, 1000)).toEqual([
expect(getOrdinalMbColorRampStops('Blues', 0, 1000, 8)).toEqual([
0,
'#f7faff',
125,
@ -82,7 +81,7 @@ describe('getColorRampStops', () => {
});
it('Should snap to end of color stops for identical range', () => {
expect(getOrdinalColorRampStops('Blues', 23, 23)).toEqual([23, '#072f6b']);
expect(getOrdinalMbColorRampStops('Blues', 23, 23, 8)).toEqual([23, '#072f6b']);
});
});

View file

@ -5,7 +5,12 @@
*/
import React from 'react';
import { COLOR_RAMP_NAMES, getRGBColorRangeStrings, getLinearGradient } from '../color_utils';
import {
COLOR_RAMP_NAMES,
GRADIENT_INTERVALS,
getRGBColorRangeStrings,
getLinearGradient,
} from '../color_utils';
import classNames from 'classnames';
export const ColorGradient = ({ colorRamp, colorRampName, className }) => {
@ -14,7 +19,9 @@ export const ColorGradient = ({ colorRamp, colorRampName, className }) => {
}
const classes = classNames('mapColorGradient', className);
const rgbColorStrings = colorRampName ? getRGBColorRangeStrings(colorRampName) : colorRamp;
const rgbColorStrings = colorRampName
? getRGBColorRangeStrings(colorRampName, GRADIENT_INTERVALS)
: colorRamp;
const background = getLinearGradient(rgbColorStrings);
return <div className={classes} style={{ background }} />;
};

View file

@ -7,19 +7,12 @@
import React from 'react';
import PropTypes from 'prop-types';
import { EuiFlexGroup, EuiFlexItem, EuiText, EuiSpacer, EuiToolTip } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui';
export function RangedStyleLegendRow({ 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' }}>
@ -29,6 +22,14 @@ export function RangedStyleLegendRow({ header, minLabel, maxLabel, propertyLabel
</EuiText>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
{header}
<EuiFlexGroup gutterSize="xs" justifyContent="spaceBetween">
<EuiFlexItem grow={true}>
<EuiText size="xs">
<small>{minLabel}</small>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={true}>
<EuiText textAlign="right" size="xs">
<small>{maxLabel}</small>

View file

@ -10,7 +10,7 @@ import { HeatmapStyleEditor } from './components/heatmap_style_editor';
import { HeatmapLegend } from './components/legend/heatmap_legend';
import { DEFAULT_HEATMAP_COLOR_RAMP_NAME } from './components/heatmap_constants';
import { LAYER_STYLE_TYPE, GRID_RESOLUTION } from '../../../../common/constants';
import { getOrdinalColorRampStops } from '../color_utils';
import { getOrdinalMbColorRampStops, GRADIENT_INTERVALS } from '../color_utils';
import { i18n } from '@kbn/i18n';
import { EuiIcon } from '@elastic/eui';
@ -85,7 +85,12 @@ export class HeatmapStyle extends AbstractStyle {
const { colorRampName } = this._descriptor;
if (colorRampName && colorRampName !== DEFAULT_HEATMAP_COLOR_RAMP_NAME) {
const colorStops = getOrdinalColorRampStops(colorRampName, MIN_RANGE, MAX_RANGE);
const colorStops = getOrdinalMbColorRampStops(
colorRampName,
MIN_RANGE,
MAX_RANGE,
GRADIENT_INTERVALS
);
mbMap.setPaintProperty(layerId, 'heatmap-color', [
'interpolate',
['linear'],

View file

@ -0,0 +1,3 @@
.mapLegendIconPreview {
width: $euiSizeL;
}

View file

@ -0,0 +1,5 @@
.vectorStyleLegendSpacer {
&:not(:last-child) {
margin-bottom: $euiSizeS;
}
}

View file

@ -0,0 +1,82 @@
/*
* 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 _ from 'lodash';
import { EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui';
import { Category } from './category';
const EMPTY_VALUE = '';
export class BreakedLegend extends React.Component {
state = {
label: EMPTY_VALUE,
};
componentDidMount() {
this._isMounted = true;
this._loadParams();
}
componentDidUpdate() {
this._loadParams();
}
componentWillUnmount() {
this._isMounted = false;
}
async _loadParams() {
const label = await this.props.style.getField().getLabel();
const newState = { label };
if (this._isMounted && !_.isEqual(this.state, newState)) {
this.setState(newState);
}
}
render() {
if (this.state.label === EMPTY_VALUE) {
return null;
}
const categories = this.props.breaks.map((brk, index) => {
return (
<EuiFlexItem key={index}>
<Category
styleName={this.props.style.getStyleName()}
label={brk.label}
color={brk.color}
isLinesOnly={this.props.isLinesOnly}
isPointsOnly={this.props.isPointsOnly}
symbolId={brk.symbolId}
/>
</EuiFlexItem>
);
});
return (
<div>
<EuiFlexGroup gutterSize="xs" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiToolTip
position="top"
title={this.props.style.getDisplayStyleName()}
content={this.state.label}
>
<EuiText className="eui-textTruncate" size="xs" style={{ maxWidth: '180px' }}>
<small>
<strong>{this.state.label}</strong>
</small>
</EuiText>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup direction="column" gutterSize="none">
{categories}
</EuiFlexGroup>
</div>
);
}
}

View file

@ -31,13 +31,13 @@ export function Category({ styleName, label, color, isLinesOnly, isPointsOnly, s
}
return (
<EuiFlexItem>
<EuiFlexGroup direction="row" gutterSize="none">
<EuiFlexItem>
<EuiText size="xs">{label}</EuiText>
</EuiFlexItem>
<EuiFlexItem>{renderIcon()}</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexGroup direction="row" gutterSize="none">
<EuiFlexItem className="mapLegendIconPreview" grow={false}>
{renderIcon()}
</EuiFlexItem>
<EuiFlexItem>
<EuiText size="xs">{label}</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
}

View file

@ -4,9 +4,37 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import React, { Fragment } from 'react';
import _ from 'lodash';
import { RangedStyleLegendRow } from '../../../components/ranged_style_legend_row';
import { VECTOR_STYLES } from '../../../../../../common/constants';
import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui';
import { CircleIcon } from './circle_icon';
function getLineWidthIcons() {
const defaultStyle = {
stroke: 'grey',
fill: 'none',
width: '12px',
};
return [
<CircleIcon style={{ ...defaultStyle, strokeWidth: '1px' }} />,
<CircleIcon style={{ ...defaultStyle, strokeWidth: '2px' }} />,
<CircleIcon style={{ ...defaultStyle, strokeWidth: '3px' }} />,
];
}
function getSymbolSizeIcons() {
const defaultStyle = {
stroke: 'grey',
fill: 'grey',
};
return [
<CircleIcon style={{ ...defaultStyle, width: '4px' }} />,
<CircleIcon style={{ ...defaultStyle, width: '8px' }} />,
<CircleIcon style={{ ...defaultStyle, width: '12px' }} />,
];
}
const EMPTY_VALUE = '';
export class OrdinalLegend extends React.Component {
@ -45,7 +73,46 @@ export class OrdinalLegend extends React.Component {
this._isMounted = true;
this._loadParams();
}
_renderRangeLegendHeader() {
let icons;
if (this.props.style.getStyleName() === VECTOR_STYLES.LINE_WIDTH) {
icons = getLineWidthIcons();
} else if (this.props.style.getStyleName() === VECTOR_STYLES.ICON_SIZE) {
icons = getSymbolSizeIcons();
} else {
return null;
}
return (
<EuiFlexGroup gutterSize="xs" justifyContent="spaceBetween" alignItems="center">
{icons.map((icon, index) => {
const isLast = index === icons.length - 1;
let spacer;
if (!isLast) {
spacer = (
<EuiFlexItem>
<EuiHorizontalRule margin="xs" />
</EuiFlexItem>
);
}
return (
<Fragment key={index}>
<EuiFlexItem grow={false}>{icon}</EuiFlexItem>
{spacer}
</Fragment>
);
})}
</EuiFlexGroup>
);
}
render() {
const header = this._renderRangeLegendHeader();
if (!header) {
return null;
}
const fieldMeta = this.props.style.getRangeFieldMeta();
let minLabel = EMPTY_VALUE;
@ -67,7 +134,7 @@ export class OrdinalLegend extends React.Component {
return (
<RangedStyleLegendRow
header={this.props.style.renderRangeLegendHeader()}
header={header}
minLabel={minLabel}
maxLabel={maxLabel}
propertyLabel={this.props.style.getDisplayStyleName()}

View file

@ -4,18 +4,24 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment } from 'react';
import React from 'react';
export function VectorStyleLegend({ isLinesOnly, isPointsOnly, styles, symbolId }) {
return styles.map((style) => {
return (
<Fragment key={style.getStyleName()}>
{style.renderLegendDetailRow({
isLinesOnly,
isPointsOnly,
symbolId,
})}
</Fragment>
const legendRows = [];
for (let i = 0; i < styles.length; i++) {
const row = styles[i].renderLegendDetailRow({
isLinesOnly,
isPointsOnly,
symbolId,
});
legendRows.push(
<div key={i} className="vectorStyleLegendSpacer">
{row}
</div>
);
});
}
return legendRows;
}

View file

@ -1,50 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Should render categorical legend with breaks from custom 1`] = `""`;
exports[`categorical Should render categorical legend with breaks from custom 1`] = `""`;
exports[`Should render categorical legend with breaks from default 1`] = `
exports[`categorical Should render categorical legend with breaks from default 1`] = `
<div>
<EuiSpacer
size="s"
/>
<EuiFlexGroup
direction="column"
gutterSize="none"
>
<Category
color="#54B399"
isLinesOnly={false}
isPointsOnly={true}
key="US"
label="US_format"
styleName="lineColor"
/>
<Category
color="#6092C0"
isLinesOnly={false}
isPointsOnly={true}
key="CN"
label="CN_format"
styleName="lineColor"
/>
<Category
color="#D36086"
isLinesOnly={false}
isPointsOnly={true}
key="fallbackCategory"
label={
<EuiTextColor
color="secondary"
>
Other
</EuiTextColor>
}
styleName="lineColor"
/>
</EuiFlexGroup>
<EuiFlexGroup
gutterSize="xs"
justifyContent="spaceAround"
justifyContent="spaceBetween"
>
<EuiFlexItem
grow={false}
@ -73,52 +35,58 @@ exports[`Should render categorical legend with breaks from default 1`] = `
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
</div>
`;
exports[`Should render ordinal legend 1`] = `
<RangedStyleLegendRow
fieldLabel=""
header={
<ColorGradient
colorRampName="Blues"
/>
}
maxLabel="100_format"
minLabel="0_format"
propertyLabel="Border color"
/>
`;
exports[`Should render ordinal legend with breaks 1`] = `
<div>
<EuiSpacer
size="s"
/>
<EuiFlexGroup
direction="column"
gutterSize="none"
>
<Category
color="#FF0000"
isLinesOnly={false}
isPointsOnly={true}
<EuiFlexItem
key="0"
label="0_format"
styleName="lineColor"
/>
<Category
color="#00FF00"
isLinesOnly={false}
isPointsOnly={true}
key="10"
label="10_format"
styleName="lineColor"
/>
>
<Category
color="#54B399"
isLinesOnly={false}
isPointsOnly={true}
label="US_format"
styleName="lineColor"
/>
</EuiFlexItem>
<EuiFlexItem
key="1"
>
<Category
color="#6092C0"
isLinesOnly={false}
isPointsOnly={true}
label="CN_format"
styleName="lineColor"
/>
</EuiFlexItem>
<EuiFlexItem
key="2"
>
<Category
color="#D36086"
isLinesOnly={false}
isPointsOnly={true}
label={
<EuiTextColor
color="secondary"
>
Other
</EuiTextColor>
}
styleName="lineColor"
/>
</EuiFlexItem>
</EuiFlexGroup>
</div>
`;
exports[`ordinal Should render custom ordinal legend with breaks 1`] = `
<div>
<EuiFlexGroup
gutterSize="xs"
justifyContent="spaceAround"
justifyContent="spaceBetween"
>
<EuiFlexItem
grow={false}
@ -147,5 +115,191 @@ exports[`Should render ordinal legend with breaks 1`] = `
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
direction="column"
gutterSize="none"
>
<EuiFlexItem
key="0"
>
<Category
color="#00FF00"
isLinesOnly={false}
isPointsOnly={true}
label="10_format"
styleName="lineColor"
/>
</EuiFlexItem>
</EuiFlexGroup>
</div>
`;
exports[`ordinal Should render only single band of last color when delta is 0 1`] = `
<div>
<EuiFlexGroup
gutterSize="xs"
justifyContent="spaceBetween"
>
<EuiFlexItem
grow={false}
>
<EuiToolTip
content="foobar_label"
delay="regular"
position="top"
title="Border color"
>
<EuiText
className="eui-textTruncate"
size="xs"
style={
Object {
"maxWidth": "180px",
}
}
>
<small>
<strong>
foobar_label
</strong>
</small>
</EuiText>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
direction="column"
gutterSize="none"
>
<EuiFlexItem
key="0"
>
<Category
color="#072f6b"
isLinesOnly={false}
isPointsOnly={true}
label="100_format"
styleName="lineColor"
/>
</EuiFlexItem>
</EuiFlexGroup>
</div>
`;
exports[`ordinal Should render ordinal legend as bands 1`] = `
<div>
<EuiFlexGroup
gutterSize="xs"
justifyContent="spaceBetween"
>
<EuiFlexItem
grow={false}
>
<EuiToolTip
content="foobar_label"
delay="regular"
position="top"
title="Border color"
>
<EuiText
className="eui-textTruncate"
size="xs"
style={
Object {
"maxWidth": "180px",
}
}
>
<small>
<strong>
foobar_label
</strong>
</small>
</EuiText>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
direction="column"
gutterSize="none"
>
<EuiFlexItem
key="0"
>
<Category
color="#ddeaf7"
isLinesOnly={false}
isPointsOnly={true}
label="13_format"
styleName="lineColor"
/>
</EuiFlexItem>
<EuiFlexItem
key="1"
>
<Category
color="#c5daee"
isLinesOnly={false}
isPointsOnly={true}
label="25_format"
styleName="lineColor"
/>
</EuiFlexItem>
<EuiFlexItem
key="2"
>
<Category
color="#9dc9e0"
isLinesOnly={false}
isPointsOnly={true}
label="38_format"
styleName="lineColor"
/>
</EuiFlexItem>
<EuiFlexItem
key="3"
>
<Category
color="#6aadd5"
isLinesOnly={false}
isPointsOnly={true}
label="50_format"
styleName="lineColor"
/>
</EuiFlexItem>
<EuiFlexItem
key="4"
>
<Category
color="#4191c5"
isLinesOnly={false}
isPointsOnly={true}
label="63_format"
styleName="lineColor"
/>
</EuiFlexItem>
<EuiFlexItem
key="5"
>
<Category
color="#2070b4"
isLinesOnly={false}
isPointsOnly={true}
label="75_format"
styleName="lineColor"
/>
</EuiFlexItem>
<EuiFlexItem
key="6"
>
<Category
color="#072f6b"
isLinesOnly={false}
isPointsOnly={true}
label="88_format"
styleName="lineColor"
/>
</EuiFlexItem>
</EuiFlexGroup>
</div>
`;

View file

@ -2,50 +2,9 @@
exports[`Should render categorical legend with breaks 1`] = `
<div>
<EuiSpacer
size="s"
/>
<EuiFlexGroup
direction="column"
gutterSize="none"
>
<Category
color="grey"
isLinesOnly={false}
isPointsOnly={true}
key="US"
label="US_format"
styleName="icon"
symbolId="circle"
/>
<Category
color="grey"
isLinesOnly={false}
isPointsOnly={true}
key="CN"
label="CN_format"
styleName="icon"
symbolId="marker"
/>
<Category
color="grey"
isLinesOnly={false}
isPointsOnly={true}
key="fallbackCategory"
label={
<EuiTextColor
color="secondary"
>
Other
</EuiTextColor>
}
styleName="icon"
symbolId="square"
/>
</EuiFlexGroup>
<EuiFlexGroup
gutterSize="xs"
justifyContent="spaceAround"
justifyContent="spaceBetween"
>
<EuiFlexItem
grow={false}
@ -74,5 +33,34 @@ exports[`Should render categorical legend with breaks 1`] = `
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
direction="column"
gutterSize="none"
>
<EuiFlexItem
key="0"
>
<Category
color="grey"
isLinesOnly={false}
isPointsOnly={true}
label="US_format"
styleName="icon"
symbolId="circle"
/>
</EuiFlexItem>
<EuiFlexItem
key="1"
>
<Category
color="grey"
isLinesOnly={false}
isPointsOnly={true}
label="CN_format"
styleName="icon"
symbolId="marker"
/>
</EuiFlexItem>
</EuiFlexGroup>
</div>
`;

View file

@ -0,0 +1,73 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renderLegendDetailRow Should render as range 1`] = `
<RangedStyleLegendRow
fieldLabel="foobar_label"
header={
<ForwardRef
alignItems="center"
gutterSize="xs"
justifyContent="spaceBetween"
>
<React.Fragment>
<EuiFlexItem
grow={false}
>
<CircleIcon
style={
Object {
"fill": "grey",
"stroke": "grey",
"width": "4px",
}
}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiHorizontalRule
margin="xs"
/>
</EuiFlexItem>
</React.Fragment>
<React.Fragment>
<EuiFlexItem
grow={false}
>
<CircleIcon
style={
Object {
"fill": "grey",
"stroke": "grey",
"width": "8px",
}
}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiHorizontalRule
margin="xs"
/>
</EuiFlexItem>
</React.Fragment>
<React.Fragment>
<EuiFlexItem
grow={false}
>
<CircleIcon
style={
Object {
"fill": "grey",
"stroke": "grey",
"width": "12px",
}
}
/>
</EuiFlexItem>
</React.Fragment>
</ForwardRef>
}
maxLabel="100_format"
minLabel="0_format"
propertyLabel="Symbol size"
/>
`;

View file

@ -14,6 +14,7 @@ import {
StyleMetaDescriptor,
} from '../../../../../../common/descriptor_types';
import { AbstractField, IField } from '../../../../fields/field';
import { IStyle, AbstractStyle } from '../../../style';
class MockField extends AbstractField {
async getLabel(): Promise<string> {
@ -29,14 +30,27 @@ export const mockField: IField = new MockField({
origin: FIELD_ORIGIN.SOURCE,
});
class MockStyle {
export class MockStyle extends AbstractStyle implements IStyle {
private readonly _min: number;
private readonly _max: number;
constructor({ min = 0, max = 100 } = {}) {
super(null);
this._min = min;
this._max = max;
}
getStyleMeta(): StyleMeta {
const geomTypes: GeometryTypes = {
isPointsOnly: false,
isLinesOnly: false,
isPolygonsOnly: false,
};
const rangeFieldMeta: RangeFieldMeta = { min: 0, max: 100, delta: 100 };
const rangeFieldMeta: RangeFieldMeta = {
min: this._min,
max: this._max,
delta: this._max - this._min,
};
const catFieldMeta: CategoryFieldMeta = {
categories: [
{
@ -65,8 +79,12 @@ class MockStyle {
}
export class MockLayer {
private readonly _style: IStyle;
constructor(style = new MockStyle()) {
this._style = style;
}
getStyle() {
return new MockStyle();
return this._style;
}
getDataRequest() {

View file

@ -1,48 +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 _ from 'lodash';
const EMPTY_VALUE = '';
export class CategoricalLegend extends React.Component {
state = {
label: EMPTY_VALUE,
};
componentDidMount() {
this._isMounted = true;
this._loadParams();
}
componentDidUpdate() {
this._loadParams();
}
componentWillUnmount() {
this._isMounted = false;
}
async _loadParams() {
const label = await this.props.style.getField().getLabel();
const newState = { label };
if (this._isMounted && !_.isEqual(this.state, newState)) {
this.setState(newState);
}
}
render() {
if (this.state.label === EMPTY_VALUE) {
return null;
}
return this.props.style.renderBreakedLegend({
fieldLabel: this.state.label,
isLinesOnly: this.props.isLinesOnly,
isPointsOnly: this.props.isPointsOnly,
symbolId: this.props.symbolId,
});
}
}

View file

@ -5,23 +5,24 @@
*/
import { DynamicStyleProperty } from './dynamic_style_property';
import { getOtherCategoryLabel, makeMbClampedNumberExpression } from '../style_util';
import { getOrdinalColorRampStops, getColorPalette } from '../../color_utils';
import { ColorGradient } from '../../components/color_gradient';
import React from 'react';
import { makeMbClampedNumberExpression, dynamicRound } from '../style_util';
import {
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiText,
EuiToolTip,
EuiTextColor,
} from '@elastic/eui';
import { Category } from '../components/legend/category';
import { COLOR_MAP_TYPE, RGBA_0000 } from '../../../../../common/constants';
import { isCategoricalStopsInvalid } from '../components/color/color_stops_utils';
getOrdinalMbColorRampStops,
getColorPalette,
getHexColorRangeStrings,
GRADIENT_INTERVALS,
} from '../../color_utils';
import React from 'react';
import { COLOR_MAP_TYPE } from '../../../../../common/constants';
import {
isCategoricalStopsInvalid,
getOtherCategoryLabel,
} from '../components/color/color_stops_utils';
import { BreakedLegend } from '../components/legend/breaked_legend';
import { EuiTextColor } from '@elastic/eui';
const EMPTY_STOPS = { stops: [], defaultColor: null };
const RGBA_0000 = 'rgba(0,0,0,0)';
export class DynamicColorProperty extends DynamicStyleProperty {
syncCircleColorWithMb(mbLayerId, mbMap, alpha) {
@ -99,14 +100,6 @@ export class DynamicColorProperty extends DynamicStyleProperty {
return true;
}
isOrdinalRanged() {
return this.isOrdinal() && !this._options.useCustomColorRamp;
}
hasOrdinalBreaks() {
return (this.isOrdinal() && this._options.useCustomColorRamp) || this.isCategorical();
}
_getMbColor() {
if (!this._field || !this._field.getName()) {
return null;
@ -142,10 +135,11 @@ export class DynamicColorProperty extends DynamicStyleProperty {
return null;
}
const colorStops = getOrdinalColorRampStops(
const colorStops = getOrdinalMbColorRampStops(
this._options.color,
rangeFieldMeta.min,
rangeFieldMeta.max
rangeFieldMeta.max,
GRADIENT_INTERVALS
);
if (!colorStops) {
return null;
@ -237,28 +231,47 @@ export class DynamicColorProperty extends DynamicStyleProperty {
for (let i = 0; i < stops.length; i++) {
const stop = stops[i];
const branch = `${stop.stop}`;
if (typeof branch === 'string') {
mbStops.push(branch);
mbStops.push(stop.color);
}
mbStops.push(branch);
mbStops.push(stop.color);
}
mbStops.push(defaultColor); //last color is default color
return ['match', ['to-string', ['get', this._field.getName()]], ...mbStops];
}
renderRangeLegendHeader() {
if (this._options.color) {
return <ColorGradient colorRampName={this._options.color} />;
} else {
return null;
}
}
_getColorRampStops() {
return this._options.useCustomColorRamp && this._options.customColorRamp
? this._options.customColorRamp
: [];
if (this._options.useCustomColorRamp && this._options.customColorRamp) {
return this._options.customColorRamp;
}
if (!this._options.color) {
return [];
}
const rangeFieldMeta = this.getRangeFieldMeta();
if (!rangeFieldMeta) {
return [];
}
const colors = getHexColorRangeStrings(this._options.color, GRADIENT_INTERVALS);
if (rangeFieldMeta.delta === 0) {
//map to last color.
return [
{
color: colors[colors.length - 1],
stop: dynamicRound(rangeFieldMeta.max),
},
];
}
return colors.map((color, index) => {
const rawStopValue = rangeFieldMeta.min + rangeFieldMeta.delta * (index / GRADIENT_INTERVALS);
return {
color,
stop: dynamicRound(rawStopValue),
};
});
}
_getColorStops() {
@ -274,55 +287,33 @@ export class DynamicColorProperty extends DynamicStyleProperty {
}
}
renderBreakedLegend({ fieldLabel, isPointsOnly, isLinesOnly, symbolId }) {
const categories = [];
renderLegendDetailRow({ isPointsOnly, isLinesOnly, symbolId }) {
const { stops, defaultColor } = this._getColorStops();
stops.map(({ stop, color }) => {
categories.push(
<Category
key={stop}
styleName={this.getStyleName()}
label={this.formatField(stop)}
color={color}
isLinesOnly={isLinesOnly}
isPointsOnly={isPointsOnly}
symbolId={symbolId}
/>
);
const breaks = [];
stops.forEach(({ stop, color }) => {
if (stop) {
breaks.push({
color,
symbolId,
label: this.formatField(stop),
});
}
});
if (defaultColor) {
categories.push(
<Category
key="fallbackCategory"
styleName={this.getStyleName()}
label={<EuiTextColor color="secondary">{getOtherCategoryLabel()}</EuiTextColor>}
color={defaultColor}
isLinesOnly={isLinesOnly}
isPointsOnly={isPointsOnly}
symbolId={symbolId}
/>
);
breaks.push({
color: defaultColor,
label: <EuiTextColor color="secondary">{getOtherCategoryLabel()}</EuiTextColor>,
symbolId,
});
}
return (
<div>
<EuiSpacer size="s" />
<EuiFlexGroup direction="column" gutterSize="none">
{categories}
</EuiFlexGroup>
<EuiFlexGroup gutterSize="xs" justifyContent="spaceAround">
<EuiFlexItem grow={false}>
<EuiToolTip position="top" title={this.getDisplayStyleName()} content={fieldLabel}>
<EuiText className="eui-textTruncate" size="xs" style={{ maxWidth: '180px' }}>
<small>
<strong>{fieldLabel}</strong>
</small>
</EuiText>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
</div>
<BreakedLegend
style={this}
breaks={breaks}
isPointsOnly={isPointsOnly}
isLinesOnly={isLinesOnly}
/>
);
}
}

View file

@ -16,12 +16,18 @@ import { shallow } from 'enzyme';
import { DynamicColorProperty } from './dynamic_color_property';
import { COLOR_MAP_TYPE, VECTOR_STYLES } from '../../../../../common/constants';
import { mockField, MockLayer } from './__tests__/test_util';
import { mockField, MockLayer, MockStyle } from './__tests__/test_util';
const makeProperty = (options, field = mockField) => {
return new DynamicColorProperty(options, VECTOR_STYLES.LINE_COLOR, field, new MockLayer(), () => {
return (x) => x + '_format';
});
const makeProperty = (options, mockStyle, field = mockField) => {
return new DynamicColorProperty(
options,
VECTOR_STYLES.LINE_COLOR,
field,
new MockLayer(mockStyle),
() => {
return (x) => x + '_format';
}
);
};
const defaultLegendParams = {
@ -29,91 +35,121 @@ const defaultLegendParams = {
isLinesOnly: false,
};
test('Should render ordinal legend', async () => {
const colorStyle = makeProperty({
color: 'Blues',
type: undefined,
describe('ordinal', () => {
test('Should render ordinal legend as bands', async () => {
const colorStyle = makeProperty({
color: 'Blues',
type: undefined,
});
const legendRow = colorStyle.renderLegendDetailRow(defaultLegendParams);
const component = shallow(legendRow);
// Ensure all promises resolve
await new Promise((resolve) => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
expect(component).toMatchSnapshot();
});
const legendRow = colorStyle.renderLegendDetailRow(defaultLegendParams);
test('Should render only single band of last color when delta is 0', async () => {
const colorStyle = makeProperty(
{
color: 'Blues',
type: undefined,
},
new MockStyle({ min: 100, max: 100 })
);
const component = shallow(legendRow);
const legendRow = colorStyle.renderLegendDetailRow(defaultLegendParams);
expect(component).toMatchSnapshot();
const component = shallow(legendRow);
// Ensure all promises resolve
await new Promise((resolve) => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
expect(component).toMatchSnapshot();
});
test('Should render custom ordinal legend with breaks', async () => {
const colorStyle = makeProperty({
type: COLOR_MAP_TYPE.ORDINAL,
useCustomColorRamp: true,
customColorRamp: [
{
stop: 0,
color: '#FF0000',
},
{
stop: 10,
color: '#00FF00',
},
],
});
const legendRow = colorStyle.renderLegendDetailRow(defaultLegendParams);
const component = shallow(legendRow);
// Ensure all promises resolve
await new Promise((resolve) => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
expect(component).toMatchSnapshot();
});
});
test('Should render ordinal legend with breaks', async () => {
const colorStyle = makeProperty({
type: COLOR_MAP_TYPE.ORDINAL,
useCustomColorRamp: true,
customColorRamp: [
{
stop: 0,
color: '#FF0000',
},
{
stop: 10,
color: '#00FF00',
},
],
describe('categorical', () => {
test('Should render categorical legend with breaks from default', async () => {
const colorStyle = makeProperty({
type: COLOR_MAP_TYPE.CATEGORICAL,
useCustomColorPalette: false,
colorCategory: 'palette_0',
});
const legendRow = colorStyle.renderLegendDetailRow(defaultLegendParams);
const component = shallow(legendRow);
// Ensure all promises resolve
await new Promise((resolve) => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
expect(component).toMatchSnapshot();
});
const legendRow = colorStyle.renderLegendDetailRow(defaultLegendParams);
test('Should render categorical legend with breaks from custom', async () => {
const colorStyle = makeProperty({
type: COLOR_MAP_TYPE.CATEGORICAL,
useCustomColorPalette: true,
customColorPalette: [
{
stop: null, //should include the default stop
color: '#FFFF00',
},
{
stop: 'US_STOP',
color: '#FF0000',
},
{
stop: 'CN_STOP',
color: '#00FF00',
},
],
});
const component = shallow(legendRow);
const legendRow = colorStyle.renderLegendDetailRow(defaultLegendParams);
// Ensure all promises resolve
await new Promise((resolve) => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
const component = shallow(legendRow);
expect(component).toMatchSnapshot();
});
test('Should render categorical legend with breaks from default', async () => {
const colorStyle = makeProperty({
type: COLOR_MAP_TYPE.CATEGORICAL,
useCustomColorPalette: false,
colorCategory: 'palette_0',
expect(component).toMatchSnapshot();
});
const legendRow = colorStyle.renderLegendDetailRow(defaultLegendParams);
const component = shallow(legendRow);
// Ensure all promises resolve
await new Promise((resolve) => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
expect(component).toMatchSnapshot();
});
test('Should render categorical legend with breaks from custom', async () => {
const colorStyle = makeProperty({
type: COLOR_MAP_TYPE.CATEGORICAL,
useCustomColorPalette: true,
customColorPalette: [
{
stop: null, //should include the default stop
color: '#FFFF00',
},
{
stop: 'US_STOP',
color: '#FF0000',
},
{
stop: 'CN_STOP',
color: '#00FF00',
},
],
});
const legendRow = colorStyle.renderLegendDetailRow(defaultLegendParams);
const component = shallow(legendRow);
expect(component).toMatchSnapshot();
});
function makeFeatures(foobarPropValues) {
@ -201,7 +237,7 @@ describe('supportsFieldMeta', () => {
const dynamicStyleOptions = {
type: COLOR_MAP_TYPE.ORDINAL,
};
const styleProp = makeProperty(dynamicStyleOptions, field);
const styleProp = makeProperty(dynamicStyleOptions, undefined, field);
expect(styleProp.supportsFieldMeta()).toEqual(false);
});
@ -210,7 +246,7 @@ describe('supportsFieldMeta', () => {
const dynamicStyleOptions = {
type: COLOR_MAP_TYPE.ORDINAL,
};
const styleProp = makeProperty(dynamicStyleOptions, null);
const styleProp = makeProperty(dynamicStyleOptions, undefined, null);
expect(styleProp.supportsFieldMeta()).toEqual(false);
});

View file

@ -6,19 +6,11 @@
import _ from 'lodash';
import React from 'react';
import { getOtherCategoryLabel, assignCategoriesToPalette } from '../style_util';
import { DynamicStyleProperty } from './dynamic_style_property';
import { getIconPalette, getMakiIconId, getMakiSymbolAnchor } from '../symbol_utils';
import {
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiText,
EuiToolTip,
EuiTextColor,
} from '@elastic/eui';
import { Category } from '../components/legend/category';
import { BreakedLegend } from '../components/legend/breaked_legend';
import { getOtherCategoryLabel, assignCategoriesToPalette } from '../style_util';
import { EuiTextColor } from '@elastic/eui';
export class DynamicIconProperty extends DynamicStyleProperty {
isOrdinal() {
@ -60,7 +52,7 @@ export class DynamicIconProperty extends DynamicStyleProperty {
}
return {
fallback:
fallbackSymbolId:
this._options.customIconStops.length > 0 ? this._options.customIconStops[0].icon : null,
stops,
};
@ -73,9 +65,9 @@ export class DynamicIconProperty extends DynamicStyleProperty {
}
_getMbIconImageExpression(iconPixelSize) {
const { stops, fallback } = this._getPaletteStops();
const { stops, fallbackSymbolId } = this._getPaletteStops();
if (stops.length < 1 || !fallback) {
if (stops.length < 1 || !fallbackSymbolId) {
//occurs when no data
return null;
}
@ -85,14 +77,17 @@ export class DynamicIconProperty extends DynamicStyleProperty {
mbStops.push(`${stop}`);
mbStops.push(getMakiIconId(style, iconPixelSize));
});
mbStops.push(getMakiIconId(fallback, iconPixelSize)); //last item is fallback style for anything that does not match provided stops
if (fallbackSymbolId) {
mbStops.push(getMakiIconId(fallbackSymbolId, iconPixelSize)); //last item is fallback style for anything that does not match provided stops
}
return ['match', ['to-string', ['get', this._field.getName()]], ...mbStops];
}
_getMbIconAnchorExpression() {
const { stops, fallback } = this._getPaletteStops();
const { stops, fallbackSymbolId } = this._getPaletteStops();
if (stops.length < 1 || !fallback) {
if (stops.length < 1 || !fallbackSymbolId) {
//occurs when no data
return null;
}
@ -102,7 +97,10 @@ export class DynamicIconProperty extends DynamicStyleProperty {
mbStops.push(`${stop}`);
mbStops.push(getMakiSymbolAnchor(style));
});
mbStops.push(getMakiSymbolAnchor(fallback)); //last item is fallback style for anything that does not match provided stops
if (fallbackSymbolId) {
mbStops.push(getMakiSymbolAnchor(fallbackSymbolId)); //last item is fallback style for anything that does not match provided stops
}
return ['match', ['to-string', ['get', this._field.getName()]], ...mbStops];
}
@ -110,55 +108,34 @@ export class DynamicIconProperty extends DynamicStyleProperty {
return this._field && this._field.isValid();
}
renderBreakedLegend({ fieldLabel, isPointsOnly, isLinesOnly }) {
const categories = [];
const { stops, fallback } = this._getPaletteStops();
stops.map(({ stop, style }) => {
categories.push(
<Category
key={stop}
styleName={this.getStyleName()}
label={this.formatField(stop)}
color="grey"
isLinesOnly={isLinesOnly}
isPointsOnly={isPointsOnly}
symbolId={style}
/>
);
renderLegendDetailRow({ isPointsOnly, isLinesOnly }) {
const { stops, fallbackSymbolId } = this._getPaletteStops();
const breaks = [];
stops.forEach(({ stop, style }) => {
if (stop) {
breaks.push({
color: 'grey',
label: this.formatField(stop),
symbolId: style,
});
}
});
if (fallback) {
categories.push(
<Category
key="fallbackCategory"
styleName={this.getStyleName()}
label={<EuiTextColor color="secondary">{getOtherCategoryLabel()}</EuiTextColor>}
color="grey"
isLinesOnly={isLinesOnly}
isPointsOnly={isPointsOnly}
symbolId={fallback}
/>
);
if (fallbackSymbolId) {
breaks.push({
color: 'grey',
label: <EuiTextColor color="secondary">{getOtherCategoryLabel()}</EuiTextColor>,
symbolId: fallbackSymbolId,
});
}
return (
<div>
<EuiSpacer size="s" />
<EuiFlexGroup direction="column" gutterSize="none">
{categories}
</EuiFlexGroup>
<EuiFlexGroup gutterSize="xs" justifyContent="spaceAround">
<EuiFlexItem grow={false}>
<EuiToolTip position="top" title={this.getDisplayStyleName()} content={fieldLabel}>
<EuiText className="eui-textTruncate" size="xs" style={{ maxWidth: '180px' }}>
<small>
<strong>{fieldLabel}</strong>
</small>
</EuiText>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
</div>
<BreakedLegend
style={this}
breaks={breaks}
isPointsOnly={isPointsOnly}
isLinesOnly={isLinesOnly}
/>
);
}
}

View file

@ -5,6 +5,7 @@
*/
import { DynamicStyleProperty } from './dynamic_style_property';
import { OrdinalLegend } from '../components/legend/ordinal_legend';
import { makeMbClampedNumberExpression } from '../style_util';
import {
HALF_LARGE_MAKI_ICON_SIZE,
@ -13,34 +14,7 @@ import {
} from '../symbol_utils';
import { VECTOR_STYLES } from '../../../../../common/constants';
import _ from 'lodash';
import { CircleIcon } from '../components/legend/circle_icon';
import React, { Fragment } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui';
function getLineWidthIcons() {
const defaultStyle = {
stroke: 'grey',
fill: 'none',
width: '12px',
};
return [
<CircleIcon style={{ ...defaultStyle, strokeWidth: '1px' }} />,
<CircleIcon style={{ ...defaultStyle, strokeWidth: '2px' }} />,
<CircleIcon style={{ ...defaultStyle, strokeWidth: '3px' }} />,
];
}
function getSymbolSizeIcons() {
const defaultStyle = {
stroke: 'grey',
fill: 'grey',
};
return [
<CircleIcon style={{ ...defaultStyle, width: '4px' }} />,
<CircleIcon style={{ ...defaultStyle, width: '8px' }} />,
<CircleIcon style={{ ...defaultStyle, width: '12px' }} />,
];
}
import React from 'react';
export class DynamicSizeProperty extends DynamicStyleProperty {
constructor(options, styleName, field, vectorLayer, getFieldFormatter, isSymbolizedAsIcon) {
@ -99,13 +73,9 @@ export class DynamicSizeProperty extends DynamicStyleProperty {
}
}
syncCircleStrokeWidthWithMb(mbLayerId, mbMap, hasNoRadius) {
if (hasNoRadius) {
mbMap.setPaintProperty(mbLayerId, 'circle-stroke-width', 0);
} else {
const lineWidth = this.getMbSizeExpression();
mbMap.setPaintProperty(mbLayerId, 'circle-stroke-width', lineWidth);
}
syncCircleStrokeWidthWithMb(mbLayerId, mbMap) {
const lineWidth = this.getMbSizeExpression();
mbMap.setPaintProperty(mbLayerId, 'circle-stroke-width', lineWidth);
}
syncCircleRadiusWithMb(mbLayerId, mbMap) {
@ -166,36 +136,7 @@ export class DynamicSizeProperty extends DynamicStyleProperty {
);
}
renderRangeLegendHeader() {
let icons;
if (this.getStyleName() === VECTOR_STYLES.LINE_WIDTH) {
icons = getLineWidthIcons();
} else if (this.getStyleName() === VECTOR_STYLES.ICON_SIZE) {
icons = getSymbolSizeIcons();
} else {
return null;
}
return (
<EuiFlexGroup gutterSize="xs" justifyContent="spaceBetween" alignItems="center">
{icons.map((icon, index) => {
const isLast = index === icons.length - 1;
let spacer;
if (!isLast) {
spacer = (
<EuiFlexItem>
<EuiHorizontalRule margin="xs" />
</EuiFlexItem>
);
}
return (
<Fragment key={index}>
<EuiFlexItem grow={false}>{icon}</EuiFlexItem>
{spacer}
</Fragment>
);
})}
</EuiFlexGroup>
);
renderLegendDetailRow() {
return <OrdinalLegend style={this} />;
}
}

View file

@ -0,0 +1,102 @@
/*
* 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 { IVectorStyle } from '../vector_style';
jest.mock('ui/new_platform');
jest.mock('../components/vector_style_editor', () => ({
VectorStyleEditor: () => {
return <div>mockVectorStyleEditor</div>;
},
}));
import React from 'react';
import { shallow } from 'enzyme';
// @ts-ignore
import { DynamicSizeProperty } from './dynamic_size_property';
import { StyleMeta } from '../style_meta';
import { FIELD_ORIGIN, VECTOR_STYLES } from '../../../../../common/constants';
import { DataRequest } from '../../../util/data_request';
import { IVectorLayer } from '../../../layers/vector_layer/vector_layer';
import { IField } from '../../../fields/field';
// @ts-ignore
const mockField: IField = {
async getLabel() {
return 'foobar_label';
},
getName() {
return 'foobar';
},
getRootName() {
return 'foobar';
},
getOrigin() {
return FIELD_ORIGIN.SOURCE;
},
supportsFieldMeta() {
return true;
},
canValueBeFormatted() {
return true;
},
async getDataType() {
return 'number';
},
};
// @ts-ignore
const mockLayer: IVectorLayer = {
getDataRequest(): DataRequest | undefined {
return undefined;
},
getStyle(): IVectorStyle {
// @ts-ignore
return {
getStyleMeta(): StyleMeta {
return new StyleMeta({
geometryTypes: {
isPointsOnly: true,
isLinesOnly: false,
isPolygonsOnly: false,
},
fieldMeta: {
foobar: {
range: { min: 0, max: 100, delta: 100 },
categories: { categories: [] },
},
},
});
},
};
},
};
const makeProperty: DynamicSizeProperty = (options: object) => {
return new DynamicSizeProperty(options, VECTOR_STYLES.ICON_SIZE, mockField, mockLayer, () => {
return (x: string) => x + '_format';
});
};
const defaultLegendParams = {
isPointsOnly: true,
isLinesOnly: false,
};
describe('renderLegendDetailRow', () => {
test('Should render as range', async () => {
const sizeProp = makeProperty();
const legendRow = sizeProp.renderLegendDetailRow(defaultLegendParams);
const component = shallow(legendRow);
// Ensure all promises resolve
await new Promise((resolve) => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
expect(component).toMatchSnapshot();
});
});

View file

@ -9,8 +9,6 @@ import { AbstractStyleProperty } from './style_property';
import { DEFAULT_SIGMA } from '../vector_style_defaults';
import { STYLE_TYPE, SOURCE_META_ID_ORIGIN, FIELD_ORIGIN } from '../../../../../common/constants';
import React from 'react';
import { OrdinalLegend } from './components/ordinal_legend';
import { CategoricalLegend } from './components/categorical_legend';
import { OrdinalFieldMetaPopover } from '../components/field_meta/ordinal_field_meta_popover';
import { CategoricalFieldMetaPopover } from '../components/field_meta/categorical_field_meta_popover';
@ -119,14 +117,6 @@ export class DynamicStyleProperty extends AbstractStyleProperty {
return 0;
}
hasOrdinalBreaks() {
return false;
}
isOrdinalRanged() {
return true;
}
isComplete() {
return !!this._field;
}
@ -280,49 +270,14 @@ export class DynamicStyleProperty extends AbstractStyleProperty {
}
getNumericalMbFeatureStateValue(value) {
if (typeof value === 'number') {
return value;
}
const valueAsFloat = parseFloat(value);
return isNaN(valueAsFloat) ? null : valueAsFloat;
}
renderBreakedLegend() {
renderLegendDetailRow() {
return null;
}
_renderCategoricalLegend({ isPointsOnly, isLinesOnly, symbolId }) {
return (
<CategoricalLegend
style={this}
isPointsOnly={isPointsOnly}
isLinesOnly={isLinesOnly}
symbolId={symbolId}
/>
);
}
_renderRangeLegend() {
return <OrdinalLegend style={this} />;
}
renderLegendDetailRow({ isPointsOnly, isLinesOnly, symbolId }) {
if (this.isOrdinal()) {
if (this.isOrdinalRanged()) {
return this._renderRangeLegend();
} else if (this.hasOrdinalBreaks()) {
return this._renderCategoricalLegend({ isPointsOnly, isLinesOnly, symbolId });
} else {
return null;
}
} else if (this.isCategorical()) {
return this._renderCategoricalLegend({ isPointsOnly, isLinesOnly, symbolId });
} else {
return null;
}
}
renderFieldMetaPopover(onFieldMetaOptionsChange) {
if (!this.supportsFieldMeta()) {
return null;

View file

@ -23,7 +23,6 @@ export interface IStyleProperty {
formatField(value: string | undefined): string;
getStyleName(): VECTOR_STYLES;
getOptions(): StylePropertyOptions;
renderRangeLegendHeader(): ReactElement<any> | null;
renderLegendDetailRow(legendProps: LegendProps): ReactElement<any> | null;
renderFieldMetaPopover(
onFieldMetaOptionsChange: (fieldMetaOptions: FieldMetaOptions) => void
@ -67,10 +66,6 @@ export class AbstractStyleProperty implements IStyleProperty {
return this._options || {};
}
renderRangeLegendHeader() {
return null;
}
renderLegendDetailRow() {
return null;
}

View file

@ -34,6 +34,21 @@ export function isOnlySingleFeatureType(featureType, supportedFeatures, hasFeatu
}, true);
}
export function dynamicRound(value) {
if (typeof value !== 'number') {
return value;
}
let precision = 0;
let threshold = 10;
while (value < threshold && precision < 8) {
precision++;
threshold = threshold / 10;
}
return precision === 0 ? Math.round(value) : parseFloat(value.toFixed(precision + 1));
}
export function assignCategoriesToPalette({ categories, paletteValues }) {
const stops = [];
let fallback = null;

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { isOnlySingleFeatureType, assignCategoriesToPalette } from './style_util';
import { isOnlySingleFeatureType, assignCategoriesToPalette, dynamicRound } from './style_util';
import { VECTOR_SHAPE_TYPES } from '../../sources/vector_feature_types';
describe('isOnlySingleFeatureType', () => {
@ -100,3 +100,15 @@ describe('assignCategoriesToPalette', () => {
});
});
});
describe('dynamicRound', () => {
test('Should truncate based on magnitude of number', () => {
expect(dynamicRound(1000.1234)).toBe(1000);
expect(dynamicRound(1.1234)).toBe(1.12);
expect(dynamicRound(0.0012345678)).toBe(0.00123);
});
test('Should return argument when not a number', () => {
expect(dynamicRound('foobar')).toBe('foobar');
});
});

View file

@ -12,11 +12,13 @@ import {
VectorStyleDescriptor,
VectorStylePropertiesDescriptor,
} from '../../../../common/descriptor_types';
import { StyleMeta } from './style_meta';
export interface IVectorStyle extends IStyle {
getAllStyleProperties(): IStyleProperty[];
getDynamicPropertiesArray(): IDynamicStyleProperty[];
getSourceFieldNames(): string[];
getStyleMeta(): StyleMeta;
}
export class VectorStyle extends AbstractStyle implements IVectorStyle {
@ -26,4 +28,5 @@ export class VectorStyle extends AbstractStyle implements IVectorStyle {
getSourceFieldNames(): string[];
getAllStyleProperties(): IStyleProperty[];
getDynamicPropertiesArray(): IDynamicStyleProperty[];
getStyleMeta(): StyleMeta;
}

View file

@ -58,11 +58,18 @@ export default function ({ getPageObjects, getService }) {
const layerTOCDetails = await PageObjects.maps.getLayerTOCDetails('geo_shapes*');
const split = layerTOCDetails.trim().split('\n');
const min = split[0];
expect(min).to.equal('3');
//field display name
expect(split[0]).to.equal('max prop1');
const max = split[2];
expect(max).to.equal('12');
//bands 1-8
expect(split[1]).to.equal('3');
expect(split[2]).to.equal('4.13');
expect(split[3]).to.equal('5.25');
expect(split[4]).to.equal('6.38');
expect(split[5]).to.equal('7.5');
expect(split[6]).to.equal('8.63');
expect(split[7]).to.equal('9.75');
expect(split[8]).to.equal('11');
});
it('should decorate feature properties with join property', async () => {
@ -164,10 +171,10 @@ export default function ({ getPageObjects, getService }) {
const split = layerTOCDetails.trim().split('\n');
const min = split[0];
expect(min).to.equal('12');
expect(min).to.equal('max prop1');
const max = split[2];
expect(max).to.equal('12');
const max = split[1];
expect(max).to.equal('12'); // just single band because single value
});
it('should flag only the joined features as visible', async () => {