Skip to content

Commit e6ab3d4

Browse files
authored
Merge pull request #611 from plotly/color-multiple-traces
Color multiple traces
2 parents b771962 + c59786c commit e6ab3d4

File tree

13 files changed

+231
-83
lines changed

13 files changed

+231
-83
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"prop-types": "^15.5.10",
2020
"raf": "^3.4.0",
2121
"react-color": "^2.13.8",
22-
"react-colorscales": "0.5.7",
22+
"react-colorscales": "0.5.8",
2323
"react-dropzone": "^4.2.9",
2424
"react-plotly.js": "^2.2.0",
2525
"react-rangeslider": "^2.2.0",

src/components/containers/TraceAccordion.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,17 +86,17 @@ class TraceAccordion extends Component {
8686
<TraceRequiredPanel noPadding>
8787
<Tabs>
8888
<TabList>
89-
<Tab>{_('All Traces')}</Tab>
90-
<Tab>{_('Individual')}</Tab>
89+
<Tab>{_('Individually')}</Tab>
90+
<Tab>{_('By Type')}</Tab>
9191
</TabList>
92-
<TabPanel>
93-
<PlotlyPanel>{groupedTraces ? groupedTraces : null}</PlotlyPanel>
94-
</TabPanel>
9592
<TabPanel>
9693
<PlotlyPanel>
9794
{individualTraces ? individualTraces : null}
9895
</PlotlyPanel>
9996
</TabPanel>
97+
<TabPanel>
98+
<PlotlyPanel>{groupedTraces ? groupedTraces : null}</PlotlyPanel>
99+
</TabPanel>
100100
</Tabs>
101101
</TraceRequiredPanel>
102102
);

