diff --git a/package.json b/package.json index e5d31ad77..0c2771119 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "prop-types": "^15.5.10", "raf": "^3.4.0", "react-color": "^2.13.8", - "react-colorscales": "0.5.7", + "react-colorscales": "0.5.8", "react-dropzone": "^4.2.9", "react-plotly.js": "^2.2.0", "react-rangeslider": "^2.2.0", diff --git a/src/components/containers/TraceAccordion.js b/src/components/containers/TraceAccordion.js index 42f5389eb..9b7f863fe 100644 --- a/src/components/containers/TraceAccordion.js +++ b/src/components/containers/TraceAccordion.js @@ -86,17 +86,17 @@ class TraceAccordion extends Component { - {_('All Traces')} - {_('Individual')} + {_('Individually')} + {_('By Type')} - - {groupedTraces ? groupedTraces : null} - {individualTraces ? individualTraces : null} + + {groupedTraces ? groupedTraces : null} + ); diff --git a/src/components/fields/Colorscale.js b/src/components/fields/Colorscale.js index f8f817b2a..8b2058b08 100644 --- a/src/components/fields/Colorscale.js +++ b/src/components/fields/Colorscale.js @@ -35,6 +35,7 @@ class Colorscale extends Component { ); @@ -44,7 +45,24 @@ class Colorscale extends Component { Colorscale.propTypes = { fullValue: PropTypes.any, updatePlot: PropTypes.func, + initialCategory: PropTypes.string, ...Field.propTypes, }; -export default connectToContainer(Colorscale); +export default connectToContainer(Colorscale, { + modifyPlotProps: (props, context, plotProps) => { + if ( + props.attr === 'marker.color' && + context.fullData + .filter(t => context.traceIndexes.includes(t.index)) + .every(t => t.marker && t.marker.color) && + (plotProps.fullValue && typeof plotProps.fullValue === 'string') + ) { + plotProps.fullValue = + context.fullData && + context.fullData + .filter(t => context.traceIndexes.includes(t.index)) + .map(t => [0, t.marker.color]); + } + }, +}); diff --git a/src/components/fields/DataSelector.js b/src/components/fields/DataSelector.js index d665a8c94..9ae425242 100644 --- a/src/components/fields/DataSelector.js +++ b/src/components/fields/DataSelector.js @@ -108,6 +108,7 @@ export class UnconnectedDataSelector extends Component { optionRenderer={this.context.dataSourceOptionRenderer} valueRenderer={this.context.dataSourceValueRenderer} clearable={true} + placeholder={this.props.placeholder} /> ); @@ -118,6 +119,7 @@ UnconnectedDataSelector.propTypes = { fullValue: PropTypes.any, updatePlot: PropTypes.func, container: PropTypes.object, + placeholder: PropTypes.string, ...Field.propTypes, }; diff --git a/src/components/fields/MarkerColor.js b/src/components/fields/MarkerColor.js index aa3a7c22c..f64369feb 100644 --- a/src/components/fields/MarkerColor.js +++ b/src/components/fields/MarkerColor.js @@ -7,9 +7,11 @@ import Color from './Color'; import Colorscale from './Colorscale'; import Numeric from './Numeric'; import Radio from './Radio'; +import Info from './Info'; import DataSelector from './DataSelector'; import VisibilitySelect from './VisibilitySelect'; import {MULTI_VALUED, COLORS} from 'lib/constants'; +import {getColorscale} from 'react-colorscales'; class UnconnectedMarkerColor extends Component { constructor(props, context) { @@ -36,28 +38,33 @@ class UnconnectedMarkerColor extends Component { constant: type === 'constant' ? props.fullValue : COLORS.mutedBlue, variable: type === 'variable' ? props.fullValue : null, }, + constantSelectedOption: + type === 'constant' && props.multiValued ? 'multiple' : 'single', }; this.setType = this.setType.bind(this); this.setValue = this.setValue.bind(this); this.setColorScale = this.setColorScale.bind(this); + this.setColors = this.setColors.bind(this); } setType(type) { - this.setState({type: type}); - this.props.updatePlot(this.state.value[type]); - if (type === 'constant') { - this.context.updateContainer({ - ['marker.colorsrc']: null, - ['marker.colorscale']: null, - }); - this.setState({colorscale: null}); - } else { - this.context.updateContainer({ - ['marker.color']: null, - ['marker.colorsrc']: null, - ['marker.colorscale']: [], - }); + if (this.state.type !== type) { + this.setState({type: type}); + this.props.updatePlot(this.state.value[type]); + if (type === 'constant') { + this.context.updateContainer({ + ['marker.colorsrc']: null, + ['marker.colorscale']: null, + }); + this.setState({colorscale: null}); + } else { + this.context.updateContainer({ + ['marker.color']: null, + ['marker.colorsrc']: null, + ['marker.colorscale']: [], + }); + } } } @@ -77,58 +84,158 @@ class UnconnectedMarkerColor extends Component { this.context.updateContainer({['marker.colorscale']: inputValue}); } + isMultiValued() { + return ( + this.props.multiValued || + (Array.isArray(this.props.fullValue) && + this.props.fullValue.includes(MULTI_VALUED)) || + (this.props.container.marker && + this.props.container.marker.colorscale === MULTI_VALUED) || + (this.props.container.marker && + this.props.container.marker.colorsrc === MULTI_VALUED) || + (this.props.container.marker && + this.props.container.marker.color && + Array.isArray(this.props.container.marker.color) && + this.props.container.marker.color.includes(MULTI_VALUED)) + ); + } + + setColors(colorscale) { + const numberOfTraces = this.context.traceIndexes.length; + const colors = colorscale.map(c => c[1]); + + let adjustedColors = getColorscale(colors, numberOfTraces); + if (adjustedColors.every(c => c === adjustedColors[0])) { + adjustedColors = colors; + } + + const updates = adjustedColors.map(color => ({ + ['marker.color']: color, + })); + + this.setState({ + colorscale: adjustedColors, + }); + + this.context.updateContainer(updates); + } + + renderConstantControls() { + const _ = this.context.localize; + const constantOptions = [ + {label: _('Single'), value: 'single'}, + {label: _('Multiple'), value: 'multiple'}, + ]; + + if (this.context.traceIndexes.length > 1) { + return ( +
+ + this.setState({constantSelectedOption: value}) + } + /> + + {this.state.constantSelectedOption === 'single' + ? _('All traces will be colored in the the same color.') + : _( + 'Each trace will be colored according to the selected colorscale.' + )} + + {this.state.constantSelectedOption === 'single' ? ( + + ) : ( + + )} +
+ ); + } + + return ( + + ); + } + + renderVariableControls() { + const _ = this.context.localize; + const multiValued = + (this.props.container && + this.props.container.marker && + (this.props.container.marker.colorscale && + this.props.container.marker.colorscale === MULTI_VALUED)) || + (this.props.container.marker.colorsrc && + this.props.container.marker.colorsrc === MULTI_VALUED); + return ( + + + {this.props.container.marker && + this.props.container.marker.colorscale === MULTI_VALUED ? null : ( + + )} + + ); + } + render() { - const {attr, fullValue, container} = this.props; + const {attr} = this.props; const {localize: _} = this.context; - const {type, value, colorscale} = this.state; + const {type} = this.state; const options = [ {label: _('Constant'), value: 'constant'}, {label: _('Variable'), value: 'variable'}, ]; - const multiValued = - this.props.multiValued || - (Array.isArray(fullValue) && fullValue.includes(MULTI_VALUED)) || - (container.marker && container.marker.colorscale === MULTI_VALUED) || - (container.marker && container.marker.colorsrc === MULTI_VALUED) || - (container.marker && - container.marker.color && - Array.isArray(container.marker.color) && - container.marker.color.includes(MULTI_VALUED)); return ( - - - {!type ? null : type === 'constant' ? ( - + + - ) : container.marker && - container.marker.colorsrc === MULTI_VALUED ? null : ( - - - {container.marker && - container.marker.colorscale === MULTI_VALUED ? null : ( - - )} - - )} + + {!type ? null : ( + + {type === 'constant' + ? _('All points in a trace are colored in the same color.') + : _('Each point in a trace is colored according to data.')} + + )} + + + {!type + ? null + : type === 'constant' + ? this.renderConstantControls() + : this.renderVariableControls()} - {type === 'constant' ? ( - '' - ) : ( + {type === 'constant' ? null : ( ( /> - + 1) { @@ -108,13 +109,25 @@ export default function connectTraceToPlot(WrappedComponent) { updateTrace(update) { if (this.context.onUpdate) { - this.context.onUpdate({ - type: EDITOR_ACTIONS.UPDATE_TRACES, - payload: { - update, - traceIndexes: this.props.traceIndexes, - }, - }); + if (Array.isArray(update)) { + update.forEach((u, i) => { + this.context.onUpdate({ + type: EDITOR_ACTIONS.UPDATE_TRACES, + payload: { + update: u, + traceIndexes: [this.props.traceIndexes[i]], + }, + }); + }); + } else { + this.context.onUpdate({ + type: EDITOR_ACTIONS.UPDATE_TRACES, + payload: { + update, + traceIndexes: this.props.traceIndexes, + }, + }); + } } } @@ -156,6 +169,7 @@ export default function connectTraceToPlot(WrappedComponent) { defaultContainer: PropTypes.object, container: PropTypes.object, fullContainer: PropTypes.object, + traceIndexes: PropTypes.array, }; const {plotly_editor_traits} = WrappedComponent; diff --git a/src/styles/components/fields/_main.scss b/src/styles/components/fields/_main.scss index 3c7187a9b..2abaf1e4c 100644 --- a/src/styles/components/fields/_main.scss +++ b/src/styles/components/fields/_main.scss @@ -1,2 +1,3 @@ @import 'field'; @import 'symbolselector'; +@import 'markercolor'; diff --git a/src/styles/components/fields/_markercolor.scss b/src/styles/components/fields/_markercolor.scss new file mode 100644 index 000000000..0ab02f03f --- /dev/null +++ b/src/styles/components/fields/_markercolor.scss @@ -0,0 +1,3 @@ +.markercolor-constantcontrols__container { + margin-top: var(--spacing-quarter-unit); +} diff --git a/src/styles/components/widgets/_colorpicker.scss b/src/styles/components/widgets/_colorpicker.scss index 362b7717e..522168bfa 100644 --- a/src/styles/components/widgets/_colorpicker.scss +++ b/src/styles/components/widgets/_colorpicker.scss @@ -22,7 +22,7 @@ $slider-picker-height: 10px; padding: var(--spacing-eighth-unit); &__popover { - left: -90px; + left: -60px; position: absolute; top: 100%; margin: var(--spacing-quarter-unit) 0 var(--spacing-base-unit); @@ -71,13 +71,13 @@ $slider-picker-height: 10px; } &__custom-input { padding: var(--spacing-quarter-unit) var(--spacing-half-unit); - input{ + input { border: var(--border-default) !important; box-shadow: none !important; background-color: var(--color-background-inputs); color: var(--color-text-dark); } - input + span{ + input + span { color: var(--color-text) !important; } } @@ -117,9 +117,9 @@ $slider-picker-height: 10px; &__preset-colors { margin: 0 var(--spacing-half-unit); - & > div{ + & > div { border-top: var(--border-light) !important; // inline style override - padding:var(--spacing-half-unit) !important; + padding: var(--spacing-half-unit) !important; padding-bottom: var(--spacing-quarter-unit) !important; } } diff --git a/src/styles/components/widgets/_colorscalepicker.scss b/src/styles/components/widgets/_colorscalepicker.scss index a6ac60709..f931114cc 100644 --- a/src/styles/components/widgets/_colorscalepicker.scss +++ b/src/styles/components/widgets/_colorscalepicker.scss @@ -24,5 +24,5 @@ } .customPickerContainer { - margin-top: 10px; + margin-top: var(--spacing-quarter-unit); }