diff --git a/src/components/containers/TraceAccordion.js b/src/components/containers/TraceAccordion.js index 5e4d1f521..8dd5a8a1a 100644 --- a/src/components/containers/TraceAccordion.js +++ b/src/components/containers/TraceAccordion.js @@ -11,7 +11,7 @@ const TraceFold = connectTraceToPlot(Fold); class TraceAccordion extends Component { render() { - const {data = [], fullData = []} = this.context; + const {data = []} = this.context; const { canAdd, canGroup, @@ -52,31 +52,30 @@ class TraceAccordion extends Component { ); } - if (canGroup && data.length > 1) { - const tracesByGroup = data.reduce((allTraces, next, index) => { - const traceType = plotlyTraceToCustomTrace( - fullData.filter(trace => trace.index === index)[0] + const tracesByGroup = data.reduce((allTraces, nextTrace, index) => { + const traceType = plotlyTraceToCustomTrace(nextTrace); + if (!allTraces[traceType]) { + allTraces[traceType] = []; + } + allTraces[traceType].push(index); + return allTraces; + }, {}); + + const groupedTraces = Object.keys(tracesByGroup) + .filter(traceType => !['ohlc', 'candlestick'].includes(traceType)) + .map((traceType, index) => { + return ( + + {this.props.children} + ); - if (!allTraces[traceType]) { - allTraces[traceType] = []; - } - allTraces[traceType].push(index); - return allTraces; - }, {}); + }); - const groupedTraces = Object.keys(tracesByGroup).map( - (traceType, index) => { - return ( - - {this.props.children} - - ); - } - ); + if (canGroup && data.length > 1 && groupedTraces.length > 0) { return ( diff --git a/src/components/containers/derived.js b/src/components/containers/derived.js index cd9e9c06a..6c94b1216 100644 --- a/src/components/containers/derived.js +++ b/src/components/containers/derived.js @@ -18,8 +18,9 @@ const TraceTypeSection = (props, context) => { const {fullContainer} = context; if ( fullContainer && - fullContainer._fullInput && - props.traceTypes.includes(fullContainer._fullInput.type) + ((fullContainer._fullInput && + props.traceTypes.includes(fullContainer._fullInput.type)) || + props.traceTypes.includes(fullContainer.type)) ) { return
; } diff --git a/src/components/fields/SymbolSelector.js b/src/components/fields/SymbolSelector.js index efd438645..c216184b6 100644 --- a/src/components/fields/SymbolSelector.js +++ b/src/components/fields/SymbolSelector.js @@ -4,6 +4,7 @@ import React, {Component} from 'react'; import SymbolSelectorWidget from '../widgets/SymbolSelector'; import nestedProperty from 'plotly.js/src/lib/nested_property'; import {connectToContainer, tooLight} from 'lib'; +import {MULTI_VALUED} from '../../lib/constants'; // TODO compute these from plotly.js const SYMBOLS = [ @@ -353,27 +354,38 @@ const SYMBOLS = [ ]; class SymbolSelector extends Component { - constructor(props) { - super(props); - this.setLocals(props); + constructor(props, context) { + super(props, context); + this.setLocals(props, context); } - componentWillReceiveProps(nextProps) { - this.setLocals(nextProps); + componentWillReceiveProps(nextProps, nextContext) { + this.setLocals(nextProps, nextContext); } - setLocals(props) { + setLocals(props, context) { const {fullContainer} = props; + const {defaultContainer} = context; this.markerColor = nestedProperty(fullContainer, 'marker.color').get(); this.borderWidth = nestedProperty(fullContainer, 'marker.line.width').get(); + if (this.markerColor === MULTI_VALUED) { + this.markerColor = nestedProperty(defaultContainer, 'marker.color').get(); + } + this.borderColor = this.markerColor; if (this.borderWidth) { this.borderColor = nestedProperty( fullContainer, 'marker.line.color' ).get(); + if (this.borderColor === MULTI_VALUED) { + this.borderColor = nestedProperty( + defaultContainer, + 'marker.line.color' + ).get(); + } } if (this.props.is3D) { @@ -403,11 +415,14 @@ class SymbolSelector extends Component { } SymbolSelector.propTypes = { - defaultValue: PropTypes.number, + defaultValue: PropTypes.string, fullValue: PropTypes.any, updatePlot: PropTypes.func, ...Field.propTypes, }; +SymbolSelector.contextTypes = { + defaultContainer: PropTypes.object, +}; SymbolSelector.defaultProps = { showArrows: true, diff --git a/src/components/fields/TextEditor.js b/src/components/fields/TextEditor.js index 7bf39ee93..36a042e89 100644 --- a/src/components/fields/TextEditor.js +++ b/src/components/fields/TextEditor.js @@ -85,4 +85,10 @@ UnconnectedTextEditor.propTypes = { export const LocalizedTextEditor = localize(UnconnectedTextEditor); -export default connectToContainer(LocalizedTextEditor); +export default connectToContainer(LocalizedTextEditor, { + modifyPlotProps: (props, context, plotProps) => { + if (plotProps.isVisible && plotProps.multiValued) { + plotProps.isVisible = false; + } + }, +}); diff --git a/src/default_panels/StyleTracesPanel.js b/src/default_panels/StyleTracesPanel.js index 89425a00a..ecd2e0754 100644 --- a/src/default_panels/StyleTracesPanel.js +++ b/src/default_panels/StyleTracesPanel.js @@ -20,27 +20,27 @@ import { TraceOrientation, ColorscalePicker, HoverInfo, + Dropdown, FillDropdown, + FontSelector, } from '../components'; import {localize} from '../lib'; const StyleTracesPanel = ({localize: _}) => ( -
- - + + - - -
+ +
( + + + + + + +
1) { + const multiValuedContainer = deepCopyPublic(fullTrace); + fullData.forEach(t => + Object.keys(t).forEach(key => + setMultiValuedContainer(multiValuedContainer, t, key, { + searchArrays: true, + }) + ) + ); + this.childContext.fullContainer = multiValuedContainer; + this.childContext.defaultContainer = fullTrace; + this.childContext.container = {}; + } + if (trace && fullTrace) { this.icon = renderTraceIcon(plotlyTraceToCustomTrace(trace)); this.name = fullTrace.name; @@ -121,6 +136,7 @@ export default function connectTraceToPlot(WrappedComponent) { getValObject: PropTypes.func, updateContainer: PropTypes.func, deleteContainer: PropTypes.func, + defaultContainer: PropTypes.object, container: PropTypes.object, fullContainer: PropTypes.object, }; diff --git a/src/lib/multiValues.js b/src/lib/multiValues.js new file mode 100644 index 000000000..388d05e37 --- /dev/null +++ b/src/lib/multiValues.js @@ -0,0 +1,77 @@ +import {MULTI_VALUED} from './constants'; +import {isPlainObject} from '../lib'; + +/** + * Deep-copies the value using JSON. Underscored (private) keys are removed. + * @param {*} value Some nested value from the plotDiv object. + * @returns {*} A deepcopy of the value. + */ +function deepCopyPublic(value) { + if (typeof value === 'undefined') { + return value; + } + + const skipPrivateKeys = (key, value) => (key.startsWith('_') ? 0 : value); + + return window.JSON.parse(window.JSON.stringify(value, skipPrivateKeys)); +} + +function setMultiValuedContainer(intoObj, fromObj, key, config = {}) { + var intoVal = intoObj[key], + fromVal = fromObj[key]; + + var searchArrays = config.searchArrays; + + // don't merge private attrs + if ( + (typeof key === 'string' && key.charAt(0) === '_') || + typeof intoVal === 'function' || + key === 'module' + ) { + return; + } + + // already a mixture of values, can't get any worse + if (intoVal === MULTI_VALUED) { + return; + } else if (intoVal === void 0) { + // if the original doesn't have the key it's because that key + // doesn't do anything there - so use the new value + // note that if fromObj doesn't have a key in intoObj we will not + // attempt to merge them at all, so this behavior makes the merge + // independent of order. + intoObj[key] = fromVal; + } else if (key === 'colorscale') { + // colorscales are arrays... need to stringify before comparing + // (other vals we don't want to stringify, as differences could + // potentially be real, like 'false' and false) + if (String(intoVal) !== String(fromVal)) { + intoObj[key] = MULTI_VALUED; + } + } else if (Array.isArray(intoVal)) { + // in data, other arrays are data, which we don't care about + // for styling purposes + if (!searchArrays) { + return; + } + // in layout though, we need to recurse into arrays + for (var i = 0; i < fromVal.length; i++) { + setMultiValuedContainer(intoVal, fromVal, i, searchArrays); + } + } else if (isPlainObject(fromVal)) { + // recurse into objects + if (!isPlainObject(intoVal)) { + throw new Error('tried to merge object into non-object: ' + key); + } + Object.keys(fromVal).forEach(function(key2) { + setMultiValuedContainer(intoVal, fromVal, key2, searchArrays); + }); + } else if (isPlainObject(intoVal)) { + throw new Error('tried to merge non-object into object: ' + key); + } else if (intoVal !== fromVal) { + // different non-empty values - + intoObj[key] = MULTI_VALUED; + } +} + +export {deepCopyPublic, setMultiValuedContainer};