diff --git a/src/PlotlyEditor.js b/src/PlotlyEditor.js index 7fc1c4f9f..f4df4c4e3 100644 --- a/src/PlotlyEditor.js +++ b/src/PlotlyEditor.js @@ -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'; @@ -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(); @@ -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); diff --git a/src/components/containers/ImageAccordion.js b/src/components/containers/ImageAccordion.js index 850db31d8..c3c963318 100644 --- a/src/components/containers/ImageAccordion.js +++ b/src/components/containers/ImageAccordion.js @@ -13,8 +13,8 @@ class ImageAccordion extends Component { const content = images.length && - images.map((ann, i) => ( - + images.map((img, i) => ( + {children} )); diff --git a/src/components/containers/Section.js b/src/components/containers/Section.js index 82a8ef22b..ff0f28512 100644 --- a/src/components/containers/Section.js +++ b/src/components/containers/Section.js @@ -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 { @@ -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); @@ -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; } diff --git a/src/components/containers/ShapeAccordion.js b/src/components/containers/ShapeAccordion.js index d3f872379..43f8db002 100644 --- a/src/components/containers/ShapeAccordion.js +++ b/src/components/containers/ShapeAccordion.js @@ -13,8 +13,8 @@ class ShapeAccordion extends Component { const content = shapes.length && - shapes.map((ann, i) => ( - + shapes.map((shp, i) => ( + {children} )); diff --git a/src/components/fields/AxesSelector.js b/src/components/fields/AxesSelector.js index 20af4504c..edaadb564 100644 --- a/src/components/fields/AxesSelector.js +++ b/src/components/fields/AxesSelector.js @@ -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); @@ -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 ( + + { + if (option.value !== 'allaxes') { + return { + label: option.title, + value: option.value, + }; + } + + return option; + })} + value={axesTarget} + onChange={axesTargetHandler} + clearable={false} + /> + + ); + } return ( @@ -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); diff --git a/src/components/fields/AxisCreator.js b/src/components/fields/AxisCreator.js new file mode 100644 index 000000000..58cda7a14 --- /dev/null +++ b/src/components/fields/AxisCreator.js @@ -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 = ; + const extraComponent = this.canAddAxis() ? ( +