Skip to content

Commit

Permalink
[Maps] custom color ramp (#41603)
Browse files Browse the repository at this point in the history
* [Maps] custom color ramp

* round value down to find center color

* do not update redux state with invalide color stops

* rename EuiColorStop to ColorStop

* remove untracked file

* fix jest tests

* review feedback

* use steps instead of interpolate

* add percy functional test to verify rendering of interpolate and step color expressions

* add padding to color stop row so add/remove icons do not overlap color select
  • Loading branch information
nreese authored Aug 15, 2019
1 parent df72f91 commit c633565
Show file tree
Hide file tree
Showing 16 changed files with 497 additions and 41 deletions.
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
@import './components/color_gradient';
@import './components/static_dynamic_style_row';
@import './components/vector/color/color_stops';
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ export function getColorRampStops(colorRampName, numberColors = GRADIENT_INTERVA

export const COLOR_GRADIENTS = Object.keys(vislibColorMaps).map(colorRampName => ({
value: colorRampName,
text: colorRampName,
inputDisplay: <ColorGradient colorRampName={colorRampName}/>
}));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ describe('COLOR_GRADIENTS', () => {
it('Should contain EuiSuperSelect options list of color ramps', () => {
expect(COLOR_GRADIENTS.length).toBe(6);
const colorGradientOption = COLOR_GRADIENTS[0];
expect(colorGradientOption.text).toBe('Blues');
expect(colorGradientOption.value).toBe('Blues');
});
});
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
.mapColorStop {
position: relative;
padding-right: $euiSizeXL + $euiSizeXS;

&:hover,
&:focus {
.mapColorStop__icons {
visibility: visible;
opacity: 1;
display: block;
animation: mapColorStopBecomeVisible $euiAnimSpeedFast $euiAnimSlightResistance;
}
}
}

.mapColorStop__icons {
flex-shrink: 0;
display: none;
position: absolute;
right: 0;
top: 50%;
margin-right: -$euiSizeS;
margin-top: -$euiSizeM;
}

@keyframes mapColorStopBecomeVisible {
0% { opacity: 0; }
100% { opacity: 1; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,95 @@
* you may not use this file except in compliance with the Elastic License.
*/

import React from 'react';
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';

import { EuiSuperSelect } from '@elastic/eui';
import { EuiSuperSelect, EuiSpacer } from '@elastic/eui';
import { COLOR_GRADIENTS } from '../../../color_utils';
import { FormattedMessage } from '@kbn/i18n/react';
import { ColorStops } from './color_stops';

export function ColorRampSelect({ color, onChange }) {
const onColorRampChange = (selectedColorRampString) => {
onChange({
color: selectedColorRampString
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,
});
};
return (
<EuiSuperSelect
options={COLOR_GRADIENTS}
onChange={onColorRampChange}
valueOfSelected={color}
hasDividers={true}
/>
);

render() {
let colorStopsInput;
if (this.props.useCustomColorRamp) {
colorStopsInput = (
<Fragment>
<EuiSpacer size="m" />
<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={this.props.useCustomColorRamp ? CUSTOM_COLOR_RAMP : this.props.color}
hasDividers={true}
/>
{colorStopsInput}
</Fragment>
);
}
}

ColorRampSelect.propTypes = {
color: PropTypes.string.isRequired,
color: PropTypes.string,
onChange: PropTypes.func.isRequired,
useCustomColorRamp: PropTypes.bool,
customColorRamp: PropTypes.array,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/*
* 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 _ from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';

import {
EuiColorPicker,
EuiFormRow,
EuiFieldNumber,
EuiFlexGroup,
EuiFlexItem,
EuiButtonIcon
} from '@elastic/eui';
import {
addRow,
removeRow,
isColorInvalid,
isStopInvalid,
isInvalid,
} from './color_stops_utils';

const DEFAULT_COLOR = '#FF0000';

export const ColorStops = ({
colorStops = [{ stop: 0, color: DEFAULT_COLOR }],
onChange,
}) => {
function getStopInput(stop, index) {
const onStopChange = e => {
const newColorStops = _.cloneDeep(colorStops);
const sanitizedValue = parseFloat(e.target.value);
newColorStops[index].stop = isNaN(sanitizedValue) ? '' : sanitizedValue;
onChange({
colorStops: newColorStops,
isInvalid: isInvalid(newColorStops),
});
};

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';
}

return {
stopError: error,
stopInput: (
<EuiFieldNumber
aria-label="Stop"
value={stop}
onChange={onStopChange}
/>
),
};
}

function getColorInput(color, index) {
const onColorChange = color => {
const newColorStops = _.cloneDeep(colorStops);
newColorStops[index].color = color;
onChange({
colorStops: newColorStops,
isInvalid: isInvalid(newColorStops),
});
};

return {
colorError: isColorInvalid(color)
? 'Color must provide a valid hex value'
: undefined,
colorInput: <EuiColorPicker onChange={onColorChange} color={color} />,
};
}

const rows = colorStops.map((colorStop, index) => {
const { stopError, stopInput } = getStopInput(colorStop.stop, index);
const { colorError, colorInput } = getColorInput(colorStop.color, index);
const errors = [];
if (stopError) {
errors.push(stopError);
}
if (colorError) {
errors.push(colorError);
}

const onRemove = () => {
const newColorStops = removeRow(colorStops, index);
onChange({
colorStops: newColorStops,
isInvalid: isInvalid(newColorStops),
});
};

const onAdd = () => {
const newColorStops = addRow(colorStops, index);

onChange({
colorStops: newColorStops,
isInvalid: isInvalid(newColorStops),
});
};

let deleteButton;
if (colorStops.length > 1) {
deleteButton = (
<EuiButtonIcon
iconType="trash"
color="danger"
aria-label="Delete"
title="Delete"
onClick={onRemove}
/>
);
}

return (
<EuiFormRow
key={index}
className="mapColorStop"
isInvalid={errors.length !== 0}
error={errors}
>
<div>
<EuiFlexGroup responsive={false} alignItems="center" gutterSize="s">
<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 <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,
};
Loading

0 comments on commit c633565

Please sign in to comment.