Skip to content

Commit be01eb4

Browse files
authored
Merge pull request #357 from plotly/subplots2
Subplots2
2 parents 572ac69 + f8c86ed commit be01eb4

23 files changed

+624
-100
lines changed

src/PlotlyEditor.js

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import DefaultEditor from './DefaultEditor';
22
import PropTypes from 'prop-types';
33
import React, {Component} from 'react';
44
import {bem} from './lib';
5-
import {noShame, maybeClearAxisTypes} from './shame';
5+
import {maybeClearAxisTypes} from './shame';
66
import {EDITOR_ACTIONS} from './lib/constants';
77
import isNumeric from 'fast-isnumeric';
88
import nestedProperty from 'plotly.js/src/lib/nested_property';
@@ -11,8 +11,6 @@ class PlotlyEditor extends Component {
1111
constructor(props, context) {
1212
super(props, context);
1313

14-
noShame({plotly: this.props.plotly});
15-
1614
// we only need to compute this once.
1715
if (this.props.plotly) {
1816
this.plotSchema = this.props.plotly.PlotSchema.get();
@@ -102,6 +100,31 @@ class PlotlyEditor extends Component {
102100
}
103101
break;
104102

103+
case EDITOR_ACTIONS.UPDATE_AXIS_REFERENCES:
104+
payload.tracesToAdjust.forEach(trace => {
105+
const axis = trace[payload.attrToAdjust].charAt(0);
106+
// n.b: currentAxisIdNumber will never be 0, i.e. Number('x'.slice(1)),
107+
// because payload.tracesToAdjust is a filter of all traces that have
108+
// an axis ID above the one of the axis ID we deprecated
109+
const currentAxisIdNumber = Number(
110+
trace[payload.attrToAdjust].slice(1)
111+
);
112+
const adjustedAxisIdNumber = currentAxisIdNumber - 1;
113+
114+
const currentAxisLayoutProperties = {
115+
...graphDiv.layout[payload.attrToAdjust + currentAxisIdNumber],
116+
};
117+
118+
graphDiv.data[trace.index][payload.attrToAdjust] =
119+
// for cases when we're adjusting x2 => x, so that it becomes x not x1
120+
adjustedAxisIdNumber === 1 ? axis : axis + adjustedAxisIdNumber;
121+
122+
graphDiv.layout[
123+
payload.attrToAdjust + adjustedAxisIdNumber
124+
] = currentAxisLayoutProperties;
125+
});
126+
break;
127+
105128
case EDITOR_ACTIONS.ADD_TRACE:
106129
if (this.props.beforeAddTrace) {
107130
this.props.beforeAddTrace(payload);

src/components/containers/ImageAccordion.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ class ImageAccordion extends Component {
1313

1414
const content =
1515
images.length &&
16-
images.map((ann, i) => (
17-
<ImageFold key={i} imageIndex={i} name={ann.text} canDelete={canAdd}>
16+
images.map((img, i) => (
17+
<ImageFold key={i} imageIndex={i} name={img.text} canDelete={canAdd}>
1818
{children}
1919
</ImageFold>
2020
));

src/components/containers/Section.js

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import Info from '../fields/Info';
21
import React, {Component, cloneElement} from 'react';
32
import PropTypes from 'prop-types';
43
import {
54
containerConnectedContextTypes,
65
localize,
76
unpackPlotProps,
7+
traceTypeToAxisType,
88
} from '../../lib';
99

1010
class Section extends Component {
@@ -36,7 +36,23 @@ class Section extends Component {
3636
return null;
3737
}
3838

39-
if (child.props.attr) {
39+
if ((child.type.plotly_editor_traits || {}).is_axis_creator) {
40+
const {data, fullContainer} = this.context;
41+
42+
// for now, only allowing for cartesian chart types
43+
if (
44+
data.length > 1 &&
45+
traceTypeToAxisType(data[fullContainer.index].type) === 'cartesian'
46+
) {
47+
this.sectionVisible = true;
48+
return cloneElement(child, {
49+
isVisible: true,
50+
container: this.context.container,
51+
fullContainer: this.context.fullContainer,
52+
});
53+
}
54+
this.sectionVisible = false;
55+
} else if (child.props.attr) {
4056
let plotProps;
4157
if (child.type.supplyPlotProps) {
4258
plotProps = child.type.supplyPlotProps(child.props, nextContext);
@@ -51,7 +67,9 @@ class Section extends Component {
5167
// it will see plotProps and skip recomputing them.
5268
this.sectionVisible = this.sectionVisible || plotProps.isVisible;
5369
return cloneElement(child, {plotProps});
54-
} else if (child.type !== Info) {
70+
} else if (
71+
!(child.type.plotly_editor_traits || {}).no_visibility_forcing
72+
) {
5573
// custom UI other than Info forces section visibility.
5674
this.sectionVisible = true;
5775
}

src/components/containers/ShapeAccordion.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ class ShapeAccordion extends Component {
1313

1414
const content =
1515
shapes.length &&
16-
shapes.map((ann, i) => (
17-
<ShapeFold key={i} shapeIndex={i} name={ann.text} canDelete={canAdd}>
16+
shapes.map((shp, i) => (
17+
<ShapeFold key={i} shapeIndex={i} name={shp.text} canDelete={canAdd}>
1818
{children}
1919
</ShapeFold>
2020
));

src/components/fields/AxesSelector.js

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import Field from './Field';
22
import PropTypes from 'prop-types';
3+
import Dropdown from '../widgets/Dropdown';
34
import RadioBlocks from '../widgets/RadioBlocks';
45
import React, {Component} from 'react';
6+
import {localize} from 'lib';
57

6-
export default class AxesSelector extends Component {
8+
class AxesSelector extends Component {
79
constructor(props, context) {
810
super(props, context);
911

@@ -16,6 +18,37 @@ export default class AxesSelector extends Component {
1618

1719
render() {
1820
const {axesTargetHandler, axesOptions, axesTarget} = this.context;
21+
const {localize: _} = this.props;
22+
const hasSecondaryAxis =
23+
axesOptions &&
24+
axesOptions.some(option => {
25+
return (
26+
option.axisGroup &&
27+
this.context.fullLayout._subplots[option.axisGroup].length > 1
28+
);
29+
});
30+
31+
if (hasSecondaryAxis) {
32+
return (
33+
<Field {...this.props} label={_('Axis to Style')}>
34+
<Dropdown
35+
options={axesOptions.map(option => {
36+
if (option.value !== 'allaxes') {
37+
return {
38+
label: option.title,
39+
value: option.value,
40+
};
41+
}
42+
43+
return option;
44+
})}
45+
value={axesTarget}
46+
onChange={axesTargetHandler}
47+
clearable={false}
48+
/>
49+
</Field>
50+
);
51+
}
1952

2053
return (
2154
<Field {...this.props} center>
@@ -33,4 +66,11 @@ AxesSelector.contextTypes = {
3366
axesTargetHandler: PropTypes.func,
3467
axesOptions: PropTypes.array,
3568
axesTarget: PropTypes.string,
69+
fullLayout: PropTypes.object,
3670
};
71+
72+
AxesSelector.propTypes = {
73+
localize: PropTypes.func,
74+
};
75+
76+
export default localize(AxesSelector);

src/components/fields/AxisCreator.js

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import Dropdown from './Dropdown';
2+
import Info from './Info';
3+
import PropTypes from 'prop-types';
4+
import React, {Component, Fragment} from 'react';
5+
import {EDITOR_ACTIONS} from 'lib/constants';
6+
import Button from '../widgets/Button';
7+
import {PlusIcon} from 'plotly-icons';
8+
import {
9+
connectToContainer,
10+
localize,
11+
traceTypeToAxisType,
12+
getAxisTitle,
13+
axisIdToAxisName,
14+
} from 'lib';
15+
16+
class UnconnectedNewAxisCreator extends Component {
17+
canAddAxis() {
18+
const currentAxisId = this.props.fullContainer[this.props.attr];
19+
const currentTraceIndex = this.props.fullContainer.index;
20+
return this.context.fullData.some(
21+
d => d.index !== currentTraceIndex && d[this.props.attr] === currentAxisId
22+
);
23+
}
24+
25+
updateAxis() {
26+
const {attr, updateContainer} = this.props;
27+
const {onUpdate, fullLayout} = this.context;
28+
29+
updateContainer({
30+
[attr]: attr.charAt(0) + (fullLayout._subplots[attr].length + 1),
31+
});
32+
33+
if (attr === 'yaxis') {
34+
onUpdate({
35+
type: EDITOR_ACTIONS.UPDATE_LAYOUT,
36+
payload: {
37+
update: {
38+
[`${attr + (fullLayout._subplots[attr].length + 1)}.side`]: 'right',
39+
[`${attr +
40+
(fullLayout._subplots[attr].length + 1)}.overlaying`]: 'y',
41+
},
42+
},
43+
});
44+
}
45+
46+
if (attr === 'xaxis') {
47+
onUpdate({
48+
type: EDITOR_ACTIONS.UPDATE_LAYOUT,
49+
payload: {
50+
update: {
51+
[`${attr + (fullLayout._subplots[attr].length + 1)}.side`]: 'top',
52+
[`${attr +
53+
(fullLayout._subplots[attr].length + 1)}.overlaying`]: 'x',
54+
},
55+
},
56+
});
57+
}
58+
}
59+
60+
recalcAxes(update) {
61+
const currentAxisId = this.props.fullContainer[this.props.attr];
62+
63+
// When we select another axis, make sure no unused axes are left:
64+
// does any other trace have this axisID? If so, nothing needs to change
65+
if (
66+
this.context.fullData.some(
67+
t =>
68+
t[this.props.attr] === currentAxisId &&
69+
t.index !== this.props.fullContainer.index
70+
)
71+
) {
72+
this.props.updateContainer({[this.props.attr]: update});
73+
return;
74+
}
75+
76+
// if not, send action to readjust axis references in trace data and layout
77+
const tracesToAdjust = this.context.fullData.filter(
78+
trace =>
79+
Number(trace[this.props.attr].slice(1)) > Number(currentAxisId.slice(1))
80+
);
81+
82+
this.context.onUpdate({
83+
type: EDITOR_ACTIONS.UPDATE_AXIS_REFERENCES,
84+
payload: {tracesToAdjust, attrToAdjust: this.props.attr},
85+
});
86+
87+
this.props.updateContainer({[this.props.attr]: update});
88+
}
89+
90+
render() {
91+
const icon = <PlusIcon />;
92+
const extraComponent = this.canAddAxis() ? (
93+
<Button variant="no-text" icon={icon} onClick={() => this.updateAxis()} />
94+
) : (
95+
<Button variant="no-text--disabled" icon={icon} onClick={() => {}} />
96+
);
97+
98+
return (
99+
<Dropdown
100+
label={this.props.label}
101+
attr={this.props.attr}
102+
clearable={false}
103+
options={this.props.options}
104+
updatePlot={u => this.recalcAxes(u)}
105+
extraComponent={extraComponent}
106+
/>
107+
);
108+
}
109+
}
110+
111+
UnconnectedNewAxisCreator.propTypes = {
112+
attr: PropTypes.string,
113+
label: PropTypes.string,
114+
options: PropTypes.array,
115+
canAddAxis: PropTypes.bool,
116+
localize: PropTypes.func,
117+
container: PropTypes.object,
118+
fullContainer: PropTypes.object,
119+
updateContainer: PropTypes.func,
120+
};
121+
122+
UnconnectedNewAxisCreator.contextTypes = {
123+
fullLayout: PropTypes.object,
124+
data: PropTypes.array,
125+
fullData: PropTypes.array,
126+
onUpdate: PropTypes.func,
127+
};
128+
129+
const ConnectedNewAxisCreator = connectToContainer(UnconnectedNewAxisCreator);
130+
131+
class AxisCreator extends Component {
132+
render() {
133+
const isFirstTraceOfType =
134+
this.context.data.filter(d => d.type === this.props.container.type)
135+
.length === 1;
136+
137+
if (isFirstTraceOfType) {
138+
return null;
139+
}
140+
141+
const {localize: _} = this.props;
142+
const {fullLayout} = this.context;
143+
const axisType = traceTypeToAxisType(this.props.container.type);
144+
const controls = [];
145+
146+
function getOptions(axisType) {
147+
return fullLayout._subplots[axisType].map(axisId => ({
148+
label: getAxisTitle(fullLayout[axisIdToAxisName(axisId)]),
149+
value: axisId,
150+
}));
151+
}
152+
153+
// for the moment only cartesian subplots are supported
154+
if (axisType === 'cartesian') {
155+
['xaxis', 'yaxis'].forEach((type, index) => {
156+
controls.push(
157+
<ConnectedNewAxisCreator
158+
key={index}
159+
attr={type}
160+
label={type.charAt(0).toUpperCase() + ' Axis'}
161+
options={getOptions(type)}
162+
localize={_}
163+
/>
164+
);
165+
});
166+
}
167+
168+
return (
169+
<Fragment>
170+
{controls}
171+
<Info>
172+
{_('You can style and position your axes in the Style > Axes Panel')}
173+
</Info>
174+
</Fragment>
175+
);
176+
}
177+
}
178+
179+
AxisCreator.propTypes = {
180+
localize: PropTypes.func,
181+
container: PropTypes.object,
182+
fullContainer: PropTypes.object,
183+
};
184+
185+
AxisCreator.plotly_editor_traits = {
186+
is_axis_creator: true,
187+
};
188+
189+
AxisCreator.contextTypes = {
190+
data: PropTypes.array,
191+
fullData: PropTypes.array,
192+
fullLayout: PropTypes.object,
193+
};
194+
195+
export default localize(connectToContainer(AxisCreator));

0 commit comments

Comments
 (0)