mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Maps] display ranged-data with bands (#60570)
This commit is contained in:
parent
b2019c1924
commit
4bf3955c70
29 changed files with 960 additions and 562 deletions
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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]];
|
||||
|
|
|
@ -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']);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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 }} />;
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
.mapLegendIconPreview {
|
||||
width: $euiSizeL;
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
.vectorStyleLegendSpacer {
|
||||
&:not(:last-child) {
|
||||
margin-bottom: $euiSizeS;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -31,13 +31,13 @@ export function Category({ styleName, label, color, isLinesOnly, isPointsOnly, s
|
|||
}
|
||||
|
||||
return (
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup direction="row" gutterSize="none">
|
||||
<EuiFlexItem className="mapLegendIconPreview" grow={false}>
|
||||
{renderIcon()}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="xs">{label}</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>{renderIcon()}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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()}
|
|
@ -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({
|
||||
const legendRows = [];
|
||||
|
||||
for (let i = 0; i < styles.length; i++) {
|
||||
const row = styles[i].renderLegendDetailRow({
|
||||
isLinesOnly,
|
||||
isPointsOnly,
|
||||
symbolId,
|
||||
})}
|
||||
</Fragment>
|
||||
);
|
||||
});
|
||||
|
||||
legendRows.push(
|
||||
<div key={i} className="vectorStyleLegendSpacer">
|
||||
{row}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return legendRows;
|
||||
}
|
||||
|
|
|
@ -1,37 +1,73 @@
|
|||
// 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
|
||||
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="#54B399"
|
||||
isLinesOnly={false}
|
||||
isPointsOnly={true}
|
||||
key="US"
|
||||
label="US_format"
|
||||
styleName="lineColor"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
key="1"
|
||||
>
|
||||
<Category
|
||||
color="#6092C0"
|
||||
isLinesOnly={false}
|
||||
isPointsOnly={true}
|
||||
key="CN"
|
||||
label="CN_format"
|
||||
styleName="lineColor"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
key="2"
|
||||
>
|
||||
<Category
|
||||
color="#D36086"
|
||||
isLinesOnly={false}
|
||||
isPointsOnly={true}
|
||||
key="fallbackCategory"
|
||||
label={
|
||||
<EuiTextColor
|
||||
color="secondary"
|
||||
|
@ -41,10 +77,16 @@ exports[`Should render categorical legend with breaks from default 1`] = `
|
|||
}
|
||||
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}
|
||||
|
@ -73,52 +115,30 @@ 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"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ordinal Should render only single band of last color when delta is 0 1`] = `
|
||||
<div>
|
||||
<EuiFlexGroup
|
||||
gutterSize="xs"
|
||||
justifyContent="spaceAround"
|
||||
justifyContent="spaceBetween"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
|
@ -147,5 +167,139 @@ exports[`Should render ordinal legend with breaks 1`] = `
|
|||
</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>
|
||||
`;
|
||||
|
|
|
@ -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>
|
||||
`;
|
||||
|
|
|
@ -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"
|
||||
/>
|
||||
`;
|
|
@ -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() {
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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(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() {
|
||||
if (this._options.useCustomColorRamp && this._options.customColorRamp) {
|
||||
return this._options.customColorRamp;
|
||||
}
|
||||
|
||||
_getColorRampStops() {
|
||||
return this._options.useCustomColorRamp && this._options.customColorRamp
|
||||
? 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(), () => {
|
||||
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,7 +35,8 @@ const defaultLegendParams = {
|
|||
isLinesOnly: false,
|
||||
};
|
||||
|
||||
test('Should render ordinal legend', async () => {
|
||||
describe('ordinal', () => {
|
||||
test('Should render ordinal legend as bands', async () => {
|
||||
const colorStyle = makeProperty({
|
||||
color: 'Blues',
|
||||
type: undefined,
|
||||
|
@ -39,10 +46,36 @@ test('Should render ordinal legend', async () => {
|
|||
|
||||
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 () => {
|
||||
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 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 custom ordinal legend with breaks', async () => {
|
||||
const colorStyle = makeProperty({
|
||||
type: COLOR_MAP_TYPE.ORDINAL,
|
||||
useCustomColorRamp: true,
|
||||
|
@ -69,7 +102,9 @@ test('Should render ordinal legend with breaks', async () => {
|
|||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('categorical', () => {
|
||||
test('Should render categorical legend with breaks from default', async () => {
|
||||
const colorStyle = makeProperty({
|
||||
type: COLOR_MAP_TYPE.CATEGORICAL,
|
||||
|
@ -115,6 +150,7 @@ test('Should render categorical legend with breaks from custom', async () => {
|
|||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
function makeFeatures(foobarPropValues) {
|
||||
return foobarPropValues.map((value) => {
|
||||
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,14 +73,10 @@ export class DynamicSizeProperty extends DynamicStyleProperty {
|
|||
}
|
||||
}
|
||||
|
||||
syncCircleStrokeWidthWithMb(mbLayerId, mbMap, hasNoRadius) {
|
||||
if (hasNoRadius) {
|
||||
mbMap.setPaintProperty(mbLayerId, 'circle-stroke-width', 0);
|
||||
} else {
|
||||
syncCircleStrokeWidthWithMb(mbLayerId, mbMap) {
|
||||
const lineWidth = this.getMbSizeExpression();
|
||||
mbMap.setPaintProperty(mbLayerId, 'circle-stroke-width', lineWidth);
|
||||
}
|
||||
}
|
||||
|
||||
syncCircleRadiusWithMb(mbLayerId, mbMap) {
|
||||
const circleRadius = this.getMbSizeExpression();
|
||||
|
@ -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} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue