mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
This allows users to style fields by category. Users can either uses one of default color palettes or specify a custom ramp. Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
8ff26e3e9f
commit
1cf848041d
32 changed files with 1459 additions and 378 deletions
|
@ -140,3 +140,12 @@ export const LAYER_STYLE_TYPE = {
|
|||
VECTOR: 'VECTOR',
|
||||
HEATMAP: 'HEATMAP',
|
||||
};
|
||||
|
||||
export const COLOR_MAP_TYPE = {
|
||||
CATEGORICAL: 'CATEGORICAL',
|
||||
ORDINAL: 'ORDINAL',
|
||||
};
|
||||
|
||||
export const COLOR_PALETTE_MAX_SIZE = 10;
|
||||
|
||||
export const CATEGORICAL_DATA_TYPES = ['string', 'ip', 'boolean'];
|
||||
|
|
|
@ -82,7 +82,7 @@ export class ESAggMetricField extends AbstractField {
|
|||
return !isMetricCountable(this.getAggType());
|
||||
}
|
||||
|
||||
async getFieldMetaRequest(config) {
|
||||
return this._esDocField.getFieldMetaRequest(config);
|
||||
async getOrdinalFieldMetaRequest(config) {
|
||||
return this._esDocField.getOrdinalFieldMetaRequest(config);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import { AbstractField } from './field';
|
||||
import { ESTooltipProperty } from '../tooltips/es_tooltip_property';
|
||||
import { COLOR_PALETTE_MAX_SIZE } from '../../../common/constants';
|
||||
|
||||
export class ESDocField extends AbstractField {
|
||||
static type = 'ES_DOC';
|
||||
|
@ -29,7 +30,7 @@ export class ESDocField extends AbstractField {
|
|||
return true;
|
||||
}
|
||||
|
||||
async getFieldMetaRequest(/* config */) {
|
||||
async getOrdinalFieldMetaRequest() {
|
||||
const field = await this._getField();
|
||||
|
||||
if (field.type !== 'number' && field.type !== 'date') {
|
||||
|
@ -51,4 +52,29 @@ export class ESDocField extends AbstractField {
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
async getCategoricalFieldMetaRequest() {
|
||||
const field = await this._getField();
|
||||
if (field.type !== 'string') {
|
||||
//UX does not support categorical styling for number/date fields
|
||||
return null;
|
||||
}
|
||||
|
||||
const topTerms = {
|
||||
size: COLOR_PALETTE_MAX_SIZE - 1, //need additional color for the "other"-value
|
||||
};
|
||||
if (field.scripted) {
|
||||
topTerms.script = {
|
||||
source: field.script,
|
||||
lang: field.lang,
|
||||
};
|
||||
} else {
|
||||
topTerms.field = this._fieldName;
|
||||
}
|
||||
return {
|
||||
[this._fieldName]: {
|
||||
terms: topTerms,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,7 +45,11 @@ export class AbstractField {
|
|||
return false;
|
||||
}
|
||||
|
||||
async getFieldMetaRequest(/* config */) {
|
||||
async getOrdinalFieldMetaRequest(/* config */) {
|
||||
return null;
|
||||
}
|
||||
|
||||
async getCategoricalFieldMetaRequest() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -344,6 +344,10 @@ export class AbstractLayer {
|
|||
return [];
|
||||
}
|
||||
|
||||
async getCategoricalFields() {
|
||||
return [];
|
||||
}
|
||||
|
||||
async getFields() {
|
||||
return [];
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
getDefaultDynamicProperties,
|
||||
VECTOR_STYLES,
|
||||
} from '../../styles/vector/vector_style_defaults';
|
||||
import { COLOR_GRADIENTS } from '../../styles/color_utils';
|
||||
import { RENDER_AS } from './render_as';
|
||||
import { CreateSourceEditor } from './create_source_editor';
|
||||
import { UpdateSourceEditor } from './update_source_editor';
|
||||
|
@ -249,7 +250,7 @@ export class ESGeoGridSource extends AbstractESAggSource {
|
|||
name: COUNT_PROP_NAME,
|
||||
origin: SOURCE_DATA_ID_ORIGIN,
|
||||
},
|
||||
color: 'Blues',
|
||||
color: COLOR_GRADIENTS[0].value,
|
||||
},
|
||||
},
|
||||
[VECTOR_STYLES.LINE_COLOR]: {
|
||||
|
|
|
@ -24,6 +24,7 @@ import { Schemas } from 'ui/vis/editors/default/schemas';
|
|||
import { AggConfigs } from 'ui/agg_types';
|
||||
import { AbstractESAggSource } from '../es_agg_source';
|
||||
import { DynamicStyleProperty } from '../../styles/vector/properties/dynamic_style_property';
|
||||
import { COLOR_GRADIENTS } from '../../styles/color_utils';
|
||||
|
||||
const MAX_GEOTILE_LEVEL = 29;
|
||||
|
||||
|
@ -136,7 +137,7 @@ export class ESPewPewSource extends AbstractESAggSource {
|
|||
name: COUNT_PROP_NAME,
|
||||
origin: SOURCE_DATA_ID_ORIGIN,
|
||||
},
|
||||
color: 'Blues',
|
||||
color: COLOR_GRADIENTS[0].value,
|
||||
},
|
||||
},
|
||||
[VECTOR_STYLES.LINE_WIDTH]: {
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
ES_GEO_FIELD_TYPE,
|
||||
DEFAULT_MAX_BUCKETS_LIMIT,
|
||||
SORT_ORDER,
|
||||
CATEGORICAL_DATA_TYPES,
|
||||
} from '../../../../common/constants';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { getDataSourceLabel } from '../../../../common/i18n_getters';
|
||||
|
@ -125,6 +126,27 @@ export class ESSearchSource extends AbstractESSource {
|
|||
}
|
||||
}
|
||||
|
||||
async getCategoricalFields() {
|
||||
try {
|
||||
const indexPattern = await this.getIndexPattern();
|
||||
|
||||
const aggFields = [];
|
||||
CATEGORICAL_DATA_TYPES.forEach(dataType => {
|
||||
indexPattern.fields.getByType(dataType).forEach(field => {
|
||||
if (field.aggregatable) {
|
||||
aggFields.push(field);
|
||||
}
|
||||
});
|
||||
});
|
||||
return aggFields.map(field => {
|
||||
return this.createField({ fieldName: field.name });
|
||||
});
|
||||
} catch (error) {
|
||||
//error surfaces in the LayerTOC UI
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async getFields() {
|
||||
try {
|
||||
const indexPattern = await this.getIndexPattern();
|
||||
|
|
|
@ -107,6 +107,10 @@ export class AbstractVectorSource extends AbstractSource {
|
|||
return [...(await this.getDateFields()), ...(await this.getNumberFields())];
|
||||
}
|
||||
|
||||
async getCategoricalFields() {
|
||||
return [];
|
||||
}
|
||||
|
||||
async getLeftJoinFields() {
|
||||
return [];
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import { ColorGradient } from './components/color_gradient';
|
|||
import { euiPaletteColorBlind } from '@elastic/eui/lib/services';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import chroma from 'chroma-js';
|
||||
import { COLOR_PALETTE_MAX_SIZE } from '../../../common/constants';
|
||||
|
||||
const GRADIENT_INTERVALS = 8;
|
||||
|
||||
|
@ -51,6 +52,9 @@ export function getHexColorRangeStrings(colorRampName, numberColors = GRADIENT_I
|
|||
}
|
||||
|
||||
export function getColorRampCenterColor(colorRampName) {
|
||||
if (!colorRampName) {
|
||||
return null;
|
||||
}
|
||||
const colorRamp = getColorRamp(colorRampName);
|
||||
const centerIndex = Math.floor(colorRamp.value.length / 2);
|
||||
return getColor(colorRamp.value, centerIndex);
|
||||
|
@ -58,7 +62,10 @@ 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 getColorRampStops(colorRampName, numberColors = GRADIENT_INTERVALS) {
|
||||
export function getOrdinalColorRampStops(colorRampName, numberColors = GRADIENT_INTERVALS) {
|
||||
if (!colorRampName) {
|
||||
return null;
|
||||
}
|
||||
return getHexColorRangeStrings(colorRampName, numberColors).reduce(
|
||||
(accu, stopColor, idx, srcArr) => {
|
||||
const stopNumber = idx / srcArr.length; // number between 0 and 1, increasing as index increases
|
||||
|
@ -84,3 +91,62 @@ export function getLinearGradient(colorStrings) {
|
|||
}
|
||||
return `${linearGradient} ${colorStrings[colorStrings.length - 1]} 100%)`;
|
||||
}
|
||||
|
||||
const COLOR_PALETTES_CONFIGS = [
|
||||
{
|
||||
id: 'palette_0',
|
||||
colors: DEFAULT_FILL_COLORS.slice(0, COLOR_PALETTE_MAX_SIZE),
|
||||
},
|
||||
{
|
||||
id: 'palette_1',
|
||||
colors: [
|
||||
'#a6cee3',
|
||||
'#1f78b4',
|
||||
'#b2df8a',
|
||||
'#33a02c',
|
||||
'#fb9a99',
|
||||
'#e31a1c',
|
||||
'#fdbf6f',
|
||||
'#ff7f00',
|
||||
'#cab2d6',
|
||||
'#6a3d9a',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'palette_2',
|
||||
colors: [
|
||||
'#8dd3c7',
|
||||
'#ffffb3',
|
||||
'#bebada',
|
||||
'#fb8072',
|
||||
'#80b1d3',
|
||||
'#fdb462',
|
||||
'#b3de69',
|
||||
'#fccde5',
|
||||
'#d9d9d9',
|
||||
'#bc80bd',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export function getColorPalette(paletteId) {
|
||||
const palette = COLOR_PALETTES_CONFIGS.find(palette => palette.id === paletteId);
|
||||
return palette ? palette.colors : null;
|
||||
}
|
||||
|
||||
export const COLOR_PALETTES = COLOR_PALETTES_CONFIGS.map(palette => {
|
||||
const paletteDisplay = palette.colors.map(color => {
|
||||
const style = {
|
||||
backgroundColor: color,
|
||||
width: '10%',
|
||||
position: 'relative',
|
||||
height: '100%',
|
||||
display: 'inline-block',
|
||||
};
|
||||
return <div style={style}> </div>;
|
||||
});
|
||||
return {
|
||||
value: palette.id,
|
||||
inputDisplay: <div className={'mapColorGradient'}>{paletteDisplay}</div>,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import {
|
||||
COLOR_GRADIENTS,
|
||||
getColorRampCenterColor,
|
||||
getColorRampStops,
|
||||
getOrdinalColorRampStops,
|
||||
getHexColorRangeStrings,
|
||||
getLinearGradient,
|
||||
getRGBColorRangeStrings,
|
||||
|
@ -59,7 +59,7 @@ describe('getColorRampCenterColor', () => {
|
|||
|
||||
describe('getColorRampStops', () => {
|
||||
it('Should create color stops for color ramp', () => {
|
||||
expect(getColorRampStops('Blues')).toEqual([
|
||||
expect(getOrdinalColorRampStops('Blues')).toEqual([
|
||||
0,
|
||||
'#f7faff',
|
||||
0.125,
|
||||
|
|
|
@ -11,7 +11,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 } from '../../../../common/constants';
|
||||
import { getColorRampStops } from '../color_utils';
|
||||
import { getOrdinalColorRampStops } from '../color_utils';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiIcon } from '@elastic/eui';
|
||||
|
||||
|
@ -81,7 +81,7 @@ export class HeatmapStyle extends AbstractStyle {
|
|||
|
||||
const { colorRampName } = this._descriptor;
|
||||
if (colorRampName && colorRampName !== DEFAULT_HEATMAP_COLOR_RAMP_NAME) {
|
||||
const colorStops = getColorRampStops(colorRampName);
|
||||
const colorStops = getOrdinalColorRampStops(colorRampName);
|
||||
mbMap.setPaintProperty(layerId, 'heatmap-color', [
|
||||
'interpolate',
|
||||
['linear'],
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* 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, { Component, Fragment } from 'react';
|
||||
|
||||
import { EuiSuperSelect, EuiSpacer } from '@elastic/eui';
|
||||
import { ColorStopsOrdinal } from './color_stops_ordinal';
|
||||
import { COLOR_MAP_TYPE } from '../../../../../../common/constants';
|
||||
import { ColorStopsCategorical } from './color_stops_categorical';
|
||||
|
||||
const CUSTOM_COLOR_MAP = 'CUSTOM_COLOR_MAP';
|
||||
|
||||
export class ColorMapSelect extends Component {
|
||||
state = {
|
||||
selected: '',
|
||||
};
|
||||
|
||||
static getDerivedStateFromProps(nextProps, prevState) {
|
||||
if (nextProps.customColorMap === prevState.prevPropsCustomColorMap) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
prevPropsCustomColorMap: nextProps.customColorMap, // reset tracker to latest value
|
||||
customColorMap: nextProps.customColorMap, // reset customColorMap to latest value
|
||||
};
|
||||
}
|
||||
|
||||
_onColorMapSelect = selectedValue => {
|
||||
const useCustomColorMap = selectedValue === CUSTOM_COLOR_MAP;
|
||||
this.props.onChange({
|
||||
color: useCustomColorMap ? null : selectedValue,
|
||||
useCustomColorMap,
|
||||
type: this.props.colorMapType,
|
||||
});
|
||||
};
|
||||
|
||||
_onCustomColorMapChange = ({ colorStops, isInvalid }) => {
|
||||
// Manage invalid custom color map in local state
|
||||
if (isInvalid) {
|
||||
const newState = {
|
||||
customColorMap: colorStops,
|
||||
};
|
||||
this.setState(newState);
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onChange({
|
||||
useCustomColorMap: true,
|
||||
customColorMap: colorStops,
|
||||
type: this.props.colorMapType,
|
||||
});
|
||||
};
|
||||
|
||||
_renderColorStopsInput() {
|
||||
let colorStopsInput;
|
||||
if (this.props.useCustomColorMap) {
|
||||
if (this.props.colorMapType === COLOR_MAP_TYPE.ORDINAL) {
|
||||
colorStopsInput = (
|
||||
<Fragment>
|
||||
<EuiSpacer size="s" />
|
||||
<ColorStopsOrdinal
|
||||
colorStops={this.state.customColorMap}
|
||||
onChange={this._onCustomColorMapChange}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
} else if (this.props.colorMapType === COLOR_MAP_TYPE.CATEGORICAL) {
|
||||
colorStopsInput = (
|
||||
<Fragment>
|
||||
<EuiSpacer size="s" />
|
||||
<ColorStopsCategorical
|
||||
colorStops={this.state.customColorMap}
|
||||
onChange={this._onCustomColorMapChange}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
return colorStopsInput;
|
||||
}
|
||||
|
||||
render() {
|
||||
const colorStopsInput = this._renderColorStopsInput();
|
||||
const colorMapOptionsWithCustom = [
|
||||
{
|
||||
value: CUSTOM_COLOR_MAP,
|
||||
inputDisplay: this.props.customOptionLabel,
|
||||
},
|
||||
...this.props.colorMapOptions,
|
||||
];
|
||||
|
||||
let valueOfSelected;
|
||||
if (this.props.useCustomColorMap) {
|
||||
valueOfSelected = CUSTOM_COLOR_MAP;
|
||||
} else {
|
||||
valueOfSelected = this.props.colorMapOptions.find(option => option.value === this.props.color)
|
||||
? this.props.color
|
||||
: '';
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiSuperSelect
|
||||
options={colorMapOptionsWithCustom}
|
||||
onChange={this._onColorMapSelect}
|
||||
valueOfSelected={valueOfSelected}
|
||||
hasDividers={true}
|
||||
/>
|
||||
{colorStopsInput}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,106 +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, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { EuiSuperSelect, EuiSpacer } from '@elastic/eui';
|
||||
import { COLOR_GRADIENTS } from '../../../color_utils';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { ColorStops } from './color_stops';
|
||||
|
||||
const CUSTOM_COLOR_RAMP = 'CUSTOM_COLOR_RAMP';
|
||||
|
||||
export class ColorRampSelect extends Component {
|
||||
state = {};
|
||||
|
||||
static getDerivedStateFromProps(nextProps, prevState) {
|
||||
if (nextProps.customColorRamp !== prevState.prevPropsCustomColorRamp) {
|
||||
return {
|
||||
prevPropsCustomColorRamp: nextProps.customColorRamp, // reset tracker to latest value
|
||||
customColorRamp: nextProps.customColorRamp, // reset customColorRamp to latest value
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
_onColorRampSelect = selectedValue => {
|
||||
const useCustomColorRamp = selectedValue === CUSTOM_COLOR_RAMP;
|
||||
this.props.onChange({
|
||||
color: useCustomColorRamp ? null : selectedValue,
|
||||
useCustomColorRamp,
|
||||
});
|
||||
};
|
||||
|
||||
_onCustomColorRampChange = ({ colorStops, isInvalid }) => {
|
||||
// Manage invalid custom color ramp in local state
|
||||
if (isInvalid) {
|
||||
this.setState({ customColorRamp: colorStops });
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onChange({
|
||||
customColorRamp: colorStops,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
color,
|
||||
onChange, // eslint-disable-line no-unused-vars
|
||||
useCustomColorRamp,
|
||||
customColorRamp, // eslint-disable-line no-unused-vars
|
||||
...rest
|
||||
} = this.props;
|
||||
|
||||
let colorStopsInput;
|
||||
if (useCustomColorRamp) {
|
||||
colorStopsInput = (
|
||||
<Fragment>
|
||||
<EuiSpacer size="s" />
|
||||
<ColorStops
|
||||
colorStops={this.state.customColorRamp}
|
||||
onChange={this._onCustomColorRampChange}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
const colorRampOptions = [
|
||||
{
|
||||
value: CUSTOM_COLOR_RAMP,
|
||||
inputDisplay: (
|
||||
<FormattedMessage
|
||||
id="xpack.maps.style.customColorRampLabel"
|
||||
defaultMessage="Custom color ramp"
|
||||
/>
|
||||
),
|
||||
},
|
||||
...COLOR_GRADIENTS,
|
||||
];
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiSuperSelect
|
||||
options={colorRampOptions}
|
||||
onChange={this._onColorRampSelect}
|
||||
valueOfSelected={useCustomColorRamp ? CUSTOM_COLOR_RAMP : color}
|
||||
hasDividers={true}
|
||||
{...rest}
|
||||
/>
|
||||
{colorStopsInput}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ColorRampSelect.propTypes = {
|
||||
color: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
useCustomColorRamp: PropTypes.bool,
|
||||
customColorRamp: PropTypes.array,
|
||||
};
|
|
@ -6,66 +6,106 @@
|
|||
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { removeRow, isColorInvalid } from './color_stops_utils';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiButtonIcon, EuiColorPicker, EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui';
|
||||
|
||||
import {
|
||||
EuiColorPicker,
|
||||
EuiFormRow,
|
||||
EuiFieldNumber,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButtonIcon,
|
||||
} from '@elastic/eui';
|
||||
import { addRow, removeRow, isColorInvalid, isStopInvalid, isInvalid } from './color_stops_utils';
|
||||
function getColorStopRow({ index, errors, stopInput, colorInput, deleteButton, onAdd }) {
|
||||
return (
|
||||
<EuiFormRow
|
||||
key={index}
|
||||
className="mapColorStop"
|
||||
isInvalid={errors.length !== 0}
|
||||
error={errors}
|
||||
display="rowCompressed"
|
||||
>
|
||||
<div>
|
||||
<EuiFlexGroup responsive={false} alignItems="center" gutterSize="xs">
|
||||
<EuiFlexItem>{stopInput}</EuiFlexItem>
|
||||
<EuiFlexItem>{colorInput}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<div className="mapColorStop__icons">
|
||||
{deleteButton}
|
||||
<EuiButtonIcon
|
||||
iconType="plusInCircle"
|
||||
color="primary"
|
||||
aria-label="Add"
|
||||
title="Add"
|
||||
onClick={onAdd}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
|
||||
const DEFAULT_COLOR = '#FF0000';
|
||||
export function getDeleteButton(onRemove) {
|
||||
return (
|
||||
<EuiButtonIcon
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
aria-label={i18n.translate('xpack.maps.styles.colorStops.deleteButtonAriaLabel', {
|
||||
defaultMessage: 'Delete',
|
||||
})}
|
||||
title={i18n.translate('xpack.maps.styles.colorStops.deleteButtonLabel', {
|
||||
defaultMessage: 'Delete',
|
||||
})}
|
||||
onClick={onRemove}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const ColorStops = ({ colorStops = [{ stop: 0, color: DEFAULT_COLOR }], onChange }) => {
|
||||
export const ColorStops = ({
|
||||
onChange,
|
||||
colorStops,
|
||||
isStopsInvalid,
|
||||
sanitizeStopInput,
|
||||
getStopError,
|
||||
renderStopInput,
|
||||
addNewRow,
|
||||
canDeleteStop,
|
||||
}) => {
|
||||
function getStopInput(stop, index) {
|
||||
const onStopChange = e => {
|
||||
const newColorStops = _.cloneDeep(colorStops);
|
||||
const sanitizedValue = parseFloat(e.target.value);
|
||||
newColorStops[index].stop = isNaN(sanitizedValue) ? '' : sanitizedValue;
|
||||
newColorStops[index].stop = sanitizeStopInput(e.target.value);
|
||||
const invalid = isStopsInvalid(newColorStops);
|
||||
onChange({
|
||||
colorStops: newColorStops,
|
||||
isInvalid: isInvalid(newColorStops),
|
||||
isInvalid: invalid,
|
||||
});
|
||||
};
|
||||
|
||||
let error;
|
||||
if (isStopInvalid(stop)) {
|
||||
error = 'Stop must be a number';
|
||||
} else if (index !== 0 && colorStops[index - 1].stop >= stop) {
|
||||
error = 'Stop must be greater than previous stop value';
|
||||
}
|
||||
|
||||
const error = getStopError(stop, index);
|
||||
return {
|
||||
stopError: error,
|
||||
stopInput: (
|
||||
<EuiFieldNumber aria-label="Stop" value={stop} onChange={onStopChange} compressed />
|
||||
),
|
||||
stopInput: renderStopInput(stop, onStopChange, index),
|
||||
};
|
||||
}
|
||||
|
||||
function getColorInput(color, index) {
|
||||
const onColorChange = color => {
|
||||
const newColorStops = _.cloneDeep(colorStops);
|
||||
newColorStops[index].color = color;
|
||||
onChange({
|
||||
colorStops: newColorStops,
|
||||
isInvalid: isInvalid(newColorStops),
|
||||
});
|
||||
};
|
||||
|
||||
function getColorInput(onColorChange, color) {
|
||||
return {
|
||||
colorError: isColorInvalid(color) ? 'Color must provide a valid hex value' : undefined,
|
||||
colorError: isColorInvalid(color)
|
||||
? i18n.translate('xpack.maps.styles.colorStops.hexWarningLabel', {
|
||||
defaultMessage: 'Color must provide a valid hex value',
|
||||
})
|
||||
: undefined,
|
||||
colorInput: <EuiColorPicker onChange={onColorChange} color={color} compressed />,
|
||||
};
|
||||
}
|
||||
|
||||
const rows = colorStops.map((colorStop, index) => {
|
||||
const onColorChange = color => {
|
||||
const newColorStops = _.cloneDeep(colorStops);
|
||||
newColorStops[index].color = color;
|
||||
onChange({
|
||||
colorStops: newColorStops,
|
||||
isInvalid: isStopsInvalid(newColorStops),
|
||||
});
|
||||
};
|
||||
|
||||
const { stopError, stopInput } = getStopInput(colorStop.stop, index);
|
||||
const { colorError, colorInput } = getColorInput(colorStop.color, index);
|
||||
const { colorError, colorInput } = getColorInput(onColorChange, colorStop.color);
|
||||
const errors = [];
|
||||
if (stopError) {
|
||||
errors.push(stopError);
|
||||
|
@ -74,82 +114,28 @@ export const ColorStops = ({ colorStops = [{ stop: 0, color: DEFAULT_COLOR }], o
|
|||
errors.push(colorError);
|
||||
}
|
||||
|
||||
const onRemove = () => {
|
||||
const newColorStops = removeRow(colorStops, index);
|
||||
onChange({
|
||||
colorStops: newColorStops,
|
||||
isInvalid: isInvalid(newColorStops),
|
||||
});
|
||||
};
|
||||
|
||||
const onAdd = () => {
|
||||
const newColorStops = addRow(colorStops, index);
|
||||
|
||||
const newColorStops = addNewRow(colorStops, index);
|
||||
onChange({
|
||||
colorStops: newColorStops,
|
||||
isInvalid: isInvalid(newColorStops),
|
||||
isInvalid: isStopsInvalid(newColorStops),
|
||||
});
|
||||
};
|
||||
|
||||
let deleteButton;
|
||||
if (colorStops.length > 1) {
|
||||
deleteButton = (
|
||||
<EuiButtonIcon
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
aria-label="Delete"
|
||||
title="Delete"
|
||||
onClick={onRemove}
|
||||
/>
|
||||
);
|
||||
if (canDeleteStop(colorStops, index)) {
|
||||
const onRemove = () => {
|
||||
const newColorStops = removeRow(colorStops, index);
|
||||
onChange({
|
||||
colorStops: newColorStops,
|
||||
isInvalid: isStopsInvalid(newColorStops),
|
||||
});
|
||||
};
|
||||
deleteButton = getDeleteButton(onRemove);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
key={index}
|
||||
className="mapColorStop"
|
||||
isInvalid={errors.length !== 0}
|
||||
error={errors}
|
||||
display="rowCompressed"
|
||||
>
|
||||
<div>
|
||||
<EuiFlexGroup responsive={false} alignItems="center" gutterSize="xs">
|
||||
<EuiFlexItem>{stopInput}</EuiFlexItem>
|
||||
<EuiFlexItem>{colorInput}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<div className="mapColorStop__icons">
|
||||
{deleteButton}
|
||||
<EuiButtonIcon
|
||||
iconType="plusInCircle"
|
||||
color="primary"
|
||||
aria-label="Add"
|
||||
title="Add"
|
||||
onClick={onAdd}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</EuiFormRow>
|
||||
);
|
||||
return getColorStopRow({ index, errors, stopInput, colorInput, deleteButton, onAdd });
|
||||
});
|
||||
|
||||
return <div>{rows}</div>;
|
||||
};
|
||||
|
||||
ColorStops.propTypes = {
|
||||
/**
|
||||
* Array of { stop, color }.
|
||||
* Stops are numbers in strictly ascending order.
|
||||
* The range is from the given stop number (inclusive) to the next stop number (exclusive).
|
||||
* Colors are color hex strings (3 or 6 character).
|
||||
*/
|
||||
colorStops: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
stopKey: PropTypes.number,
|
||||
color: PropTypes.string,
|
||||
})
|
||||
),
|
||||
/**
|
||||
* Callback for when the color stops changes. Called with { colorStops, isInvalid }
|
||||
*/
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { EuiFieldText } from '@elastic/eui';
|
||||
import {
|
||||
addCategoricalRow,
|
||||
isCategoricalStopsInvalid,
|
||||
getOtherCategoryLabel,
|
||||
DEFAULT_CUSTOM_COLOR,
|
||||
DEFAULT_NEXT_COLOR,
|
||||
} from './color_stops_utils';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ColorStops } from './color_stops';
|
||||
|
||||
export const ColorStopsCategorical = ({
|
||||
colorStops = [
|
||||
{ stop: null, color: DEFAULT_CUSTOM_COLOR }, //first stop is the "other" color
|
||||
{ stop: '', color: DEFAULT_NEXT_COLOR },
|
||||
],
|
||||
onChange,
|
||||
}) => {
|
||||
const sanitizeStopInput = value => {
|
||||
return value;
|
||||
};
|
||||
|
||||
const getStopError = (stop, index) => {
|
||||
let count = 0;
|
||||
for (let i = 1; i < colorStops.length; i++) {
|
||||
if (colorStops[i].stop === stop && i !== index) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return count
|
||||
? i18n.translate('xpack.maps.styles.colorStops.categoricalStop.noDupesWarningLabel', {
|
||||
defaultMessage: 'Stop values must be unique',
|
||||
})
|
||||
: null;
|
||||
};
|
||||
|
||||
const renderStopInput = (stop, onStopChange, index) => {
|
||||
const stopValue = typeof stop === 'string' ? stop : '';
|
||||
if (index === 0) {
|
||||
return (
|
||||
<EuiFieldText
|
||||
aria-label={i18n.translate(
|
||||
'xpack.maps.styles.colorStops.categoricalStop.defaultCategoryAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Default stop',
|
||||
}
|
||||
)}
|
||||
value={stopValue}
|
||||
placeholder={getOtherCategoryLabel()}
|
||||
disabled
|
||||
onChange={onStopChange}
|
||||
compressed
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<EuiFieldText
|
||||
aria-label={i18n.translate(
|
||||
'xpack.maps.styles.colorStops.categoricalStop.categoryAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Category',
|
||||
}
|
||||
)}
|
||||
value={stopValue}
|
||||
onChange={onStopChange}
|
||||
compressed
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const canDeleteStop = (colorStops, index) => {
|
||||
return colorStops.length > 2 && index !== 0;
|
||||
};
|
||||
|
||||
return (
|
||||
<ColorStops
|
||||
onChange={onChange}
|
||||
colorStops={colorStops}
|
||||
isStopsInvalid={isCategoricalStopsInvalid}
|
||||
sanitizeStopInput={sanitizeStopInput}
|
||||
getStopError={getStopError}
|
||||
renderStopInput={renderStopInput}
|
||||
canDeleteStop={canDeleteStop}
|
||||
addNewRow={addCategoricalRow}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
ColorStopsCategorical.propTypes = {
|
||||
/**
|
||||
* Array of { stop, color }.
|
||||
* Stops are any strings
|
||||
* Stops cannot include duplicates
|
||||
* Colors are color hex strings (3 or 6 character).
|
||||
*/
|
||||
colorStops: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
stopKey: PropTypes.number,
|
||||
color: PropTypes.string,
|
||||
})
|
||||
),
|
||||
/**
|
||||
* Callback for when the color stops changes. Called with { colorStops, isInvalid }
|
||||
*/
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { ColorStops } from './color_stops';
|
||||
import { EuiFieldNumber } from '@elastic/eui';
|
||||
import {
|
||||
addOrdinalRow,
|
||||
isOrdinalStopInvalid,
|
||||
isOrdinalStopsInvalid,
|
||||
DEFAULT_CUSTOM_COLOR,
|
||||
} from './color_stops_utils';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const ColorStopsOrdinal = ({
|
||||
colorStops = [{ stop: 0, color: DEFAULT_CUSTOM_COLOR }],
|
||||
onChange,
|
||||
}) => {
|
||||
const sanitizeStopInput = value => {
|
||||
const sanitizedValue = parseFloat(value);
|
||||
return isNaN(sanitizedValue) ? '' : sanitizedValue;
|
||||
};
|
||||
|
||||
const getStopError = (stop, index) => {
|
||||
let error;
|
||||
if (isOrdinalStopInvalid(stop)) {
|
||||
error = i18n.translate('xpack.maps.styles.colorStops.ordinalStop.numberWarningLabel', {
|
||||
defaultMessage: 'Stop must be a number',
|
||||
});
|
||||
} else if (index !== 0 && colorStops[index - 1].stop >= stop) {
|
||||
error = i18n.translate(
|
||||
'xpack.maps.styles.colorStops.ordinalStop.numberOrderingWarningLabel',
|
||||
{
|
||||
defaultMessage: 'Stop must be greater than previous stop value',
|
||||
}
|
||||
);
|
||||
}
|
||||
return error;
|
||||
};
|
||||
|
||||
const renderStopInput = (stop, onStopChange) => {
|
||||
return (
|
||||
<EuiFieldNumber
|
||||
aria-label={i18n.translate('xpack.maps.styles.colorStops.ordinalStop.stopLabel', {
|
||||
defaultMessage: 'Stop',
|
||||
})}
|
||||
value={stop}
|
||||
onChange={onStopChange}
|
||||
compressed
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const canDeleteStop = colorStops => {
|
||||
return colorStops.length > 1;
|
||||
};
|
||||
|
||||
return (
|
||||
<ColorStops
|
||||
onChange={onChange}
|
||||
colorStops={colorStops}
|
||||
isStopsInvalid={isOrdinalStopsInvalid}
|
||||
sanitizeStopInput={sanitizeStopInput}
|
||||
getStopError={getStopError}
|
||||
renderStopInput={renderStopInput}
|
||||
canDeleteStop={canDeleteStop}
|
||||
addNewRow={addOrdinalRow}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
ColorStopsOrdinal.propTypes = {
|
||||
/**
|
||||
* Array of { stop, color }.
|
||||
* Stops are numbers in strictly ascending order.
|
||||
* The range is from the given stop number (inclusive) to the next stop number (exclusive).
|
||||
* Colors are color hex strings (3 or 6 character).
|
||||
*/
|
||||
colorStops: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
stopKey: PropTypes.number,
|
||||
color: PropTypes.string,
|
||||
})
|
||||
),
|
||||
/**
|
||||
* Callback for when the color stops changes. Called with { colorStops, isInvalid }
|
||||
*/
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
|
@ -5,6 +5,11 @@
|
|||
*/
|
||||
|
||||
import { isValidHex } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import _ from 'lodash';
|
||||
|
||||
export const DEFAULT_CUSTOM_COLOR = '#FF0000';
|
||||
export const DEFAULT_NEXT_COLOR = '#00FF00';
|
||||
|
||||
export function removeRow(colorStops, index) {
|
||||
if (colorStops.length === 1) {
|
||||
|
@ -14,7 +19,7 @@ export function removeRow(colorStops, index) {
|
|||
return [...colorStops.slice(0, index), ...colorStops.slice(index + 1)];
|
||||
}
|
||||
|
||||
export function addRow(colorStops, index) {
|
||||
export function addOrdinalRow(colorStops, index) {
|
||||
const currentStop = colorStops[index].stop;
|
||||
let delta = 1;
|
||||
if (index === colorStops.length - 1) {
|
||||
|
@ -28,10 +33,20 @@ export function addRow(colorStops, index) {
|
|||
const nextStop = colorStops[index + 1].stop;
|
||||
delta = (nextStop - currentStop) / 2;
|
||||
}
|
||||
const nextValue = currentStop + delta;
|
||||
return addRow(colorStops, index, nextValue);
|
||||
}
|
||||
|
||||
export function addCategoricalRow(colorStops, index) {
|
||||
const currentStop = colorStops[index].stop;
|
||||
const nextValue = currentStop === '' ? currentStop + 'a' : '';
|
||||
return addRow(colorStops, index, nextValue);
|
||||
}
|
||||
|
||||
function addRow(colorStops, index, nextValue) {
|
||||
const newRow = {
|
||||
stop: currentStop + delta,
|
||||
color: '#FF0000',
|
||||
stop: nextValue,
|
||||
color: DEFAULT_CUSTOM_COLOR,
|
||||
};
|
||||
return [...colorStops.slice(0, index + 1), newRow, ...colorStops.slice(index + 1)];
|
||||
}
|
||||
|
@ -40,11 +55,18 @@ export function isColorInvalid(color) {
|
|||
return !isValidHex(color) || color === '';
|
||||
}
|
||||
|
||||
export function isStopInvalid(stop) {
|
||||
export function isOrdinalStopInvalid(stop) {
|
||||
return stop === '' || isNaN(stop);
|
||||
}
|
||||
|
||||
export function isInvalid(colorStops) {
|
||||
export function isCategoricalStopsInvalid(colorStops) {
|
||||
const nonDefaults = colorStops.slice(1); //
|
||||
const values = nonDefaults.map(stop => stop.stop);
|
||||
const uniques = _.uniq(values);
|
||||
return values.length !== uniques.length;
|
||||
}
|
||||
|
||||
export function isOrdinalStopsInvalid(colorStops) {
|
||||
return colorStops.some((colorStop, index) => {
|
||||
// expect stops to be in ascending order
|
||||
let isDescending = false;
|
||||
|
@ -53,6 +75,12 @@ export function isInvalid(colorStops) {
|
|||
isDescending = prevStop >= colorStop.stop;
|
||||
}
|
||||
|
||||
return isColorInvalid(colorStop.color) || isStopInvalid(colorStop.stop) || isDescending;
|
||||
return isColorInvalid(colorStop.color) || isOrdinalStopInvalid(colorStop.stop) || isDescending;
|
||||
});
|
||||
}
|
||||
|
||||
export function getOtherCategoryLabel() {
|
||||
return i18n.translate('xpack.maps.styles.categorical.otherCategoryLabel', {
|
||||
defaultMessage: 'Other',
|
||||
});
|
||||
}
|
||||
|
|
|
@ -7,56 +7,146 @@
|
|||
import _ from 'lodash';
|
||||
import React, { Fragment } from 'react';
|
||||
import { FieldSelect } from '../field_select';
|
||||
import { ColorRampSelect } from './color_ramp_select';
|
||||
import { ColorMapSelect } from './color_map_select';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||
import { CATEGORICAL_DATA_TYPES, COLOR_MAP_TYPE } from '../../../../../../common/constants';
|
||||
import { COLOR_GRADIENTS, COLOR_PALETTES } from '../../../color_utils';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export function DynamicColorForm({
|
||||
fields,
|
||||
onDynamicStyleChange,
|
||||
staticDynamicSelect,
|
||||
styleProperty,
|
||||
}) {
|
||||
const styleOptions = styleProperty.getOptions();
|
||||
|
||||
const onFieldChange = ({ field }) => {
|
||||
onDynamicStyleChange(styleProperty.getStyleName(), { ...styleOptions, field });
|
||||
export class DynamicColorForm extends React.Component {
|
||||
state = {
|
||||
colorMapType: COLOR_MAP_TYPE.ORDINAL,
|
||||
};
|
||||
|
||||
const onColorChange = colorOptions => {
|
||||
onDynamicStyleChange(styleProperty.getStyleName(), {
|
||||
...styleOptions,
|
||||
...colorOptions,
|
||||
});
|
||||
};
|
||||
|
||||
let colorRampSelect;
|
||||
if (styleOptions.field && styleOptions.field.name) {
|
||||
colorRampSelect = (
|
||||
<ColorRampSelect
|
||||
onChange={onColorChange}
|
||||
color={styleOptions.color}
|
||||
customColorRamp={styleOptions.customColorRamp}
|
||||
useCustomColorRamp={_.get(styleOptions, 'useCustomColorRamp', false)}
|
||||
compressed
|
||||
/>
|
||||
);
|
||||
constructor() {
|
||||
super();
|
||||
this._isMounted = false;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiFlexGroup gutterSize="none" justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>{staticDynamicSelect}</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<FieldSelect
|
||||
fields={fields}
|
||||
selectedFieldName={_.get(styleOptions, 'field.name')}
|
||||
onChange={onFieldChange}
|
||||
compressed
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="s" />
|
||||
{colorRampSelect}
|
||||
</Fragment>
|
||||
);
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
this._loadColorMapType();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this._loadColorMapType();
|
||||
}
|
||||
|
||||
async _loadColorMapType() {
|
||||
const field = this.props.styleProperty.getField();
|
||||
if (!field) {
|
||||
return;
|
||||
}
|
||||
const dataType = await field.getDataType();
|
||||
const colorMapType = CATEGORICAL_DATA_TYPES.includes(dataType)
|
||||
? COLOR_MAP_TYPE.CATEGORICAL
|
||||
: COLOR_MAP_TYPE.ORDINAL;
|
||||
if (this._isMounted && this.state.colorMapType !== colorMapType) {
|
||||
this.setState({ colorMapType }, () => {
|
||||
const options = this.props.styleProperty.getOptions();
|
||||
this.props.onDynamicStyleChange(this.props.styleProperty.getStyleName(), {
|
||||
...options,
|
||||
type: colorMapType,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_getColorSelector() {
|
||||
const { onDynamicStyleChange, styleProperty } = this.props;
|
||||
const styleOptions = styleProperty.getOptions();
|
||||
|
||||
if (!styleOptions.field || !styleOptions.field.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
let colorSelect;
|
||||
const onColorChange = colorOptions => {
|
||||
const newColorOptions = {
|
||||
type: colorOptions.type,
|
||||
};
|
||||
if (colorOptions.type === COLOR_MAP_TYPE.ORDINAL) {
|
||||
newColorOptions.useCustomColorRamp = colorOptions.useCustomColorMap;
|
||||
newColorOptions.customColorRamp = colorOptions.customColorMap;
|
||||
newColorOptions.color = colorOptions.color;
|
||||
} else {
|
||||
newColorOptions.useCustomColorPalette = colorOptions.useCustomColorMap;
|
||||
newColorOptions.customColorPalette = colorOptions.customColorMap;
|
||||
newColorOptions.colorCategory = colorOptions.color;
|
||||
}
|
||||
|
||||
onDynamicStyleChange(styleProperty.getStyleName(), {
|
||||
...styleOptions,
|
||||
...newColorOptions,
|
||||
});
|
||||
};
|
||||
|
||||
if (this.state.colorMapType === COLOR_MAP_TYPE.ORDINAL) {
|
||||
const customOptionLabel = i18n.translate('xpack.maps.style.customColorRampLabel', {
|
||||
defaultMessage: 'Custom color ramp',
|
||||
});
|
||||
colorSelect = (
|
||||
<ColorMapSelect
|
||||
colorMapOptions={COLOR_GRADIENTS}
|
||||
customOptionLabel={customOptionLabel}
|
||||
onChange={options => onColorChange(options)}
|
||||
colorMapType={COLOR_MAP_TYPE.ORDINAL}
|
||||
color={styleOptions.color}
|
||||
customColorMap={styleOptions.customColorRamp}
|
||||
useCustomColorMap={_.get(styleOptions, 'useCustomColorRamp', false)}
|
||||
compressed
|
||||
/>
|
||||
);
|
||||
} else if (this.state.colorMapType === COLOR_MAP_TYPE.CATEGORICAL) {
|
||||
const customOptionLabel = i18n.translate('xpack.maps.style.customColorPaletteLabel', {
|
||||
defaultMessage: 'Custom color palette',
|
||||
});
|
||||
colorSelect = (
|
||||
<ColorMapSelect
|
||||
colorMapOptions={COLOR_PALETTES}
|
||||
customOptionLabel={customOptionLabel}
|
||||
onChange={options => onColorChange(options)}
|
||||
colorMapType={COLOR_MAP_TYPE.CATEGORICAL}
|
||||
color={styleOptions.colorCategory}
|
||||
customColorMap={styleOptions.customColorPalette}
|
||||
useCustomColorMap={_.get(styleOptions, 'useCustomColorPalette', false)}
|
||||
compressed
|
||||
/>
|
||||
);
|
||||
}
|
||||
return colorSelect;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { fields, onDynamicStyleChange, staticDynamicSelect, styleProperty } = this.props;
|
||||
const styleOptions = styleProperty.getOptions();
|
||||
const onFieldChange = options => {
|
||||
const field = options.field;
|
||||
onDynamicStyleChange(styleProperty.getStyleName(), { ...styleOptions, field });
|
||||
};
|
||||
|
||||
const colorSelect = this._getColorSelector();
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiFlexGroup gutterSize="none" justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>{staticDynamicSelect}</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<FieldSelect
|
||||
fields={fields}
|
||||
selectedFieldName={_.get(styleOptions, 'field.name')}
|
||||
onChange={onFieldChange}
|
||||
compressed
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="s" />
|
||||
{colorSelect}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
*/
|
||||
|
||||
import { VectorStyle } from '../../vector_style';
|
||||
import { getColorRampCenterColor } from '../../../color_utils';
|
||||
import { getColorRampCenterColor, getColorPalette } from '../../../color_utils';
|
||||
import { COLOR_MAP_TYPE } from '../../../../../../common/constants';
|
||||
|
||||
export function extractColorFromStyleProperty(colorStyleProperty, defaultColor) {
|
||||
if (!colorStyleProperty) {
|
||||
|
@ -21,19 +22,37 @@ export function extractColorFromStyleProperty(colorStyleProperty, defaultColor)
|
|||
return defaultColor;
|
||||
}
|
||||
|
||||
// return middle of gradient for dynamic style property
|
||||
|
||||
if (colorStyleProperty.options.useCustomColorRamp) {
|
||||
if (
|
||||
!colorStyleProperty.options.customColorRamp ||
|
||||
!colorStyleProperty.options.customColorRamp.length
|
||||
) {
|
||||
return defaultColor;
|
||||
if (colorStyleProperty.options.type === COLOR_MAP_TYPE.CATEGORICAL) {
|
||||
if (colorStyleProperty.options.useCustomColorPalette) {
|
||||
return colorStyleProperty.options.customColorPalette &&
|
||||
colorStyleProperty.options.customColorPalette.length
|
||||
? colorStyleProperty.options.customColorPalette[0].colorCategory
|
||||
: defaultColor;
|
||||
}
|
||||
// favor the lowest color in even arrays
|
||||
const middleIndex = Math.floor((colorStyleProperty.options.customColorRamp.length - 1) / 2);
|
||||
return colorStyleProperty.options.customColorRamp[middleIndex].color;
|
||||
}
|
||||
|
||||
return getColorRampCenterColor(colorStyleProperty.options.color);
|
||||
if (!colorStyleProperty.options.colorCategory) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const palette = getColorPalette(colorStyleProperty.options.colorCategory);
|
||||
return palette[0];
|
||||
} else {
|
||||
// return middle of gradient for dynamic style property
|
||||
if (colorStyleProperty.options.useCustomColorRamp) {
|
||||
if (
|
||||
!colorStyleProperty.options.customColorRamp ||
|
||||
!colorStyleProperty.options.customColorRamp.length
|
||||
) {
|
||||
return defaultColor;
|
||||
}
|
||||
// favor the lowest color in even arrays
|
||||
const middleIndex = Math.floor((colorStyleProperty.options.customColorRamp.length - 1) / 2);
|
||||
return colorStyleProperty.options.customColorRamp[middleIndex].color;
|
||||
}
|
||||
|
||||
if (!colorStyleProperty.options.color) {
|
||||
return null;
|
||||
}
|
||||
return getColorRampCenterColor(colorStyleProperty.options.color);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ function getIsEnableToggleLabel(styleName) {
|
|||
}
|
||||
}
|
||||
|
||||
export class FieldMetaOptionsPopover extends Component {
|
||||
export class OrdinalFieldMetaOptionsPopover extends Component {
|
||||
state = {
|
||||
isPopoverOpen: false,
|
||||
};
|
|
@ -5,7 +5,6 @@
|
|||
*/
|
||||
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import { FieldMetaOptionsPopover } from './field_meta_options_popover';
|
||||
import { getVectorStyleLabel } from './get_vector_style_label';
|
||||
import { EuiFormRow, EuiSelect } from '@elastic/eui';
|
||||
import { VectorStyle } from '../vector_style';
|
||||
|
@ -80,12 +79,9 @@ export class StylePropEditor extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const fieldMetaOptionsPopover = this.props.styleProperty.isDynamic() ? (
|
||||
<FieldMetaOptionsPopover
|
||||
styleProperty={this.props.styleProperty}
|
||||
onChange={this._onFieldMetaOptionsChange}
|
||||
/>
|
||||
) : null;
|
||||
const fieldMetaOptionsPopover = this.props.styleProperty.renderFieldMetaPopover(
|
||||
this._onFieldMetaOptionsChange
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
|
|
|
@ -32,6 +32,7 @@ export class VectorStyleEditor extends Component {
|
|||
state = {
|
||||
dateFields: [],
|
||||
numberFields: [],
|
||||
categoricalFields: [],
|
||||
fields: [],
|
||||
defaultDynamicProperties: getDefaultDynamicProperties(),
|
||||
defaultStaticProperties: getDefaultStaticProperties(),
|
||||
|
@ -77,6 +78,13 @@ export class VectorStyleEditor extends Component {
|
|||
this.setState({ numberFields: numberFieldsArray });
|
||||
}
|
||||
|
||||
const categoricalFields = await this.props.layer.getCategoricalFields();
|
||||
const categoricalFieldMeta = categoricalFields.map(getFieldMeta);
|
||||
const categoricalFieldsArray = await Promise.all(categoricalFieldMeta);
|
||||
if (this._isMounted && !_.isEqual(categoricalFieldsArray, this.state.categoricalFields)) {
|
||||
this.setState({ categoricalFields: categoricalFieldsArray });
|
||||
}
|
||||
|
||||
const fields = await this.props.layer.getFields();
|
||||
const fieldPromises = fields.map(getFieldMeta);
|
||||
const fieldsArray = await Promise.all(fieldPromises);
|
||||
|
@ -110,6 +118,10 @@ export class VectorStyleEditor extends Component {
|
|||
return [...this.state.dateFields, ...this.state.numberFields];
|
||||
}
|
||||
|
||||
_getOrdinalAndCategoricalFields() {
|
||||
return [...this.state.dateFields, ...this.state.numberFields, ...this.state.categoricalFields];
|
||||
}
|
||||
|
||||
_handleSelectedFeatureChange = selectedFeature => {
|
||||
this.setState({ selectedFeature });
|
||||
};
|
||||
|
@ -141,7 +153,7 @@ export class VectorStyleEditor extends Component {
|
|||
onStaticStyleChange={this._onStaticStyleChange}
|
||||
onDynamicStyleChange={this._onDynamicStyleChange}
|
||||
styleProperty={this.props.styleProperties[VECTOR_STYLES.FILL_COLOR]}
|
||||
fields={this._getOrdinalFields()}
|
||||
fields={this._getOrdinalAndCategoricalFields()}
|
||||
defaultStaticStyleOptions={
|
||||
this.state.defaultStaticProperties[VECTOR_STYLES.FILL_COLOR].options
|
||||
}
|
||||
|
@ -159,7 +171,7 @@ export class VectorStyleEditor extends Component {
|
|||
onStaticStyleChange={this._onStaticStyleChange}
|
||||
onDynamicStyleChange={this._onDynamicStyleChange}
|
||||
styleProperty={this.props.styleProperties[VECTOR_STYLES.LINE_COLOR]}
|
||||
fields={this._getOrdinalFields()}
|
||||
fields={this._getOrdinalAndCategoricalFields()}
|
||||
defaultStaticStyleOptions={
|
||||
this.state.defaultStaticProperties[VECTOR_STYLES.LINE_COLOR].options
|
||||
}
|
||||
|
@ -226,7 +238,7 @@ export class VectorStyleEditor extends Component {
|
|||
onStaticStyleChange={this._onStaticStyleChange}
|
||||
onDynamicStyleChange={this._onDynamicStyleChange}
|
||||
styleProperty={this.props.styleProperties[VECTOR_STYLES.LABEL_COLOR]}
|
||||
fields={this._getOrdinalFields()}
|
||||
fields={this._getOrdinalAndCategoricalFields()}
|
||||
defaultStaticStyleOptions={
|
||||
this.state.defaultStaticProperties[VECTOR_STYLES.LABEL_COLOR].options
|
||||
}
|
||||
|
@ -255,7 +267,7 @@ export class VectorStyleEditor extends Component {
|
|||
onStaticStyleChange={this._onStaticStyleChange}
|
||||
onDynamicStyleChange={this._onDynamicStyleChange}
|
||||
styleProperty={this.props.styleProperties[VECTOR_STYLES.LABEL_BORDER_COLOR]}
|
||||
fields={this._getOrdinalFields()}
|
||||
fields={this._getOrdinalAndCategoricalFields()}
|
||||
defaultStaticStyleOptions={
|
||||
this.state.defaultStaticProperties[VECTOR_STYLES.LABEL_BORDER_COLOR].options
|
||||
}
|
||||
|
|
|
@ -1,8 +1,128 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Should render categorical legend 1`] = `""`;
|
||||
exports[`Should render categorical legend with breaks from custom 1`] = `""`;
|
||||
|
||||
exports[`Should render ranged legend 1`] = `
|
||||
exports[`Should render categorical legend with breaks from default 1`] = `
|
||||
<div>
|
||||
<EuiSpacer
|
||||
size="s"
|
||||
/>
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="none"
|
||||
>
|
||||
<EuiFlexItem
|
||||
key="0"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
gutterSize="none"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiText
|
||||
size="xs"
|
||||
>
|
||||
US_format
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<VectorIcon
|
||||
fillColor="none"
|
||||
isLinesOnly={false}
|
||||
isPointsOnly={true}
|
||||
strokeColor="#5BBAA0"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
key="1"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
gutterSize="none"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiText
|
||||
size="xs"
|
||||
>
|
||||
CN_format
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<VectorIcon
|
||||
fillColor="none"
|
||||
isLinesOnly={false}
|
||||
isPointsOnly={true}
|
||||
strokeColor="#6092C0"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
key="2"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
gutterSize="none"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiText
|
||||
size="xs"
|
||||
>
|
||||
<EuiTextColor
|
||||
color="secondary"
|
||||
>
|
||||
Other
|
||||
</EuiTextColor>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<VectorIcon
|
||||
fillColor="none"
|
||||
isLinesOnly={false}
|
||||
isPointsOnly={true}
|
||||
strokeColor="#D36086"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGroup
|
||||
gutterSize="xs"
|
||||
justifyContent="spaceAround"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Should render ordinal legend 1`] = `
|
||||
<RangedStyleLegendRow
|
||||
fieldLabel=""
|
||||
header={
|
||||
|
@ -15,3 +135,95 @@ exports[`Should render ranged legend 1`] = `
|
|||
propertyLabel="Border color"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`Should render ordinal legend with breaks 1`] = `
|
||||
<div>
|
||||
<EuiSpacer
|
||||
size="s"
|
||||
/>
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="none"
|
||||
>
|
||||
<EuiFlexItem
|
||||
key="0"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
gutterSize="none"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiText
|
||||
size="xs"
|
||||
>
|
||||
0_format
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<VectorIcon
|
||||
fillColor="none"
|
||||
isLinesOnly={false}
|
||||
isPointsOnly={true}
|
||||
strokeColor="#FF0000"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
key="1"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
gutterSize="none"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiText
|
||||
size="xs"
|
||||
>
|
||||
10_format
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<VectorIcon
|
||||
fillColor="none"
|
||||
isLinesOnly={false}
|
||||
isPointsOnly={true}
|
||||
strokeColor="#00FF00"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGroup
|
||||
gutterSize="xs"
|
||||
justifyContent="spaceAround"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
@ -7,12 +7,26 @@
|
|||
import { DynamicStyleProperty } from './dynamic_style_property';
|
||||
import _ from 'lodash';
|
||||
import { getComputedFieldName } from '../style_util';
|
||||
import { getColorRampStops } from '../../color_utils';
|
||||
import { getOrdinalColorRampStops, getColorPalette } from '../../color_utils';
|
||||
import { ColorGradient } from '../../components/color_gradient';
|
||||
import React from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, EuiToolTip } from '@elastic/eui';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiToolTip,
|
||||
EuiTextColor,
|
||||
} from '@elastic/eui';
|
||||
import { VectorIcon } from '../components/legend/vector_icon';
|
||||
import { VECTOR_STYLES } from '../vector_style_defaults';
|
||||
import { COLOR_MAP_TYPE } from '../../../../../common/constants';
|
||||
import {
|
||||
isCategoricalStopsInvalid,
|
||||
getOtherCategoryLabel,
|
||||
} from '../components/color/color_stops_utils';
|
||||
|
||||
const EMPTY_STOPS = { stops: [], defaultColor: null };
|
||||
|
||||
export class DynamicColorProperty extends DynamicStyleProperty {
|
||||
syncCircleColorWithMb(mbLayerId, mbMap, alpha) {
|
||||
|
@ -60,7 +74,17 @@ export class DynamicColorProperty extends DynamicStyleProperty {
|
|||
mbMap.setPaintProperty(mbLayerId, 'text-halo-color', color);
|
||||
}
|
||||
|
||||
isCustomColorRamp() {
|
||||
isOrdinal() {
|
||||
return (
|
||||
typeof this._options.type === 'undefined' || this._options.type === COLOR_MAP_TYPE.ORDINAL
|
||||
);
|
||||
}
|
||||
|
||||
isCategorical() {
|
||||
return this._options.type === COLOR_MAP_TYPE.CATEGORICAL;
|
||||
}
|
||||
|
||||
isCustomOrdinalColorRamp() {
|
||||
return this._options.useCustomColorRamp;
|
||||
}
|
||||
|
||||
|
@ -68,16 +92,16 @@ export class DynamicColorProperty extends DynamicStyleProperty {
|
|||
return true;
|
||||
}
|
||||
|
||||
isScaled() {
|
||||
return !this.isCustomColorRamp();
|
||||
isOrdinalScaled() {
|
||||
return this.isOrdinal() && !this.isCustomOrdinalColorRamp();
|
||||
}
|
||||
|
||||
isRanged() {
|
||||
return !this.isCustomColorRamp();
|
||||
isOrdinalRanged() {
|
||||
return this.isOrdinal() && !this.isCustomOrdinalColorRamp();
|
||||
}
|
||||
|
||||
hasBreaks() {
|
||||
return this.isCustomColorRamp();
|
||||
hasOrdinalBreaks() {
|
||||
return (this.isOrdinal() && this.isCustomOrdinalColorRamp()) || this.isCategorical();
|
||||
}
|
||||
|
||||
_getMbColor() {
|
||||
|
@ -87,6 +111,15 @@ export class DynamicColorProperty extends DynamicStyleProperty {
|
|||
return null;
|
||||
}
|
||||
|
||||
const targetName = getComputedFieldName(this._styleName, this._options.field.name);
|
||||
if (this.isCategorical()) {
|
||||
return this._getMbDataDrivenCategoricalColor({ targetName });
|
||||
} else {
|
||||
return this._getMbDataDrivenOrdinalColor({ targetName });
|
||||
}
|
||||
}
|
||||
|
||||
_getMbDataDrivenOrdinalColor({ targetName }) {
|
||||
if (
|
||||
this._options.useCustomColorRamp &&
|
||||
(!this._options.customColorRamp || !this._options.customColorRamp.length)
|
||||
|
@ -94,15 +127,12 @@ export class DynamicColorProperty extends DynamicStyleProperty {
|
|||
return null;
|
||||
}
|
||||
|
||||
return this._getMBDataDrivenColor({
|
||||
targetName: getComputedFieldName(this._styleName, this._options.field.name),
|
||||
colorStops: this._getMBColorStops(),
|
||||
isSteps: this._options.useCustomColorRamp,
|
||||
});
|
||||
}
|
||||
const colorStops = this._getMbOrdinalColorStops();
|
||||
if (!colorStops) {
|
||||
return null;
|
||||
}
|
||||
|
||||
_getMBDataDrivenColor({ targetName, colorStops, isSteps }) {
|
||||
if (isSteps) {
|
||||
if (this._options.useCustomColorRamp) {
|
||||
const firstStopValue = colorStops[0];
|
||||
const lessThenFirstStopValue = firstStopValue - 1;
|
||||
return [
|
||||
|
@ -112,7 +142,6 @@ export class DynamicColorProperty extends DynamicStyleProperty {
|
|||
...colorStops,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'interpolate',
|
||||
['linear'],
|
||||
|
@ -123,14 +152,92 @@ export class DynamicColorProperty extends DynamicStyleProperty {
|
|||
];
|
||||
}
|
||||
|
||||
_getMBColorStops() {
|
||||
_getColorPaletteStops() {
|
||||
if (this._options.useCustomColorPalette && this._options.customColorPalette) {
|
||||
if (isCategoricalStopsInvalid(this._options.customColorPalette)) {
|
||||
return EMPTY_STOPS;
|
||||
}
|
||||
|
||||
const stops = [];
|
||||
for (let i = 1; i < this._options.customColorPalette.length; i++) {
|
||||
const config = this._options.customColorPalette[i];
|
||||
stops.push({
|
||||
stop: config.stop,
|
||||
color: config.color,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
defaultColor: this._options.customColorPalette[0].color,
|
||||
stops,
|
||||
};
|
||||
}
|
||||
|
||||
const fieldMeta = this.getFieldMeta();
|
||||
if (!fieldMeta || !fieldMeta.categories) {
|
||||
return EMPTY_STOPS;
|
||||
}
|
||||
|
||||
const colors = getColorPalette(this._options.colorCategory);
|
||||
if (!colors) {
|
||||
return EMPTY_STOPS;
|
||||
}
|
||||
|
||||
const maxLength = Math.min(colors.length, fieldMeta.categories.length + 1);
|
||||
const stops = [];
|
||||
|
||||
for (let i = 0; i < maxLength - 1; i++) {
|
||||
stops.push({
|
||||
stop: fieldMeta.categories[i].key,
|
||||
color: colors[i],
|
||||
});
|
||||
}
|
||||
return {
|
||||
stops,
|
||||
defaultColor: colors[maxLength - 1],
|
||||
};
|
||||
}
|
||||
|
||||
_getMbDataDrivenCategoricalColor() {
|
||||
if (
|
||||
this._options.useCustomColorPalette &&
|
||||
(!this._options.customColorPalette || !this._options.customColorPalette.length)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { stops, defaultColor } = this._getColorPaletteStops();
|
||||
if (stops.length < 1) {
|
||||
//occurs when no data
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!defaultColor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const mbStops = [];
|
||||
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', ['get', this._options.field.name], ...mbStops];
|
||||
}
|
||||
|
||||
_getMbOrdinalColorStops() {
|
||||
if (this._options.useCustomColorRamp) {
|
||||
return this._options.customColorRamp.reduce((accumulatedStops, nextStop) => {
|
||||
return [...accumulatedStops, nextStop.stop, nextStop.color];
|
||||
}, []);
|
||||
} else {
|
||||
return getOrdinalColorRampStops(this._options.color);
|
||||
}
|
||||
|
||||
return getColorRampStops(this._options.color);
|
||||
}
|
||||
|
||||
renderRangeLegendHeader() {
|
||||
|
@ -163,18 +270,47 @@ export class DynamicColorProperty extends DynamicStyleProperty {
|
|||
);
|
||||
}
|
||||
|
||||
_getColorRampStops() {
|
||||
return this._options.useCustomColorRamp && this._options.customColorRamp
|
||||
? this._options.customColorRamp
|
||||
: [];
|
||||
}
|
||||
|
||||
_getColorStops() {
|
||||
if (this.isOrdinal()) {
|
||||
return {
|
||||
stops: this._getColorRampStops(),
|
||||
defaultColor: null,
|
||||
};
|
||||
} else if (this.isCategorical()) {
|
||||
return this._getColorPaletteStops();
|
||||
} else {
|
||||
return EMPTY_STOPS;
|
||||
}
|
||||
}
|
||||
|
||||
_renderColorbreaks({ isLinesOnly, isPointsOnly, symbolId }) {
|
||||
if (!this._options.customColorRamp) {
|
||||
return null;
|
||||
const { stops, defaultColor } = this._getColorStops();
|
||||
const colorAndLabels = stops.map(config => {
|
||||
return {
|
||||
label: this.formatField(config.stop),
|
||||
color: config.color,
|
||||
};
|
||||
});
|
||||
|
||||
if (defaultColor) {
|
||||
colorAndLabels.push({
|
||||
label: <EuiTextColor color="secondary">{getOtherCategoryLabel()}</EuiTextColor>,
|
||||
color: defaultColor,
|
||||
});
|
||||
}
|
||||
|
||||
return this._options.customColorRamp.map((config, index) => {
|
||||
const value = this.formatField(config.stop);
|
||||
return colorAndLabels.map((config, index) => {
|
||||
return (
|
||||
<EuiFlexItem key={index}>
|
||||
<EuiFlexGroup direction={'row'} gutterSize={'none'}>
|
||||
<EuiFlexItem>
|
||||
<EuiText size={'xs'}>{value}</EuiText>
|
||||
<EuiText size={'xs'}>{config.label}</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
{this._renderStopIcon(config.color, isLinesOnly, isPointsOnly, symbolId)}
|
||||
|
|
|
@ -15,12 +15,12 @@ import { shallow } from 'enzyme';
|
|||
|
||||
import { VECTOR_STYLES } from '../vector_style_defaults';
|
||||
import { DynamicColorProperty } from './dynamic_color_property';
|
||||
import { COLOR_MAP_TYPE } from '../../../../../common/constants';
|
||||
|
||||
const mockField = {
|
||||
async getLabel() {
|
||||
return 'foobar_label';
|
||||
},
|
||||
|
||||
getName() {
|
||||
return 'foobar';
|
||||
},
|
||||
|
@ -29,33 +29,61 @@ const mockField = {
|
|||
},
|
||||
};
|
||||
|
||||
test('Should render ranged legend', () => {
|
||||
const colorStyle = new DynamicColorProperty(
|
||||
{
|
||||
color: 'Blues',
|
||||
},
|
||||
const getOrdinalFieldMeta = () => {
|
||||
return { min: 0, max: 100 };
|
||||
};
|
||||
|
||||
const getCategoricalFieldMeta = () => {
|
||||
return {
|
||||
categories: [
|
||||
{
|
||||
key: 'US',
|
||||
count: 10,
|
||||
},
|
||||
{
|
||||
key: 'CN',
|
||||
count: 8,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
const makeProperty = (options, getFieldMeta) => {
|
||||
return new DynamicColorProperty(
|
||||
options,
|
||||
VECTOR_STYLES.LINE_COLOR,
|
||||
mockField,
|
||||
() => {
|
||||
return { min: 0, max: 100 };
|
||||
},
|
||||
getFieldMeta,
|
||||
() => {
|
||||
return x => x + '_format';
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const defaultLegendParams = {
|
||||
isPointsOnly: true,
|
||||
isLinesOnly: false,
|
||||
};
|
||||
|
||||
test('Should render ordinal legend', async () => {
|
||||
const colorStyle = makeProperty(
|
||||
{
|
||||
color: 'Blues',
|
||||
type: undefined,
|
||||
},
|
||||
getOrdinalFieldMeta
|
||||
);
|
||||
|
||||
const legendRow = colorStyle.renderLegendDetailRow(defaultLegendParams);
|
||||
|
||||
const legendRow = colorStyle.renderLegendDetailRow({
|
||||
isPointsOnly: true,
|
||||
isLinesOnly: false,
|
||||
});
|
||||
const component = shallow(legendRow);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('Should render categorical legend', () => {
|
||||
const colorStyle = new DynamicColorProperty(
|
||||
test('Should render ordinal legend with breaks', async () => {
|
||||
const colorStyle = makeProperty(
|
||||
{
|
||||
type: COLOR_MAP_TYPE.ORDINAL,
|
||||
useCustomColorRamp: true,
|
||||
customColorRamp: [
|
||||
{
|
||||
|
@ -68,21 +96,128 @@ test('Should render categorical legend', () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
VECTOR_STYLES.LINE_COLOR,
|
||||
mockField,
|
||||
() => {
|
||||
return { min: 0, max: 100 };
|
||||
},
|
||||
() => {
|
||||
return x => x + '_format';
|
||||
}
|
||||
getOrdinalFieldMeta
|
||||
);
|
||||
|
||||
const legendRow = colorStyle.renderLegendDetailRow({
|
||||
isPointsOnly: true,
|
||||
isLinesOnly: false,
|
||||
});
|
||||
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 default', async () => {
|
||||
const colorStyle = makeProperty(
|
||||
{
|
||||
type: COLOR_MAP_TYPE.CATEGORICAL,
|
||||
useCustomColorPalette: false,
|
||||
colorCategory: 'palette_0',
|
||||
},
|
||||
getCategoricalFieldMeta
|
||||
);
|
||||
|
||||
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',
|
||||
},
|
||||
],
|
||||
},
|
||||
getCategoricalFieldMeta
|
||||
);
|
||||
|
||||
const legendRow = colorStyle.renderLegendDetailRow(defaultLegendParams);
|
||||
|
||||
const component = shallow(legendRow);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
function makeFeatures(foobarPropValues) {
|
||||
return foobarPropValues.map(value => {
|
||||
return {
|
||||
type: 'Feature',
|
||||
properties: {
|
||||
foobar: value,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
test('Should pluck the categorical style-meta', async () => {
|
||||
const colorStyle = makeProperty({
|
||||
type: COLOR_MAP_TYPE.CATEGORICAL,
|
||||
colorCategory: 'palette_0',
|
||||
getCategoricalFieldMeta,
|
||||
});
|
||||
|
||||
const features = makeFeatures(['CN', 'CN', 'US', 'CN', 'US', 'IN']);
|
||||
const meta = colorStyle.pluckStyleMetaFromFeatures(features);
|
||||
|
||||
expect(meta).toEqual({
|
||||
categories: [
|
||||
{ key: 'CN', count: 3 },
|
||||
{ key: 'US', count: 2 },
|
||||
{ key: 'IN', count: 1 },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('Should pluck the categorical style-meta from fieldmeta', async () => {
|
||||
const colorStyle = makeProperty({
|
||||
type: COLOR_MAP_TYPE.CATEGORICAL,
|
||||
colorCategory: 'palette_0',
|
||||
getCategoricalFieldMeta,
|
||||
});
|
||||
|
||||
const meta = colorStyle.pluckStyleMetaFromFieldMetaData({
|
||||
foobar: {
|
||||
buckets: [
|
||||
{
|
||||
key: 'CN',
|
||||
doc_count: 3,
|
||||
},
|
||||
{ key: 'US', doc_count: 2 },
|
||||
{ key: 'IN', doc_count: 1 },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(meta).toEqual({
|
||||
categories: [
|
||||
{ key: 'CN', count: 3 },
|
||||
{ key: 'US', count: 2 },
|
||||
{ key: 'IN', count: 1 },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
|
|
@ -26,7 +26,7 @@ export class DynamicOrientationProperty extends DynamicStyleProperty {
|
|||
return false;
|
||||
}
|
||||
|
||||
isScaled() {
|
||||
isOrdinalScaled() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,11 +7,12 @@
|
|||
import _ from 'lodash';
|
||||
import { AbstractStyleProperty } from './style_property';
|
||||
import { DEFAULT_SIGMA } from '../vector_style_defaults';
|
||||
import { STYLE_TYPE } from '../../../../../common/constants';
|
||||
import { COLOR_PALETTE_MAX_SIZE, STYLE_TYPE } from '../../../../../common/constants';
|
||||
import { scaleValue, getComputedFieldName } from '../style_util';
|
||||
import React from 'react';
|
||||
import { OrdinalLegend } from './components/ordinal_legend';
|
||||
import { CategoricalLegend } from './components/categorical_legend';
|
||||
import { OrdinalFieldMetaOptionsPopover } from '../components/ordinal_field_meta_options_popover';
|
||||
|
||||
export class DynamicStyleProperty extends AbstractStyleProperty {
|
||||
static type = STYLE_TYPE.DYNAMIC;
|
||||
|
@ -46,11 +47,15 @@ export class DynamicStyleProperty extends AbstractStyleProperty {
|
|||
return true;
|
||||
}
|
||||
|
||||
hasBreaks() {
|
||||
isCategorical() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isRanged() {
|
||||
hasOrdinalBreaks() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isOrdinalRanged() {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -68,21 +73,33 @@ export class DynamicStyleProperty extends AbstractStyleProperty {
|
|||
}
|
||||
|
||||
supportsFieldMeta() {
|
||||
return this.isComplete() && this.isScaled() && this._field.supportsFieldMeta();
|
||||
if (this.isOrdinal()) {
|
||||
return this.isComplete() && this.isOrdinalScaled() && this._field.supportsFieldMeta();
|
||||
} else if (this.isCategorical()) {
|
||||
return this.isComplete() && this._field.supportsFieldMeta();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async getFieldMetaRequest() {
|
||||
const fieldMetaOptions = this.getFieldMetaOptions();
|
||||
return this._field.getFieldMetaRequest({
|
||||
sigma: _.get(fieldMetaOptions, 'sigma', DEFAULT_SIGMA),
|
||||
});
|
||||
if (this.isOrdinal()) {
|
||||
const fieldMetaOptions = this.getFieldMetaOptions();
|
||||
return this._field.getOrdinalFieldMetaRequest({
|
||||
sigma: _.get(fieldMetaOptions, 'sigma', DEFAULT_SIGMA),
|
||||
});
|
||||
} else if (this.isCategorical()) {
|
||||
return this._field.getCategoricalFieldMetaRequest();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
supportsFeatureState() {
|
||||
return true;
|
||||
}
|
||||
|
||||
isScaled() {
|
||||
isOrdinalScaled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -90,11 +107,7 @@ export class DynamicStyleProperty extends AbstractStyleProperty {
|
|||
return _.get(this.getOptions(), 'fieldMetaOptions', {});
|
||||
}
|
||||
|
||||
pluckStyleMetaFromFeatures(features) {
|
||||
if (!this.isOrdinal()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
_pluckOrdinalStyleMetaFromFeatures(features) {
|
||||
const name = this.getField().getName();
|
||||
let min = Infinity;
|
||||
let max = -Infinity;
|
||||
|
@ -116,11 +129,47 @@ export class DynamicStyleProperty extends AbstractStyleProperty {
|
|||
};
|
||||
}
|
||||
|
||||
pluckStyleMetaFromFieldMetaData(fieldMetaData) {
|
||||
if (!this.isOrdinal()) {
|
||||
return null;
|
||||
_pluckCategoricalStyleMetaFromFeatures(features) {
|
||||
const fieldName = this.getField().getName();
|
||||
const counts = new Map();
|
||||
for (let i = 0; i < features.length; i++) {
|
||||
const feature = features[i];
|
||||
const term = feature.properties[fieldName];
|
||||
//properties object may be sparse, so need to check if the field is effectively present
|
||||
if (typeof term !== undefined) {
|
||||
if (counts.has(term)) {
|
||||
counts.set(term, counts.get(term) + 1);
|
||||
} else {
|
||||
counts.set(term, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ordered = [];
|
||||
for (const [key, value] of counts) {
|
||||
ordered.push({ key, count: value });
|
||||
}
|
||||
|
||||
ordered.sort((a, b) => {
|
||||
return b.count - a.count;
|
||||
});
|
||||
const truncated = ordered.slice(0, COLOR_PALETTE_MAX_SIZE);
|
||||
return {
|
||||
categories: truncated,
|
||||
};
|
||||
}
|
||||
|
||||
pluckStyleMetaFromFeatures(features) {
|
||||
if (this.isOrdinal()) {
|
||||
return this._pluckOrdinalStyleMetaFromFeatures(features);
|
||||
} else if (this.isCategorical()) {
|
||||
return this._pluckCategoricalStyleMetaFromFeatures(features);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
_pluckOrdinalStyleMetaFromFieldMetaData(fieldMetaData) {
|
||||
const realFieldName = this._field.getESDocFieldName
|
||||
? this._field.getESDocFieldName()
|
||||
: this._field.getName();
|
||||
|
@ -143,6 +192,33 @@ export class DynamicStyleProperty extends AbstractStyleProperty {
|
|||
};
|
||||
}
|
||||
|
||||
_pluckCategoricalStyleMetaFromFieldMetaData(fieldMetaData) {
|
||||
const name = this.getField().getName();
|
||||
if (!fieldMetaData[name] || !fieldMetaData[name].buckets) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ordered = fieldMetaData[name].buckets.map(bucket => {
|
||||
return {
|
||||
key: bucket.key,
|
||||
count: bucket.doc_count,
|
||||
};
|
||||
});
|
||||
return {
|
||||
categories: ordered,
|
||||
};
|
||||
}
|
||||
|
||||
pluckStyleMetaFromFieldMetaData(fieldMetaData) {
|
||||
if (this.isOrdinal()) {
|
||||
return this._pluckOrdinalStyleMetaFromFieldMetaData(fieldMetaData);
|
||||
} else if (this.isCategorical()) {
|
||||
return this._pluckCategoricalStyleMetaFromFieldMetaData(fieldMetaData);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
formatField(value) {
|
||||
if (this.getField()) {
|
||||
const fieldName = this.getField().getName();
|
||||
|
@ -159,7 +235,7 @@ export class DynamicStyleProperty extends AbstractStyleProperty {
|
|||
}
|
||||
|
||||
const valueAsFloat = parseFloat(value);
|
||||
if (this.isScaled()) {
|
||||
if (this.isOrdinalScaled()) {
|
||||
return scaleValue(valueAsFloat, this.getFieldMeta());
|
||||
}
|
||||
if (isNaN(valueAsFloat)) {
|
||||
|
@ -188,12 +264,28 @@ export class DynamicStyleProperty extends AbstractStyleProperty {
|
|||
}
|
||||
|
||||
renderLegendDetailRow({ isPointsOnly, isLinesOnly, symbolId }) {
|
||||
if (this.isRanged()) {
|
||||
return this._renderRangeLegend();
|
||||
} else if (this.hasBreaks()) {
|
||||
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.isOrdinal() || !this.supportsFieldMeta()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<OrdinalFieldMetaOptionsPopover styleProperty={this} onChange={onFieldMetaOptionsChange} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ export class DynamicTextProperty extends DynamicStyleProperty {
|
|||
return false;
|
||||
}
|
||||
|
||||
isScaled() {
|
||||
isOrdinalScaled() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,6 +45,10 @@ export class AbstractStyleProperty {
|
|||
return null;
|
||||
}
|
||||
|
||||
renderFieldMetaPopover() {
|
||||
return null;
|
||||
}
|
||||
|
||||
getDisplayStyleName() {
|
||||
return getVectorStyleLabel(this.getStyleName());
|
||||
}
|
||||
|
|
|
@ -6,7 +6,12 @@
|
|||
|
||||
import { VectorStyle } from './vector_style';
|
||||
import { SYMBOLIZE_AS_CIRCLE, DEFAULT_ICON_SIZE } from './vector_constants';
|
||||
import { COLOR_GRADIENTS, DEFAULT_FILL_COLORS, DEFAULT_LINE_COLORS } from '../color_utils';
|
||||
import {
|
||||
COLOR_GRADIENTS,
|
||||
COLOR_PALETTES,
|
||||
DEFAULT_FILL_COLORS,
|
||||
DEFAULT_LINE_COLORS,
|
||||
} from '../color_utils';
|
||||
import chrome from 'ui/chrome';
|
||||
|
||||
const DEFAULT_ICON = 'airfield';
|
||||
|
@ -136,6 +141,7 @@ export function getDefaultDynamicProperties() {
|
|||
type: VectorStyle.STYLE_TYPE.DYNAMIC,
|
||||
options: {
|
||||
color: COLOR_GRADIENTS[0].value,
|
||||
colorCategory: COLOR_PALETTES[0].value,
|
||||
field: undefined,
|
||||
fieldMetaOptions: {
|
||||
isEnabled: true,
|
||||
|
@ -146,7 +152,7 @@ export function getDefaultDynamicProperties() {
|
|||
[VECTOR_STYLES.LINE_COLOR]: {
|
||||
type: VectorStyle.STYLE_TYPE.DYNAMIC,
|
||||
options: {
|
||||
color: COLOR_GRADIENTS[0].value,
|
||||
color: undefined,
|
||||
field: undefined,
|
||||
fieldMetaOptions: {
|
||||
isEnabled: true,
|
||||
|
@ -198,6 +204,7 @@ export function getDefaultDynamicProperties() {
|
|||
type: VectorStyle.STYLE_TYPE.DYNAMIC,
|
||||
options: {
|
||||
color: COLOR_GRADIENTS[0].value,
|
||||
colorCategory: COLOR_PALETTES[0].value,
|
||||
field: undefined,
|
||||
fieldMetaOptions: {
|
||||
isEnabled: true,
|
||||
|
@ -221,6 +228,7 @@ export function getDefaultDynamicProperties() {
|
|||
type: VectorStyle.STYLE_TYPE.DYNAMIC,
|
||||
options: {
|
||||
color: COLOR_GRADIENTS[0].value,
|
||||
colorCategory: COLOR_PALETTES[0].value,
|
||||
field: undefined,
|
||||
fieldMetaOptions: {
|
||||
isEnabled: true,
|
||||
|
|
|
@ -213,6 +213,10 @@ export class VectorLayer extends AbstractLayer {
|
|||
return [...(await this.getDateFields()), ...(await this.getNumberFields())];
|
||||
}
|
||||
|
||||
async getCategoricalFields() {
|
||||
return await this._source.getCategoricalFields();
|
||||
}
|
||||
|
||||
async getFields() {
|
||||
const sourceFields = await this._source.getFields();
|
||||
return [...sourceFields, ...this._getJoinFields()];
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue