Skip to content

Multi-value support in trace tabs #314

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 8, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 23 additions & 24 deletions src/components/containers/TraceAccordion.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -52,31 +52,30 @@ class TraceAccordion extends Component {
</Panel>
);
}
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))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is ugly but prevents really weird outcomes.

.map((traceType, index) => {
return (
<TraceFold
key={index}
traceIndexes={tracesByGroup[traceType]}
name={traceType}
>
{this.props.children}
</TraceFold>
);
if (!allTraces[traceType]) {
allTraces[traceType] = [];
}
allTraces[traceType].push(index);
return allTraces;
}, {});
});

const groupedTraces = Object.keys(tracesByGroup).map(
(traceType, index) => {
return (
<TraceFold
key={index}
traceIndexes={tracesByGroup[traceType]}
name={traceType}
>
{this.props.children}
</TraceFold>
);
}
);
if (canGroup && data.length > 1 && groupedTraces.length > 0) {
return (
<TraceRequiredPanel noPadding>
<Tabs>
Expand Down
5 changes: 3 additions & 2 deletions src/components/containers/derived.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)) ||
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@VeraZab _fullInput is not available in some code paths, so this is the compromise. Specifically, in the grouped context, the all keys starting with _ are missing from container as per the original logic from the grouped Axes. If I remove this exclusion, we end up with circular JSON objects that cannot be serialized :)

props.traceTypes.includes(fullContainer.type))
) {
return <Section {...props} />;
}
Expand Down
29 changes: 22 additions & 7 deletions src/components/fields/SymbolSelector.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is very tricky: we don't actually end up using default values here and I couldn't figure out how to do it... we use the values from the first trace. I've added a note to fit'n'finish

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well, that's what the current workspace does, sounds ok to me!

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) {
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 7 additions & 1 deletion src/components/fields/TextEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
},
});
48 changes: 35 additions & 13 deletions src/default_panels/StyleTracesPanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,27 @@ import {
TraceOrientation,
ColorscalePicker,
HoverInfo,
Dropdown,
FillDropdown,
FontSelector,
} from '../components';

import {localize} from '../lib';

const StyleTracesPanel = ({localize: _}) => (
<TraceAccordion canGroup>
<Section name={_('Trace')} attr="name">
<TextEditor label={_('Name')} attr="name" richTextOnly />
<TraceOrientation
label={_('Orientation')}
attr="orientation"
options={[
{label: _('Vertical'), value: 'v'},
{label: _('Horizontal'), value: 'h'},
]}
/>
<TextEditor label={_('Name')} attr="name" richTextOnly />
<TraceOrientation
label={_('Orientation')}
attr="orientation"
options={[
{label: _('Vertical'), value: 'v'},
{label: _('Horizontal'), value: 'h'},
]}
/>

<Numeric label={_('Opacity')} step={0.1} attr="opacity" />
<ColorPicker label={_('Color')} attr="color" />
</Section>
<Numeric label={_('Opacity')} step={0.1} attr="opacity" />
<ColorPicker label={_('Color')} attr="color" />

<Section name={_('Text Attributes')}>
<Flaglist
Expand Down Expand Up @@ -143,6 +143,28 @@ const StyleTracesPanel = ({localize: _}) => (
<LineShapeSelector label={_('Shape')} attr="line.shape" />
</TraceTypeSection>

<TraceTypeSection name={_('Text')} traceTypes={['scatter']}>
<FontSelector label={_('Typeface')} attr="textfont.family" />
<Numeric label={_('Font Size')} attr="textfont.size" units="px" />
<ColorPicker label={_('Font Color')} attr="textfont.color" />
<Dropdown
label={_('Text Position')}
attr="textposition"
clearable={false}
options={[
{label: _('Top Left'), value: 'top left'},
{label: _('Top Center'), value: 'top center'},
{label: _('Top Right'), value: 'top right'},
{label: _('Middle Left'), value: 'middle left'},
{label: _('Middle Center'), value: 'middle center'},
{label: _('Middle Right'), value: 'middle right'},
{label: _('Bottom Left'), value: 'bottom left'},
{label: _('Bottom Center'), value: 'bottom center'},
{label: _('Bottom Right'), value: 'bottom right'},
]}
/>
</TraceTypeSection>

<Section name={_('Colorscale')}>
<ColorscalePicker label={_('Colorscale')} attr="colorscale" />
<Radio
Expand Down
92 changes: 2 additions & 90 deletions src/lib/connectAxesToLayout.js
Original file line number Diff line number Diff line change
@@ -1,96 +1,8 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import nestedProperty from 'plotly.js/src/lib/nested_property';
import {MULTI_VALUED} from './constants';
import {getDisplayName, isPlainObject, localize} from '../lib';

/**
* Simple replacer to use with JSON.stringify.
* @param {*} key Current object key.
* @param {*} value Current value in object at key.
* @returns {*} If we return undefined, the key is skipped in JSON.stringify.
*/
function skipPrivateKeys(key, value) {
if (key.startsWith('_')) {
return void 0;
}

return value;
}

/**
* 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;
}

return window.JSON.parse(window.JSON.stringify(value, skipPrivateKeys));
}

/*
* Test that we can connectLayoutToPlot(connectAxesToLayout(Panel))
*/
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;
}
}
import {deepCopyPublic, setMultiValuedContainer} from './multiValues';
import {getDisplayName, localize} from '../lib';

function computeAxesOptions(axes, _) {
const options = [{label: _('All'), value: 'allaxes'}];
Expand Down
16 changes: 16 additions & 0 deletions src/lib/connectTraceToPlot.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
plotlyTraceToCustomTrace,
renderTraceIcon,
} from '../lib';
import {deepCopyPublic, setMultiValuedContainer} from './multiValues';
import {EDITOR_ACTIONS} from './constants';

export default function connectTraceToPlot(WrappedComponent) {
Expand Down Expand Up @@ -57,6 +58,20 @@ export default function connectTraceToPlot(WrappedComponent) {
fullContainer: fullTrace,
};

if (traceIndexes.length > 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;
Expand Down Expand Up @@ -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,
};
Expand Down
Loading