src/components/fields/Colorscale.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class Colorscale extends Component {
3535
<ColorscalePicker
3636
selected={colorscale}
3737
onColorscaleChange={this.onUpdate}
38+
initialCategory={this.props.initialCategory}
3839
/>
3940
</Field>
4041
);
@@ -44,7 +45,24 @@ class Colorscale extends Component {
4445
Colorscale.propTypes = {
4546
fullValue: PropTypes.any,
4647
updatePlot: PropTypes.func,
48+
initialCategory: PropTypes.string,
4749
...Field.propTypes,
4850
};
4951

50-
export default connectToContainer(Colorscale);
52+
export default connectToContainer(Colorscale, {
53+
modifyPlotProps: (props, context, plotProps) => {
54+
if (
55+
props.attr === 'marker.color' &&
56+
context.fullData
57+
.filter(t => context.traceIndexes.includes(t.index))
58+
.every(t => t.marker && t.marker.color) &&
59+
(plotProps.fullValue && typeof plotProps.fullValue === 'string')
60+
) {
61+
plotProps.fullValue =
62+
context.fullData &&
63+
context.fullData
64+
.filter(t => context.traceIndexes.includes(t.index))
65+
.map(t => [0, t.marker.color]);
66+
}
67+
},
68+
});

src/components/fields/DataSelector.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ export class UnconnectedDataSelector extends Component {
108108
optionRenderer={this.context.dataSourceOptionRenderer}
109109
valueRenderer={this.context.dataSourceValueRenderer}
110110
clearable={true}
111+
placeholder={this.props.placeholder}
111112
/>
112113
</Field>
113114
);
@@ -118,6 +119,7 @@ UnconnectedDataSelector.propTypes = {
118119
fullValue: PropTypes.any,
119120
updatePlot: PropTypes.func,
120121
container: PropTypes.object,
122+
placeholder: PropTypes.string,
121123
...Field.propTypes,
122124
};
123125

src/components/fields/MarkerColor.js

Lines changed: 163 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ import Color from './Color';
77
import Colorscale from './Colorscale';
88
import Numeric from './Numeric';
99
import Radio from './Radio';
10+
import Info from './Info';
1011
import DataSelector from './DataSelector';
1112
import VisibilitySelect from './VisibilitySelect';
1213
import {MULTI_VALUED, COLORS} from 'lib/constants';
14+
import {getColorscale} from 'react-colorscales';
1315

1416
class UnconnectedMarkerColor extends Component {
1517
constructor(props, context) {
@@ -36,28 +38,33 @@ class UnconnectedMarkerColor extends Component {
3638
constant: type === 'constant' ? props.fullValue : COLORS.mutedBlue,
3739
variable: type === 'variable' ? props.fullValue : null,
3840
},
41+
constantSelectedOption:
42+
type === 'constant' && props.multiValued ? 'multiple' : 'single',
3943
};
4044

4145
this.setType = this.setType.bind(this);
4246
this.setValue = this.setValue.bind(this);
4347
this.setColorScale = this.setColorScale.bind(this);
48+
this.setColors = this.setColors.bind(this);
4449
}
4550

4651
setType(type) {
47-
this.setState({type: type});
48-
this.props.updatePlot(this.state.value[type]);
49-
if (type === 'constant') {
50-
this.context.updateContainer({
51-
['marker.colorsrc']: null,
52-
['marker.colorscale']: null,
53-
});
54-
this.setState({colorscale: null});
55-
} else {
56-
this.context.updateContainer({
57-
['marker.color']: null,
58-
['marker.colorsrc']: null,
59-
['marker.colorscale']: [],
60-
});
52+
if (this.state.type !== type) {
53+
this.setState({type: type});
54+
this.props.updatePlot(this.state.value[type]);
55+
if (type === 'constant') {
56+
this.context.updateContainer({
57+
['marker.colorsrc']: null,
58+
['marker.colorscale']: null,
59+
});
60+
this.setState({colorscale: null});
61+
} else {
62+
this.context.updateContainer({
63+
['marker.color']: null,
64+
['marker.colorsrc']: null,
65+
['marker.colorscale']: [],
66+
});
67+
}
6168
}
6269
}
6370

@@ -77,58 +84,158 @@ class UnconnectedMarkerColor extends Component {
7784
this.context.updateContainer({['marker.colorscale']: inputValue});
7885
}
7986

87+
isMultiValued() {
88+
return (
89+
this.props.multiValued ||
90+
(Array.isArray(this.props.fullValue) &&
91+
this.props.fullValue.includes(MULTI_VALUED)) ||
92+
(this.props.container.marker &&
93+
this.props.container.marker.colorscale === MULTI_VALUED) ||
94+
(this.props.container.marker &&
95+
this.props.container.marker.colorsrc === MULTI_VALUED) ||
96+
(this.props.container.marker &&
97+
this.props.container.marker.color &&
98+
Array.isArray(this.props.container.marker.color) &&
99+
this.props.container.marker.color.includes(MULTI_VALUED))
100+
);
101+
}
102+
103+
setColors(colorscale) {
104+
const numberOfTraces = this.context.traceIndexes.length;
105+
const colors = colorscale.map(c => c[1]);
106+
107+
let adjustedColors = getColorscale(colors, numberOfTraces);
108+
if (adjustedColors.every(c => c === adjustedColors[0])) {
109+
adjustedColors = colors;
110+
}
111+
112+
const updates = adjustedColors.map(color => ({
113+
['marker.color']: color,
114+
}));
115+
116+
this.setState({
117+
colorscale: adjustedColors,
118+
});
119+
120+
this.context.updateContainer(updates);
121+
}
122+
123+
renderConstantControls() {
124+
const _ = this.context.localize;
125+
const constantOptions = [
126+
{label: _('Single'), value: 'single'},
127+
{label: _('Multiple'), value: 'multiple'},
128+
];
129+
130+
if (this.context.traceIndexes.length > 1) {
131+
return (
132+
<div className="markercolor-constantcontrols__container">
133+
<RadioBlocks
134+
options={constantOptions}
135+
activeOption={this.state.constantSelectedOption}
136+
onOptionChange={value =>
137+
this.setState({constantSelectedOption: value})
138+
}
139+
/>
140+
<Info>
141+
{this.state.constantSelectedOption === 'single'
142+
? _('All traces will be colored in the the same color.')
143+
: _(
144+
'Each trace will be colored according to the selected colorscale.'
145+
)}
146+
</Info>
147+
{this.state.constantSelectedOption === 'single' ? (
148+
<Color
149+
attr="marker.color"
150+
updatePlot={this.setValue}
151+
fullValue={this.state.value.constant}
152+
/>
153+
) : (
154+
<Colorscale
155+
suppressMultiValuedMessage
156+
attr="marker.color"
157+
updatePlot={this.setColors}
158+
colorscale={this.state.colorscale}
159+
initialCategory={'categorical'}
160+
/>
161+
)}
162+
</div>
163+
);
164+
}
165+
166+
return (
167+
<Color
168+
attr="marker.color"
169+
updatePlot={this.setValue}
170+
fullValue={this.state.value.constant}
171+
/>
172+
);
173+
}
174+
175+
renderVariableControls() {
176+
const _ = this.context.localize;
177+
const multiValued =
178+
(this.props.container &&
179+
this.props.container.marker &&
180+
(this.props.container.marker.colorscale &&
181+
this.props.container.marker.colorscale === MULTI_VALUED)) ||
182+
(this.props.container.marker.colorsrc &&
183+
this.props.container.marker.colorsrc === MULTI_VALUED);
184+
return (
185+
<Field multiValued={multiValued}>
186+
<DataSelector
187+
suppressMultiValuedMessage
188+
attr="marker.color"
189+
placeholder={_('Select a Data Option')}
190+
/>
191+
{this.props.container.marker &&
192+
this.props.container.marker.colorscale === MULTI_VALUED ? null : (
193+
<Colorscale
194+
suppressMultiValuedMessage
195+
attr="marker.colorscale"
196+
updatePlot={this.setColorScale}
197+
colorscale={this.state.colorscale}
198+
/>
199+
)}
200+
</Field>
201+
);
202+
}
203+
80204
render() {
81-
const {attr, fullValue, container} = this.props;
205+
const {attr} = this.props;
82206
const {localize: _} = this.context;
83-
const {type, value, colorscale} = this.state;
207+
const {type} = this.state;
84208
const options = [
85209
{label: _('Constant'), value: 'constant'},
86210
{label: _('Variable'), value: 'variable'},
87211
];
88-
const multiValued =
89-
this.props.multiValued ||
90-
(Array.isArray(fullValue) && fullValue.includes(MULTI_VALUED)) ||
91-
(container.marker && container.marker.colorscale === MULTI_VALUED) ||
92-
(container.marker && container.marker.colorsrc === MULTI_VALUED) ||
93-
(container.marker &&
94-
container.marker.color &&
95-
Array.isArray(container.marker.color) &&
96-
container.marker.color.includes(MULTI_VALUED));
97212

98213
return (
99214
<Fragment>
100-
<Field {...this.props} multiValued={multiValued} attr={attr}>
101-
<RadioBlocks
102-
options={options}
103-
activeOption={type}
104-
onOptionChange={this.setType}
105-
/>
106-
{!type ? null : type === 'constant' ? (
107-
<Color
108-
suppressMultiValuedMessage
109-
attr="marker.color"
110-
updatePlot={this.setValue}
111-
fullValue={value.constant}
215+
<Field {...this.props} attr={attr}>
216+
<Field multiValued={this.isMultiValued() && !this.state.type}>
217+
<RadioBlocks
218+
options={options}
219+
activeOption={type}
220+
onOptionChange={this.setType}
112221
/>
113-
) : container.marker &&
114-
container.marker.colorsrc === MULTI_VALUED ? null : (
115-
<Fragment>
116-
<DataSelector suppressMultiValuedMessage attr="marker.color" />
117-
{container.marker &&
118-
container.marker.colorscale === MULTI_VALUED ? null : (
119-
<Colorscale
120-
suppressMultiValuedMessage
121-
attr="marker.colorscale"
122-
updatePlot={this.setColorScale}
123-
colorscale={colorscale}
124-
/>
125-
)}
126-
</Fragment>
127-
)}
222+
223+
{!type ? null : (
224+
<Info>
225+
{type === 'constant'
226+
? _('All points in a trace are colored in the same color.')
227+
: _('Each point in a trace is colored according to data.')}
228+
</Info>
229+
)}
230+
</Field>
231+
232+
{!type
233+
? null
234+
: type === 'constant'
235+
? this.renderConstantControls()
236+
: this.renderVariableControls()}
128237
</Field>
129-
{type === 'constant' ? (
130-
''
131-
) : (
238+
{type === 'constant' ? null : (
132239
<Fragment>
133240
<Radio
134241
label={_('Colorscale Direction')}
@@ -175,6 +282,7 @@ UnconnectedMarkerColor.propTypes = {
175282
UnconnectedMarkerColor.contextTypes = {
176283
localize: PropTypes.func,
177284
updateContainer: PropTypes.func,
285+
traceIndexes: PropTypes.array,
178286
};
179287

180288
export default connectToContainer(UnconnectedMarkerColor);

src/components/widgets/ColorscalePicker.js

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ import PropTypes from 'prop-types';
88
import React, {Component, Fragment} from 'react';
99

1010
class Scale extends Component {
11-
constructor() {
12-
super();
11+
constructor(props) {
12+
super(props);
1313

1414
this.state = {
15-
selectedColorscaleType: 'sequential',
15+
selectedColorscaleType: props.initialCategory || 'sequential',
1616
showColorscalePicker: false,
1717
};
1818

@@ -82,10 +82,7 @@ Scale.propTypes = {
8282
onColorscaleChange: PropTypes.func,
8383
selected: PropTypes.array,
8484
label: PropTypes.string,
85-
};
86-
87-
Scale.contextTypes = {
88-
localize: PropTypes.func,
85+
initialCategory: PropTypes.string,
8986
};
9087

9188
Scale.contextTypes = {

src/default_panels/StyleTracesPanel.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,11 @@ const StyleTracesPanel = (props, {localize: _}) => (
225225
/>
226226
<NumericFraction label={_('Jitter')} attr="jitter" />
227227
<Numeric label={_('Position')} attr="pointpos" step={0.1} showSlider />
228-
<MarkerColor label={_('Color')} attr="marker.color" />
228+
<MarkerColor
229+
suppressMultiValuedMessage
230+
label={_('Color')}
231+
attr="marker.color"
232+
/>
229233
<NumericFraction label={_('Opacity')} attr="marker.opacity" />
230234
<MarkerSize label={_('Size')} attr="marker.size" />
231235
<Radio

src/lib/connectToContainer.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export const containerConnectedContextTypes = {
1717
onUpdate: PropTypes.func,
1818
plotly: PropTypes.object,
1919
updateContainer: PropTypes.func,
20+
traceIndexes: PropTypes.array,
2021
};
2122

2223
export default function connectToContainer(WrappedComponent, config = {}) {

0 commit comments

Comments
 (0)