Skip to content

Subplots2 #357

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 23 commits into from
Mar 1, 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
29 changes: 26 additions & 3 deletions src/PlotlyEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import DefaultEditor from './DefaultEditor';
import PropTypes from 'prop-types';
import React, {Component} from 'react';
import {bem} from './lib';
import {noShame, maybeClearAxisTypes} from './shame';
import {maybeClearAxisTypes} from './shame';
import {EDITOR_ACTIONS} from './lib/constants';
import isNumeric from 'fast-isnumeric';
import nestedProperty from 'plotly.js/src/lib/nested_property';
Expand All @@ -11,8 +11,6 @@ class PlotlyEditor extends Component {
constructor(props, context) {
super(props, context);

noShame({plotly: this.props.plotly});

// we only need to compute this once.
if (this.props.plotly) {
this.plotSchema = this.props.plotly.PlotSchema.get();
Expand Down Expand Up @@ -102,6 +100,31 @@ class PlotlyEditor extends Component {
}
break;

case EDITOR_ACTIONS.UPDATE_AXIS_REFERENCES:
payload.tracesToAdjust.forEach(trace => {
const axis = trace[payload.attrToAdjust].charAt(0);
// n.b: currentAxisIdNumber will never be 0, i.e. Number('x'.slice(1)),
// because payload.tracesToAdjust is a filter of all traces that have
// an axis ID above the one of the axis ID we deprecated
const currentAxisIdNumber = Number(
trace[payload.attrToAdjust].slice(1)
);
const adjustedAxisIdNumber = currentAxisIdNumber - 1;

const currentAxisLayoutProperties = {
...graphDiv.layout[payload.attrToAdjust + currentAxisIdNumber],
};

graphDiv.data[trace.index][payload.attrToAdjust] =
// for cases when we're adjusting x2 => x, so that it becomes x not x1
adjustedAxisIdNumber === 1 ? axis : axis + adjustedAxisIdNumber;

graphDiv.layout[
payload.attrToAdjust + adjustedAxisIdNumber
] = currentAxisLayoutProperties;
});
break;

case EDITOR_ACTIONS.ADD_TRACE:
if (this.props.beforeAddTrace) {
this.props.beforeAddTrace(payload);
Expand Down
4 changes: 2 additions & 2 deletions src/components/containers/ImageAccordion.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ class ImageAccordion extends Component {

const content =
images.length &&
images.map((ann, i) => (
<ImageFold key={i} imageIndex={i} name={ann.text} canDelete={canAdd}>
images.map((img, i) => (
<ImageFold key={i} imageIndex={i} name={img.text} canDelete={canAdd}>
Copy link
Collaborator

Choose a reason for hiding this comment

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

images don't have .text do they? Perhaps make a name from .source? Not sure what to do about shapes... index and type perhaps?

Copy link
Contributor

Choose a reason for hiding this comment

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

hehe so this change is @VeraZab cleaning up my copy-paste work from a previous PR... When I created the Images panel I'd based it off of the Notes panel but not renamed ann to img (and ditto Shapes). So any issues with this code should probably go in a separate issue that I can handle ;)

{children}
</ImageFold>
));
Expand Down
24 changes: 21 additions & 3 deletions src/components/containers/Section.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import Info from '../fields/Info';
import React, {Component, cloneElement} from 'react';
import PropTypes from 'prop-types';
import {
containerConnectedContextTypes,
localize,
unpackPlotProps,
traceTypeToAxisType,
} from '../../lib';

class Section extends Component {
Expand Down Expand Up @@ -36,7 +36,23 @@ class Section extends Component {
return null;
}

if (child.props.attr) {
if ((child.type.plotly_editor_traits || {}).is_axis_creator) {
const {data, fullContainer} = this.context;

// for now, only allowing for cartesian chart types
if (
data.length > 1 &&
traceTypeToAxisType(data[fullContainer.index].type) === 'cartesian'
) {
this.sectionVisible = true;
return cloneElement(child, {
isVisible: true,
container: this.context.container,
fullContainer: this.context.fullContainer,
});
}
this.sectionVisible = false;
} else if (child.props.attr) {
let plotProps;
if (child.type.supplyPlotProps) {
plotProps = child.type.supplyPlotProps(child.props, nextContext);
Expand All @@ -51,7 +67,9 @@ class Section extends Component {
// it will see plotProps and skip recomputing them.
this.sectionVisible = this.sectionVisible || plotProps.isVisible;
return cloneElement(child, {plotProps});
} else if (child.type !== Info) {
} else if (
!(child.type.plotly_editor_traits || {}).no_visibility_forcing
) {
// custom UI other than Info forces section visibility.
this.sectionVisible = true;
}
Expand Down
4 changes: 2 additions & 2 deletions src/components/containers/ShapeAccordion.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ class ShapeAccordion extends Component {

const content =
shapes.length &&
shapes.map((ann, i) => (
<ShapeFold key={i} shapeIndex={i} name={ann.text} canDelete={canAdd}>
shapes.map((shp, i) => (
<ShapeFold key={i} shapeIndex={i} name={shp.text} canDelete={canAdd}>
{children}
</ShapeFold>
));
Expand Down
42 changes: 41 additions & 1 deletion src/components/fields/AxesSelector.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import Field from './Field';
import PropTypes from 'prop-types';
import Dropdown from '../widgets/Dropdown';
import RadioBlocks from '../widgets/RadioBlocks';
import React, {Component} from 'react';
import {localize} from 'lib';

export default class AxesSelector extends Component {
class AxesSelector extends Component {
constructor(props, context) {
super(props, context);

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

render() {
const {axesTargetHandler, axesOptions, axesTarget} = this.context;
const {localize: _} = this.props;
const hasSecondaryAxis =
axesOptions &&
axesOptions.some(option => {
return (
option.axisGroup &&
this.context.fullLayout._subplots[option.axisGroup].length > 1
);
});

if (hasSecondaryAxis) {
return (
<Field {...this.props} label={_('Axis to Style')}>
<Dropdown
options={axesOptions.map(option => {
if (option.value !== 'allaxes') {
return {
label: option.title,
value: option.value,
};
}

return option;
})}
value={axesTarget}
onChange={axesTargetHandler}
clearable={false}
/>
</Field>
);
}

return (
<Field {...this.props} center>
Expand All @@ -33,4 +66,11 @@ AxesSelector.contextTypes = {
axesTargetHandler: PropTypes.func,
axesOptions: PropTypes.array,
axesTarget: PropTypes.string,
fullLayout: PropTypes.object,
};

AxesSelector.propTypes = {
localize: PropTypes.func,
};

export default localize(AxesSelector);
195 changes: 195 additions & 0 deletions src/components/fields/AxisCreator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import Dropdown from './Dropdown';
import Info from './Info';
import PropTypes from 'prop-types';
import React, {Component, Fragment} from 'react';
import {EDITOR_ACTIONS} from 'lib/constants';
import Button from '../widgets/Button';
import {PlusIcon} from 'plotly-icons';
import {
connectToContainer,
localize,
traceTypeToAxisType,
getAxisTitle,
axisIdToAxisName,
} from 'lib';

class UnconnectedNewAxisCreator extends Component {
canAddAxis() {
const currentAxisId = this.props.fullContainer[this.props.attr];
const currentTraceIndex = this.props.fullContainer.index;
return this.context.fullData.some(
d => d.index !== currentTraceIndex && d[this.props.attr] === currentAxisId
);
}

updateAxis() {
const {attr, updateContainer} = this.props;
const {onUpdate, fullLayout} = this.context;

updateContainer({
[attr]: attr.charAt(0) + (fullLayout._subplots[attr].length + 1),
});

if (attr === 'yaxis') {
onUpdate({
type: EDITOR_ACTIONS.UPDATE_LAYOUT,
payload: {
update: {
[`${attr + (fullLayout._subplots[attr].length + 1)}.side`]: 'right',
[`${attr +
(fullLayout._subplots[attr].length + 1)}.overlaying`]: 'y',
},
},
});
}

if (attr === 'xaxis') {
onUpdate({
type: EDITOR_ACTIONS.UPDATE_LAYOUT,
payload: {
update: {
[`${attr + (fullLayout._subplots[attr].length + 1)}.side`]: 'top',
[`${attr +
(fullLayout._subplots[attr].length + 1)}.overlaying`]: 'x',
},
},
});
}
}

recalcAxes(update) {
const currentAxisId = this.props.fullContainer[this.props.attr];

// When we select another axis, make sure no unused axes are left:
// does any other trace have this axisID? If so, nothing needs to change
if (
this.context.fullData.some(
t =>
t[this.props.attr] === currentAxisId &&
t.index !== this.props.fullContainer.index
)
) {
this.props.updateContainer({[this.props.attr]: update});
return;
}

// if not, send action to readjust axis references in trace data and layout
const tracesToAdjust = this.context.fullData.filter(
trace =>
Number(trace[this.props.attr].slice(1)) > Number(currentAxisId.slice(1))
);

this.context.onUpdate({
type: EDITOR_ACTIONS.UPDATE_AXIS_REFERENCES,
payload: {tracesToAdjust, attrToAdjust: this.props.attr},
});

this.props.updateContainer({[this.props.attr]: update});
}

render() {
const icon = <PlusIcon />;
const extraComponent = this.canAddAxis() ? (
<Button variant="no-text" icon={icon} onClick={() => this.updateAxis()} />
) : (
<Button variant="no-text--disabled" icon={icon} onClick={() => {}} />
);

return (
<Dropdown
label={this.props.label}
attr={this.props.attr}
clearable={false}
options={this.props.options}
updatePlot={u => this.recalcAxes(u)}
extraComponent={extraComponent}
/>
);
}
}

UnconnectedNewAxisCreator.propTypes = {
attr: PropTypes.string,
label: PropTypes.string,
options: PropTypes.array,
canAddAxis: PropTypes.bool,
localize: PropTypes.func,
container: PropTypes.object,
fullContainer: PropTypes.object,
updateContainer: PropTypes.func,
};

UnconnectedNewAxisCreator.contextTypes = {
fullLayout: PropTypes.object,
data: PropTypes.array,
fullData: PropTypes.array,
onUpdate: PropTypes.func,
};

const ConnectedNewAxisCreator = connectToContainer(UnconnectedNewAxisCreator);

class AxisCreator extends Component {
render() {
const isFirstTraceOfType =
this.context.data.filter(d => d.type === this.props.container.type)
.length === 1;

if (isFirstTraceOfType) {
return null;
}

const {localize: _} = this.props;
const {fullLayout} = this.context;
const axisType = traceTypeToAxisType(this.props.container.type);
const controls = [];

function getOptions(axisType) {
return fullLayout._subplots[axisType].map(axisId => ({
label: getAxisTitle(fullLayout[axisIdToAxisName(axisId)]),
value: axisId,
}));
}

// for the moment only cartesian subplots are supported
if (axisType === 'cartesian') {
['xaxis', 'yaxis'].forEach((type, index) => {
controls.push(
<ConnectedNewAxisCreator
key={index}
attr={type}
label={type.charAt(0).toUpperCase() + ' Axis'}
options={getOptions(type)}
localize={_}
/>
);
});
}

return (
<Fragment>
{controls}
<Info>
{_('You can style and position your axes in the Style > Axes Panel')}
</Info>
</Fragment>
);
}
}

AxisCreator.propTypes = {
localize: PropTypes.func,
container: PropTypes.object,
fullContainer: PropTypes.object,
};

AxisCreator.plotly_editor_traits = {
is_axis_creator: true,
};

AxisCreator.contextTypes = {
data: PropTypes.array,
fullData: PropTypes.array,
fullLayout: PropTypes.object,
};

export default localize(connectToContainer(AxisCreator));
Loading