[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[]; getJoins(): IJoin[];
getValidJoins(): IJoin[]; getValidJoins(): IJoin[];
getSource(): IVectorSource; getSource(): IVectorSource;
getStyle(): IVectorStyle;
} }
export class VectorLayer extends AbstractLayer implements IVectorLayer { export class VectorLayer extends AbstractLayer implements IVectorLayer {
@ -73,4 +74,5 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
_setMbPointsProperties(mbMap: unknown, mvtSourceLayer?: string): void; _setMbPointsProperties(mbMap: unknown, mvtSourceLayer?: string): void;
_setMbLinePolygonProperties(mbMap: unknown, mvtSourceLayer?: string): void; _setMbLinePolygonProperties(mbMap: unknown, mvtSourceLayer?: string): void;
getSource(): IVectorSource; getSource(): IVectorSource;
getStyle(): IVectorStyle;
} }

View file

@ -2,3 +2,5 @@
@import 'vector/components/style_prop_editor'; @import 'vector/components/style_prop_editor';
@import 'vector/components/color/color_stops'; @import 'vector/components/color/color_stops';
@import 'vector/components/symbol/icon_select'; @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 { ColorGradient } from './components/color_gradient';
import { vislibColorMaps } from '../../../../../../src/plugins/charts/public'; 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_FILL_COLORS = euiPaletteColorBlind();
export const DEFAULT_LINE_COLORS = [ export const DEFAULT_LINE_COLORS = [
@ -73,7 +73,7 @@ export function getColorRampCenterColor(colorRampName) {
// Returns an array of color stops // Returns an array of color stops
// [ stop_input_1: number, stop_output_1: color, stop_input_n: number, stop_output_n: color ] // [ 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) { if (!colorRampName) {
return null; return null;
} }
@ -82,7 +82,7 @@ export function getOrdinalColorRampStops(colorRampName, min, max) {
return null; return null;
} }
const hexColors = getHexColorRangeStrings(colorRampName, GRADIENT_INTERVALS); const hexColors = getHexColorRangeStrings(colorRampName, numberColors);
if (max === min) { if (max === min) {
//just return single stop value //just return single stop value
return [max, hexColors[hexColors.length - 1]]; return [max, hexColors[hexColors.length - 1]];

View file

@ -3,11 +3,10 @@
* or more contributor license agreements. Licensed under the Elastic License; * or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
import { import {
COLOR_GRADIENTS, COLOR_GRADIENTS,
getColorRampCenterColor, getColorRampCenterColor,
getOrdinalColorRampStops, getOrdinalMbColorRampStops,
getHexColorRangeStrings, getHexColorRangeStrings,
getLinearGradient, getLinearGradient,
getRGBColorRangeStrings, getRGBColorRangeStrings,
@ -25,7 +24,7 @@ describe('COLOR_GRADIENTS', () => {
describe('getRGBColorRangeStrings', () => { describe('getRGBColorRangeStrings', () => {
it('Should create RGB color ramp', () => { it('Should create RGB color ramp', () => {
expect(getRGBColorRangeStrings('Blues')).toEqual([ expect(getRGBColorRangeStrings('Blues', 8)).toEqual([
'rgb(247,250,255)', 'rgb(247,250,255)',
'rgb(221,234,247)', 'rgb(221,234,247)',
'rgb(197,218,238)', 'rgb(197,218,238)',
@ -61,7 +60,7 @@ describe('getColorRampCenterColor', () => {
describe('getColorRampStops', () => { describe('getColorRampStops', () => {
it('Should create color stops for custom range', () => { it('Should create color stops for custom range', () => {
expect(getOrdinalColorRampStops('Blues', 0, 1000)).toEqual([ expect(getOrdinalMbColorRampStops('Blues', 0, 1000, 8)).toEqual([
0, 0,
'#f7faff', '#f7faff',
125, 125,
@ -82,7 +81,7 @@ describe('getColorRampStops', () => {
}); });
it('Should snap to end of color stops for identical range', () => { 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 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'; import classNames from 'classnames';
export const ColorGradient = ({ colorRamp, colorRampName, className }) => { export const ColorGradient = ({ colorRamp, colorRampName, className }) => {
@ -14,7 +19,9 @@ export const ColorGradient = ({ colorRamp, colorRampName, className }) => {
} }
const classes = classNames('mapColorGradient', className); const classes = classNames('mapColorGradient', className);
const rgbColorStrings = colorRampName ? getRGBColorRangeStrings(colorRampName) : colorRamp; const rgbColorStrings = colorRampName
? getRGBColorRangeStrings(colorRampName, GRADIENT_INTERVALS)
: colorRamp;
const background = getLinearGradient(rgbColorStrings); const background = getLinearGradient(rgbColorStrings);
return <div className={classes} style={{ background }} />; return <div className={classes} style={{ background }} />;
}; };

View file

@ -7,19 +7,12 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; 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 }) { export function RangedStyleLegendRow({ header, minLabel, maxLabel, propertyLabel, fieldLabel }) {
return ( return (
<div> <div>
<EuiSpacer size="xs" />
{header}
<EuiFlexGroup gutterSize="xs" justifyContent="spaceBetween"> <EuiFlexGroup gutterSize="xs" justifyContent="spaceBetween">
<EuiFlexItem grow={true}>
<EuiText size="xs">
<small>{minLabel}</small>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}> <EuiFlexItem grow={false}>
<EuiToolTip position="top" title={propertyLabel} content={fieldLabel}> <EuiToolTip position="top" title={propertyLabel} content={fieldLabel}>
<EuiText className="eui-textTruncate" size="xs" style={{ maxWidth: '180px' }}> <EuiText className="eui-textTruncate" size="xs" style={{ maxWidth: '180px' }}>
@ -29,6 +22,14 @@ export function RangedStyleLegendRow({ header, minLabel, maxLabel, propertyLabel
</EuiText> </EuiText>
</EuiToolTip> </EuiToolTip>
</EuiFlexItem> </EuiFlexItem>
</EuiFlexGroup>
{header}
<EuiFlexGroup gutterSize="xs" justifyContent="spaceBetween">
<EuiFlexItem grow={true}>
<EuiText size="xs">
<small>{minLabel}</small>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={true}> <EuiFlexItem grow={true}>
<EuiText textAlign="right" size="xs"> <EuiText textAlign="right" size="xs">
<small>{maxLabel}</small> <small>{maxLabel}</small>

View file

@ -10,7 +10,7 @@ import { HeatmapStyleEditor } from './components/heatmap_style_editor';
import { HeatmapLegend } from './components/legend/heatmap_legend'; import { HeatmapLegend } from './components/legend/heatmap_legend';
import { DEFAULT_HEATMAP_COLOR_RAMP_NAME } from './components/heatmap_constants'; import { DEFAULT_HEATMAP_COLOR_RAMP_NAME } from './components/heatmap_constants';
import { LAYER_STYLE_TYPE, GRID_RESOLUTION } from '../../../../common/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 { i18n } from '@kbn/i18n';
import { EuiIcon } from '@elastic/eui'; import { EuiIcon } from '@elastic/eui';
@ -85,7 +85,12 @@ export class HeatmapStyle extends AbstractStyle {
const { colorRampName } = this._descriptor; const { colorRampName } = this._descriptor;
if (colorRampName && colorRampName !== DEFAULT_HEATMAP_COLOR_RAMP_NAME) { 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', [ mbMap.setPaintProperty(layerId, 'heatmap-color', [
'interpolate', 'interpolate',
['linear'], ['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 ( return (
<EuiFlexItem> <EuiFlexGroup direction="row" gutterSize="none">
<EuiFlexGroup direction="row" gutterSize="none"> <EuiFlexItem className="mapLegendIconPreview" grow={false}>
<EuiFlexItem> {renderIcon()}
<EuiText size="xs">{label}</EuiText> </EuiFlexItem>
</EuiFlexItem> <EuiFlexItem>
<EuiFlexItem>{renderIcon()}</EuiFlexItem> <EuiText size="xs">{label}</EuiText>
</EuiFlexGroup> </EuiFlexItem>
</EuiFlexItem> </EuiFlexGroup>
); );
} }

View file

@ -4,9 +4,37 @@
* you may not use this file except in compliance with the Elastic License. * 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 _ from 'lodash';
import { RangedStyleLegendRow } from '../../../components/ranged_style_legend_row'; 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 = ''; const EMPTY_VALUE = '';
export class OrdinalLegend extends React.Component { export class OrdinalLegend extends React.Component {
@ -45,7 +73,46 @@ export class OrdinalLegend extends React.Component {
this._isMounted = true; this._isMounted = true;
this._loadParams(); 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() { render() {
const header = this._renderRangeLegendHeader();
if (!header) {
return null;
}
const fieldMeta = this.props.style.getRangeFieldMeta(); const fieldMeta = this.props.style.getRangeFieldMeta();
let minLabel = EMPTY_VALUE; let minLabel = EMPTY_VALUE;
@ -67,7 +134,7 @@ export class OrdinalLegend extends React.Component {
return ( return (
<RangedStyleLegendRow <RangedStyleLegendRow
header={this.props.style.renderRangeLegendHeader()} header={header}
minLabel={minLabel} minLabel={minLabel}
maxLabel={maxLabel} maxLabel={maxLabel}
propertyLabel={this.props.style.getDisplayStyleName()} propertyLabel={this.props.style.getDisplayStyleName()}

View file

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

View file

@ -1,50 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // 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> <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 <EuiFlexGroup
gutterSize="xs" gutterSize="xs"
justifyContent="spaceAround" justifyContent="spaceBetween"
> >
<EuiFlexItem <EuiFlexItem
grow={false} grow={false}
@ -73,52 +35,58 @@ exports[`Should render categorical legend with breaks from default 1`] = `
</EuiToolTip> </EuiToolTip>
</EuiFlexItem> </EuiFlexItem>
</EuiFlexGroup> </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 <EuiFlexGroup
direction="column" direction="column"
gutterSize="none" gutterSize="none"
> >
<Category <EuiFlexItem
color="#FF0000"
isLinesOnly={false}
isPointsOnly={true}
key="0" key="0"
label="0_format" >
styleName="lineColor" <Category
/> color="#54B399"
<Category isLinesOnly={false}
color="#00FF00" isPointsOnly={true}
isLinesOnly={false} label="US_format"
isPointsOnly={true} styleName="lineColor"
key="10" />
label="10_format" </EuiFlexItem>
styleName="lineColor" <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> </EuiFlexGroup>
</div>
`;
exports[`ordinal Should render custom ordinal legend with breaks 1`] = `
<div>
<EuiFlexGroup <EuiFlexGroup
gutterSize="xs" gutterSize="xs"
justifyContent="spaceAround" justifyContent="spaceBetween"
> >
<EuiFlexItem <EuiFlexItem
grow={false} grow={false}
@ -147,5 +115,191 @@ exports[`Should render ordinal legend with breaks 1`] = `
</EuiToolTip> </EuiToolTip>
</EuiFlexItem> </EuiFlexItem>
</EuiFlexGroup> </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> </div>
`; `;

View file

@ -2,50 +2,9 @@
exports[`Should render categorical legend with breaks 1`] = ` exports[`Should render categorical legend with breaks 1`] = `
<div> <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 <EuiFlexGroup
gutterSize="xs" gutterSize="xs"
justifyContent="spaceAround" justifyContent="spaceBetween"
> >
<EuiFlexItem <EuiFlexItem
grow={false} grow={false}
@ -74,5 +33,34 @@ exports[`Should render categorical legend with breaks 1`] = `
</EuiToolTip> </EuiToolTip>
</EuiFlexItem> </EuiFlexItem>
</EuiFlexGroup> </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> </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, StyleMetaDescriptor,
} from '../../../../../../common/descriptor_types'; } from '../../../../../../common/descriptor_types';
import { AbstractField, IField } from '../../../../fields/field'; import { AbstractField, IField } from '../../../../fields/field';
import { IStyle, AbstractStyle } from '../../../style';
class MockField extends AbstractField { class MockField extends AbstractField {
async getLabel(): Promise<string> { async getLabel(): Promise<string> {
@ -29,14 +30,27 @@ export const mockField: IField = new MockField({
origin: FIELD_ORIGIN.SOURCE, 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 { getStyleMeta(): StyleMeta {
const geomTypes: GeometryTypes = { const geomTypes: GeometryTypes = {
isPointsOnly: false, isPointsOnly: false,
isLinesOnly: false, isLinesOnly: false,
isPolygonsOnly: 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 = { const catFieldMeta: CategoryFieldMeta = {
categories: [ categories: [
{ {
@ -65,8 +79,12 @@ class MockStyle {
} }
export class MockLayer { export class MockLayer {
private readonly _style: IStyle;
constructor(style = new MockStyle()) {
this._style = style;
}
getStyle() { getStyle() {
return new MockStyle(); return this._style;
} }
getDataRequest() { 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 { DynamicStyleProperty } from './dynamic_style_property';
import { getOtherCategoryLabel, makeMbClampedNumberExpression } from '../style_util'; import { makeMbClampedNumberExpression, dynamicRound } from '../style_util';
import { getOrdinalColorRampStops, getColorPalette } from '../../color_utils';
import { ColorGradient } from '../../components/color_gradient';
import React from 'react';
import { import {
EuiFlexGroup, getOrdinalMbColorRampStops,
EuiFlexItem, getColorPalette,
EuiSpacer, getHexColorRangeStrings,
EuiText, GRADIENT_INTERVALS,
EuiToolTip, } from '../../color_utils';
EuiTextColor, import React from 'react';
} from '@elastic/eui'; import { COLOR_MAP_TYPE } from '../../../../../common/constants';
import { Category } from '../components/legend/category'; import {
import { COLOR_MAP_TYPE, RGBA_0000 } from '../../../../../common/constants'; isCategoricalStopsInvalid,
import { isCategoricalStopsInvalid } from '../components/color/color_stops_utils'; 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 EMPTY_STOPS = { stops: [], defaultColor: null };
const RGBA_0000 = 'rgba(0,0,0,0)';
export class DynamicColorProperty extends DynamicStyleProperty { export class DynamicColorProperty extends DynamicStyleProperty {
syncCircleColorWithMb(mbLayerId, mbMap, alpha) { syncCircleColorWithMb(mbLayerId, mbMap, alpha) {
@ -99,14 +100,6 @@ export class DynamicColorProperty extends DynamicStyleProperty {
return true; return true;
} }
isOrdinalRanged() {
return this.isOrdinal() && !this._options.useCustomColorRamp;
}
hasOrdinalBreaks() {
return (this.isOrdinal() && this._options.useCustomColorRamp) || this.isCategorical();
}
_getMbColor() { _getMbColor() {
if (!this._field || !this._field.getName()) { if (!this._field || !this._field.getName()) {
return null; return null;
@ -142,10 +135,11 @@ export class DynamicColorProperty extends DynamicStyleProperty {
return null; return null;
} }
const colorStops = getOrdinalColorRampStops( const colorStops = getOrdinalMbColorRampStops(
this._options.color, this._options.color,
rangeFieldMeta.min, rangeFieldMeta.min,
rangeFieldMeta.max rangeFieldMeta.max,
GRADIENT_INTERVALS
); );
if (!colorStops) { if (!colorStops) {
return null; return null;
@ -237,28 +231,47 @@ export class DynamicColorProperty extends DynamicStyleProperty {
for (let i = 0; i < stops.length; i++) { for (let i = 0; i < stops.length; i++) {
const stop = stops[i]; const stop = stops[i];
const branch = `${stop.stop}`; const branch = `${stop.stop}`;
if (typeof branch === 'string') { mbStops.push(branch);
mbStops.push(branch); mbStops.push(stop.color);
mbStops.push(stop.color);
}
} }
mbStops.push(defaultColor); //last color is default color mbStops.push(defaultColor); //last color is default color
return ['match', ['to-string', ['get', this._field.getName()]], ...mbStops]; return ['match', ['to-string', ['get', this._field.getName()]], ...mbStops];
} }
renderRangeLegendHeader() {
if (this._options.color) {
return <ColorGradient colorRampName={this._options.color} />;
} else {
return null;
}
}
_getColorRampStops() { _getColorRampStops() {
return this._options.useCustomColorRamp && this._options.customColorRamp if (this._options.useCustomColorRamp && this._options.customColorRamp) {
? 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() { _getColorStops() {
@ -274,55 +287,33 @@ export class DynamicColorProperty extends DynamicStyleProperty {
} }
} }
renderBreakedLegend({ fieldLabel, isPointsOnly, isLinesOnly, symbolId }) { renderLegendDetailRow({ isPointsOnly, isLinesOnly, symbolId }) {
const categories = [];
const { stops, defaultColor } = this._getColorStops(); const { stops, defaultColor } = this._getColorStops();
stops.map(({ stop, color }) => { const breaks = [];
categories.push( stops.forEach(({ stop, color }) => {
<Category if (stop) {
key={stop} breaks.push({
styleName={this.getStyleName()} color,
label={this.formatField(stop)} symbolId,
color={color} label: this.formatField(stop),
isLinesOnly={isLinesOnly} });
isPointsOnly={isPointsOnly} }
symbolId={symbolId}
/>
);
}); });
if (defaultColor) { if (defaultColor) {
categories.push( breaks.push({
<Category color: defaultColor,
key="fallbackCategory" label: <EuiTextColor color="secondary">{getOtherCategoryLabel()}</EuiTextColor>,
styleName={this.getStyleName()} symbolId,
label={<EuiTextColor color="secondary">{getOtherCategoryLabel()}</EuiTextColor>} });
color={defaultColor}
isLinesOnly={isLinesOnly}
isPointsOnly={isPointsOnly}
symbolId={symbolId}
/>
);
} }
return ( return (
<div> <BreakedLegend
<EuiSpacer size="s" /> style={this}
<EuiFlexGroup direction="column" gutterSize="none"> breaks={breaks}
{categories} isPointsOnly={isPointsOnly}
</EuiFlexGroup> isLinesOnly={isLinesOnly}
<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>
); );
} }
} }

View file

@ -16,12 +16,18 @@ import { shallow } from 'enzyme';
import { DynamicColorProperty } from './dynamic_color_property'; import { DynamicColorProperty } from './dynamic_color_property';
import { COLOR_MAP_TYPE, VECTOR_STYLES } from '../../../../../common/constants'; 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) => { const makeProperty = (options, mockStyle, field = mockField) => {
return new DynamicColorProperty(options, VECTOR_STYLES.LINE_COLOR, field, new MockLayer(), () => { return new DynamicColorProperty(
return (x) => x + '_format'; options,
}); VECTOR_STYLES.LINE_COLOR,
field,
new MockLayer(mockStyle),
() => {
return (x) => x + '_format';
}
);
}; };
const defaultLegendParams = { const defaultLegendParams = {
@ -29,91 +35,121 @@ const defaultLegendParams = {
isLinesOnly: false, isLinesOnly: false,
}; };
test('Should render ordinal legend', async () => { describe('ordinal', () => {
const colorStyle = makeProperty({ test('Should render ordinal legend as bands', async () => {
color: 'Blues', const colorStyle = makeProperty({
type: undefined, 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 () => { describe('categorical', () => {
const colorStyle = makeProperty({ test('Should render categorical legend with breaks from default', async () => {
type: COLOR_MAP_TYPE.ORDINAL, const colorStyle = makeProperty({
useCustomColorRamp: true, type: COLOR_MAP_TYPE.CATEGORICAL,
customColorRamp: [ useCustomColorPalette: false,
{ colorCategory: 'palette_0',
stop: 0, });
color: '#FF0000',
}, const legendRow = colorStyle.renderLegendDetailRow(defaultLegendParams);
{
stop: 10, const component = shallow(legendRow);
color: '#00FF00',
}, // 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 const component = shallow(legendRow);
await new Promise((resolve) => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
expect(component).toMatchSnapshot(); 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',
}); });
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) { function makeFeatures(foobarPropValues) {
@ -201,7 +237,7 @@ describe('supportsFieldMeta', () => {
const dynamicStyleOptions = { const dynamicStyleOptions = {
type: COLOR_MAP_TYPE.ORDINAL, type: COLOR_MAP_TYPE.ORDINAL,
}; };
const styleProp = makeProperty(dynamicStyleOptions, field); const styleProp = makeProperty(dynamicStyleOptions, undefined, field);
expect(styleProp.supportsFieldMeta()).toEqual(false); expect(styleProp.supportsFieldMeta()).toEqual(false);
}); });
@ -210,7 +246,7 @@ describe('supportsFieldMeta', () => {
const dynamicStyleOptions = { const dynamicStyleOptions = {
type: COLOR_MAP_TYPE.ORDINAL, type: COLOR_MAP_TYPE.ORDINAL,
}; };
const styleProp = makeProperty(dynamicStyleOptions, null); const styleProp = makeProperty(dynamicStyleOptions, undefined, null);
expect(styleProp.supportsFieldMeta()).toEqual(false); expect(styleProp.supportsFieldMeta()).toEqual(false);
}); });

View file

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

View file

@ -5,6 +5,7 @@
*/ */
import { DynamicStyleProperty } from './dynamic_style_property'; import { DynamicStyleProperty } from './dynamic_style_property';
import { OrdinalLegend } from '../components/legend/ordinal_legend';
import { makeMbClampedNumberExpression } from '../style_util'; import { makeMbClampedNumberExpression } from '../style_util';
import { import {
HALF_LARGE_MAKI_ICON_SIZE, HALF_LARGE_MAKI_ICON_SIZE,
@ -13,34 +14,7 @@ import {
} from '../symbol_utils'; } from '../symbol_utils';
import { VECTOR_STYLES } from '../../../../../common/constants'; import { VECTOR_STYLES } from '../../../../../common/constants';
import _ from 'lodash'; import _ from 'lodash';
import { CircleIcon } from '../components/legend/circle_icon'; import React from 'react';
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' }} />,
];
}
export class DynamicSizeProperty extends DynamicStyleProperty { export class DynamicSizeProperty extends DynamicStyleProperty {
constructor(options, styleName, field, vectorLayer, getFieldFormatter, isSymbolizedAsIcon) { constructor(options, styleName, field, vectorLayer, getFieldFormatter, isSymbolizedAsIcon) {
@ -99,13 +73,9 @@ export class DynamicSizeProperty extends DynamicStyleProperty {
} }
} }
syncCircleStrokeWidthWithMb(mbLayerId, mbMap, hasNoRadius) { syncCircleStrokeWidthWithMb(mbLayerId, mbMap) {
if (hasNoRadius) { const lineWidth = this.getMbSizeExpression();
mbMap.setPaintProperty(mbLayerId, 'circle-stroke-width', 0); mbMap.setPaintProperty(mbLayerId, 'circle-stroke-width', lineWidth);
} else {
const lineWidth = this.getMbSizeExpression();
mbMap.setPaintProperty(mbLayerId, 'circle-stroke-width', lineWidth);
}
} }
syncCircleRadiusWithMb(mbLayerId, mbMap) { syncCircleRadiusWithMb(mbLayerId, mbMap) {
@ -166,36 +136,7 @@ export class DynamicSizeProperty extends DynamicStyleProperty {
); );
} }
renderRangeLegendHeader() { renderLegendDetailRow() {
let icons; return <OrdinalLegend style={this} />;
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>
);
} }
} }

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 { DEFAULT_SIGMA } from '../vector_style_defaults';
import { STYLE_TYPE, SOURCE_META_ID_ORIGIN, FIELD_ORIGIN } from '../../../../../common/constants'; import { STYLE_TYPE, SOURCE_META_ID_ORIGIN, FIELD_ORIGIN } from '../../../../../common/constants';
import React from 'react'; 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 { OrdinalFieldMetaPopover } from '../components/field_meta/ordinal_field_meta_popover';
import { CategoricalFieldMetaPopover } from '../components/field_meta/categorical_field_meta_popover'; import { CategoricalFieldMetaPopover } from '../components/field_meta/categorical_field_meta_popover';
@ -119,14 +117,6 @@ export class DynamicStyleProperty extends AbstractStyleProperty {
return 0; return 0;
} }
hasOrdinalBreaks() {
return false;
}
isOrdinalRanged() {
return true;
}
isComplete() { isComplete() {
return !!this._field; return !!this._field;
} }
@ -280,49 +270,14 @@ export class DynamicStyleProperty extends AbstractStyleProperty {
} }
getNumericalMbFeatureStateValue(value) { getNumericalMbFeatureStateValue(value) {
if (typeof value === 'number') {
return value;
}
const valueAsFloat = parseFloat(value); const valueAsFloat = parseFloat(value);
return isNaN(valueAsFloat) ? null : valueAsFloat; return isNaN(valueAsFloat) ? null : valueAsFloat;
} }
renderBreakedLegend() { renderLegendDetailRow() {
return null; 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) { renderFieldMetaPopover(onFieldMetaOptionsChange) {
if (!this.supportsFieldMeta()) { if (!this.supportsFieldMeta()) {
return null; return null;

View file

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

View file

@ -34,6 +34,21 @@ export function isOnlySingleFeatureType(featureType, supportedFeatures, hasFeatu
}, true); }, 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 }) { export function assignCategoriesToPalette({ categories, paletteValues }) {
const stops = []; const stops = [];
let fallback = null; let fallback = null;

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License. * 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'; import { VECTOR_SHAPE_TYPES } from '../../sources/vector_feature_types';
describe('isOnlySingleFeatureType', () => { 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, VectorStyleDescriptor,
VectorStylePropertiesDescriptor, VectorStylePropertiesDescriptor,
} from '../../../../common/descriptor_types'; } from '../../../../common/descriptor_types';
import { StyleMeta } from './style_meta';
export interface IVectorStyle extends IStyle { export interface IVectorStyle extends IStyle {
getAllStyleProperties(): IStyleProperty[]; getAllStyleProperties(): IStyleProperty[];
getDynamicPropertiesArray(): IDynamicStyleProperty[]; getDynamicPropertiesArray(): IDynamicStyleProperty[];
getSourceFieldNames(): string[]; getSourceFieldNames(): string[];
getStyleMeta(): StyleMeta;
} }
export class VectorStyle extends AbstractStyle implements IVectorStyle { export class VectorStyle extends AbstractStyle implements IVectorStyle {
@ -26,4 +28,5 @@ export class VectorStyle extends AbstractStyle implements IVectorStyle {
getSourceFieldNames(): string[]; getSourceFieldNames(): string[];
getAllStyleProperties(): IStyleProperty[]; getAllStyleProperties(): IStyleProperty[];
getDynamicPropertiesArray(): IDynamicStyleProperty[]; getDynamicPropertiesArray(): IDynamicStyleProperty[];
getStyleMeta(): StyleMeta;
} }

View file

@ -58,11 +58,18 @@ export default function ({ getPageObjects, getService }) {
const layerTOCDetails = await PageObjects.maps.getLayerTOCDetails('geo_shapes*'); const layerTOCDetails = await PageObjects.maps.getLayerTOCDetails('geo_shapes*');
const split = layerTOCDetails.trim().split('\n'); const split = layerTOCDetails.trim().split('\n');
const min = split[0]; //field display name
expect(min).to.equal('3'); expect(split[0]).to.equal('max prop1');
const max = split[2]; //bands 1-8
expect(max).to.equal('12'); 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 () => { 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 split = layerTOCDetails.trim().split('\n');
const min = split[0]; const min = split[0];
expect(min).to.equal('12'); expect(min).to.equal('max prop1');
const max = split[2]; const max = split[1];
expect(max).to.equal('12'); expect(max).to.equal('12'); // just single band because single value
}); });
it('should flag only the joined features as visible', async () => { it('should flag only the joined features as visible', async () => {