Skip to content

Commit 3555fa5

Browse files
Multi-value support in trace tabs except for fin traces (#314)
1 parent cb591cb commit 3555fa5

File tree

8 files changed

+185
-137
lines changed

8 files changed

+185
-137
lines changed

src/components/containers/TraceAccordion.js

+23-24
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const TraceFold = connectTraceToPlot(Fold);
1111

1212
class TraceAccordion extends Component {
1313
render() {
14-
const {data = [], fullData = []} = this.context;
14+
const {data = []} = this.context;
1515
const {
1616
canAdd,
1717
canGroup,
@@ -52,31 +52,30 @@ class TraceAccordion extends Component {
5252
</Panel>
5353
);
5454
}
55-
if (canGroup && data.length > 1) {
56-
const tracesByGroup = data.reduce((allTraces, next, index) => {
57-
const traceType = plotlyTraceToCustomTrace(
58-
fullData.filter(trace => trace.index === index)[0]
55+
const tracesByGroup = data.reduce((allTraces, nextTrace, index) => {
56+
const traceType = plotlyTraceToCustomTrace(nextTrace);
57+
if (!allTraces[traceType]) {
58+
allTraces[traceType] = [];
59+
}
60+
allTraces[traceType].push(index);
61+
return allTraces;
62+
}, {});
63+
64+
const groupedTraces = Object.keys(tracesByGroup)
65+
.filter(traceType => !['ohlc', 'candlestick'].includes(traceType))
66+
.map((traceType, index) => {
67+
return (
68+
<TraceFold
69+
key={index}
70+
traceIndexes={tracesByGroup[traceType]}
71+
name={traceType}
72+
>
73+
{this.props.children}
74+
</TraceFold>
5975
);
60-
if (!allTraces[traceType]) {
61-
allTraces[traceType] = [];
62-
}
63-
allTraces[traceType].push(index);
64-
return allTraces;
65-
}, {});
76+
});
6677

67-
const groupedTraces = Object.keys(tracesByGroup).map(
68-
(traceType, index) => {
69-
return (
70-
<TraceFold
71-
key={index}
72-
traceIndexes={tracesByGroup[traceType]}
73-
name={traceType}
74-
>
75-
{this.props.children}
76-
</TraceFold>
77-
);
78-
}
79-
);
78+
if (canGroup && data.length > 1 && groupedTraces.length > 0) {
8079
return (
8180
<TraceRequiredPanel noPadding>
8281
<Tabs>

src/components/containers/derived.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ const TraceTypeSection = (props, context) => {
1818
const {fullContainer} = context;
1919
if (
2020
fullContainer &&
21-
fullContainer._fullInput &&
22-
props.traceTypes.includes(fullContainer._fullInput.type)
21+
((fullContainer._fullInput &&
22+
props.traceTypes.includes(fullContainer._fullInput.type)) ||
23+
props.traceTypes.includes(fullContainer.type))
2324
) {
2425
return <Section {...props} />;
2526
}

src/components/fields/SymbolSelector.js

+22-7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import React, {Component} from 'react';
44
import SymbolSelectorWidget from '../widgets/SymbolSelector';
55
import nestedProperty from 'plotly.js/src/lib/nested_property';
66
import {connectToContainer, tooLight} from 'lib';
7+
import {MULTI_VALUED} from '../../lib/constants';
78

89
// TODO compute these from plotly.js
910
const SYMBOLS = [
@@ -353,27 +354,38 @@ const SYMBOLS = [
353354
];
354355

355356
class SymbolSelector extends Component {
356-
constructor(props) {
357-
super(props);
358-
this.setLocals(props);
357+
constructor(props, context) {
358+
super(props, context);
359+
this.setLocals(props, context);
359360
}
360361

361-
componentWillReceiveProps(nextProps) {
362-
this.setLocals(nextProps);
362+
componentWillReceiveProps(nextProps, nextContext) {
363+
this.setLocals(nextProps, nextContext);
363364
}
364365

365-
setLocals(props) {
366+
setLocals(props, context) {
366367
const {fullContainer} = props;
368+
const {defaultContainer} = context;
367369

368370
this.markerColor = nestedProperty(fullContainer, 'marker.color').get();
369371
this.borderWidth = nestedProperty(fullContainer, 'marker.line.width').get();
370372

373+
if (this.markerColor === MULTI_VALUED) {
374+
this.markerColor = nestedProperty(defaultContainer, 'marker.color').get();
375+
}
376+
371377
this.borderColor = this.markerColor;
372378
if (this.borderWidth) {
373379
this.borderColor = nestedProperty(
374380
fullContainer,
375381
'marker.line.color'
376382
).get();
383+
if (this.borderColor === MULTI_VALUED) {
384+
this.borderColor = nestedProperty(
385+
defaultContainer,
386+
'marker.line.color'
387+
).get();
388+
}
377389
}
378390

379391
if (this.props.is3D) {
@@ -403,11 +415,14 @@ class SymbolSelector extends Component {
403415
}
404416

405417
SymbolSelector.propTypes = {
406-
defaultValue: PropTypes.number,
418+
defaultValue: PropTypes.string,
407419
fullValue: PropTypes.any,
408420
updatePlot: PropTypes.func,
409421
...Field.propTypes,
410422
};
423+
SymbolSelector.contextTypes = {
424+
defaultContainer: PropTypes.object,
425+
};
411426

412427
SymbolSelector.defaultProps = {
413428
showArrows: true,

src/components/fields/TextEditor.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,10 @@ UnconnectedTextEditor.propTypes = {
8585

8686
export const LocalizedTextEditor = localize(UnconnectedTextEditor);
8787

88-
export default connectToContainer(LocalizedTextEditor);
88+
export default connectToContainer(LocalizedTextEditor, {
89+
modifyPlotProps: (props, context, plotProps) => {
90+
if (plotProps.isVisible && plotProps.multiValued) {
91+
plotProps.isVisible = false;
92+
}
93+
},
94+
});

src/default_panels/StyleTracesPanel.js

+35-13
Original file line numberDiff line numberDiff line change
@@ -20,27 +20,27 @@ import {
2020
TraceOrientation,
2121
ColorscalePicker,
2222
HoverInfo,
23+
Dropdown,
2324
FillDropdown,
25+
FontSelector,
2426
} from '../components';
2527

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

2830
const StyleTracesPanel = ({localize: _}) => (
2931
<TraceAccordion canGroup>
30-
<Section name={_('Trace')} attr="name">
31-
<TextEditor label={_('Name')} attr="name" richTextOnly />
32-
<TraceOrientation
33-
label={_('Orientation')}
34-
attr="orientation"
35-
options={[
36-
{label: _('Vertical'), value: 'v'},
37-
{label: _('Horizontal'), value: 'h'},
38-
]}
39-
/>
32+
<TextEditor label={_('Name')} attr="name" richTextOnly />
33+
<TraceOrientation
34+
label={_('Orientation')}
35+
attr="orientation"
36+
options={[
37+
{label: _('Vertical'), value: 'v'},
38+
{label: _('Horizontal'), value: 'h'},
39+
]}
40+
/>
4041

41-
<Numeric label={_('Opacity')} step={0.1} attr="opacity" />
42-
<ColorPicker label={_('Color')} attr="color" />
43-
</Section>
42+
<Numeric label={_('Opacity')} step={0.1} attr="opacity" />
43+
<ColorPicker label={_('Color')} attr="color" />
4444

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

146+
<TraceTypeSection name={_('Text')} traceTypes={['scatter']}>
147+
<FontSelector label={_('Typeface')} attr="textfont.family" />
148+
<Numeric label={_('Font Size')} attr="textfont.size" units="px" />
149+
<ColorPicker label={_('Font Color')} attr="textfont.color" />
150+
<Dropdown
151+
label={_('Text Position')}
152+
attr="textposition"
153+
clearable={false}
154+
options={[
155+
{label: _('Top Left'), value: 'top left'},
156+
{label: _('Top Center'), value: 'top center'},
157+
{label: _('Top Right'), value: 'top right'},
158+
{label: _('Middle Left'), value: 'middle left'},
159+
{label: _('Middle Center'), value: 'middle center'},
160+
{label: _('Middle Right'), value: 'middle right'},
161+
{label: _('Bottom Left'), value: 'bottom left'},
162+
{label: _('Bottom Center'), value: 'bottom center'},
163+
{label: _('Bottom Right'), value: 'bottom right'},
164+
]}
165+
/>
166+
</TraceTypeSection>
167+
146168
<Section name={_('Colorscale')}>
147169
<ColorscalePicker label={_('Colorscale')} attr="colorscale" />
148170
<Radio

src/lib/connectAxesToLayout.js

+2-90
Original file line numberDiff line numberDiff line change
@@ -1,96 +1,8 @@
11
import React, {Component} from 'react';
22
import PropTypes from 'prop-types';
33
import nestedProperty from 'plotly.js/src/lib/nested_property';
4-
import {MULTI_VALUED} from './constants';
5-
import {getDisplayName, isPlainObject, localize} from '../lib';
6-
7-
/**
8-
* Simple replacer to use with JSON.stringify.
9-
* @param {*} key Current object key.
10-
* @param {*} value Current value in object at key.
11-
* @returns {*} If we return undefined, the key is skipped in JSON.stringify.
12-
*/
13-
function skipPrivateKeys(key, value) {
14-
if (key.startsWith('_')) {
15-
return void 0;
16-
}
17-
18-
return value;
19-
}
20-
21-
/**
22-
* Deep-copies the value using JSON. Underscored (private) keys are removed.
23-
* @param {*} value Some nested value from the plotDiv object.
24-
* @returns {*} A deepcopy of the value.
25-
*/
26-
function deepCopyPublic(value) {
27-
if (typeof value === 'undefined') {
28-
return value;
29-
}
30-
31-
return window.JSON.parse(window.JSON.stringify(value, skipPrivateKeys));
32-
}
33-
34-
/*
35-
* Test that we can connectLayoutToPlot(connectAxesToLayout(Panel))
36-
*/
37-
function setMultiValuedContainer(intoObj, fromObj, key, config = {}) {
38-
var intoVal = intoObj[key],
39-
fromVal = fromObj[key];
40-
41-
var searchArrays = config.searchArrays;
42-
43-
// don't merge private attrs
44-
if (
45-
(typeof key === 'string' && key.charAt(0) === '_') ||
46-
typeof intoVal === 'function' ||
47-
key === 'module'
48-
) {
49-
return;
50-
}
51-
52-
// already a mixture of values, can't get any worse
53-
if (intoVal === MULTI_VALUED) {
54-
return;
55-
} else if (intoVal === void 0) {
56-
// if the original doesn't have the key it's because that key
57-
// doesn't do anything there - so use the new value
58-
// note that if fromObj doesn't have a key in intoObj we will not
59-
// attempt to merge them at all, so this behavior makes the merge
60-
// independent of order.
61-
intoObj[key] = fromVal;
62-
} else if (key === 'colorscale') {
63-
// colorscales are arrays... need to stringify before comparing
64-
// (other vals we don't want to stringify, as differences could
65-
// potentially be real, like 'false' and false)
66-
if (String(intoVal) !== String(fromVal)) {
67-
intoObj[key] = MULTI_VALUED;
68-
}
69-
} else if (Array.isArray(intoVal)) {
70-
// in data, other arrays are data, which we don't care about
71-
// for styling purposes
72-
if (!searchArrays) {
73-
return;
74-
}
75-
// in layout though, we need to recurse into arrays
76-
for (var i = 0; i < fromVal.length; i++) {
77-
setMultiValuedContainer(intoVal, fromVal, i, searchArrays);
78-
}
79-
} else if (isPlainObject(fromVal)) {
80-
// recurse into objects
81-
if (!isPlainObject(intoVal)) {
82-
throw new Error('tried to merge object into non-object: ' + key);
83-
}
84-
Object.keys(fromVal).forEach(function(key2) {
85-
setMultiValuedContainer(intoVal, fromVal, key2, searchArrays);
86-
});
87-
} else if (isPlainObject(intoVal)) {
88-
throw new Error('tried to merge non-object into object: ' + key);
89-
} else if (intoVal !== fromVal) {
90-
// different non-empty values -
91-
intoObj[key] = MULTI_VALUED;
92-
}
93-
}
4+
import {deepCopyPublic, setMultiValuedContainer} from './multiValues';
5+
import {getDisplayName, localize} from '../lib';
946

957
function computeAxesOptions(axes, _) {
968
const options = [{label: _('All'), value: 'allaxes'}];

src/lib/connectTraceToPlot.js

+16
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
plotlyTraceToCustomTrace,
88
renderTraceIcon,
99
} from '../lib';
10+
import {deepCopyPublic, setMultiValuedContainer} from './multiValues';
1011
import {EDITOR_ACTIONS} from './constants';
1112

1213
export default function connectTraceToPlot(WrappedComponent) {
@@ -57,6 +58,20 @@ export default function connectTraceToPlot(WrappedComponent) {
5758
fullContainer: fullTrace,
5859
};
5960

61+
if (traceIndexes.length > 1) {
62+
const multiValuedContainer = deepCopyPublic(fullTrace);
63+
fullData.forEach(t =>
64+
Object.keys(t).forEach(key =>
65+
setMultiValuedContainer(multiValuedContainer, t, key, {
66+
searchArrays: true,
67+
})
68+
)
69+
);
70+
this.childContext.fullContainer = multiValuedContainer;
71+
this.childContext.defaultContainer = fullTrace;
72+
this.childContext.container = {};
73+
}
74+
6075
if (trace && fullTrace) {
6176
this.icon = renderTraceIcon(plotlyTraceToCustomTrace(trace));
6277
this.name = fullTrace.name;
@@ -121,6 +136,7 @@ export default function connectTraceToPlot(WrappedComponent) {
121136
getValObject: PropTypes.func,
122137
updateContainer: PropTypes.func,
123138
deleteContainer: PropTypes.func,
139+
defaultContainer: PropTypes.object,
124140
container: PropTypes.object,
125141
fullContainer: PropTypes.object,
126142
};

0 commit comments

Comments
 (0)