diff --git a/package.json b/package.json index d66f1fd9a..d044779e0 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,10 @@ "react-dropzone": "^4.2.9", "react-plotly.js": "^2.2.0", "react-rangeslider": "^2.2.0", + "react-resizable-rotatable-draggable": "^0.1.8", "react-select": "^1.2.0", "react-tabs": "^2.2.1", + "styled-components": "^3.3.3", "tinycolor2": "^1.4.1" }, "devDependencies": { diff --git a/src/DefaultEditor.js b/src/DefaultEditor.js index fcb10bc15..9d12746c8 100644 --- a/src/DefaultEditor.js +++ b/src/DefaultEditor.js @@ -4,6 +4,7 @@ import {PanelMenuWrapper} from './components'; import { GraphCreatePanel, GraphTransformsPanel, + GraphSubplotsPanel, StyleLayoutPanel, StyleAxesPanel, StyleLegendPanel, @@ -26,6 +27,7 @@ class DefaultEditor extends Component { {logo ? logo : null} + diff --git a/src/EditorControls.js b/src/EditorControls.js index 3ddc20af5..4a6e47efc 100644 --- a/src/EditorControls.js +++ b/src/EditorControls.js @@ -149,6 +149,9 @@ class EditorControls extends Component { if (this.props.beforeDeleteTrace) { this.props.beforeDeleteTrace(payload); } + + shamefullyAdjustAxisRef(graphDiv, payload); + graphDiv.data.splice(payload.traceIndexes[0], 1); if (this.props.afterDeleteTrace) { this.props.afterDeleteTrace(payload); diff --git a/src/components/containers/PlotlyPanel.js b/src/components/containers/PlotlyPanel.js index d40030578..a89f81743 100644 --- a/src/components/containers/PlotlyPanel.js +++ b/src/components/containers/PlotlyPanel.js @@ -68,7 +68,10 @@ export class Panel extends Component { let numFolds = 0; React.Children.forEach(this.props.children, child => { - if ((child.type.plotly_editor_traits || {}).foldable) { + if ( + ((child && child.type && child.type.plotly_editor_traits) || {}) + .foldable + ) { numFolds++; } }); @@ -100,7 +103,10 @@ export class Panel extends Component { const newChildren = React.Children.map( this.props.children, (child, index) => { - if ((child.type.plotly_editor_traits || {}).foldable) { + if ( + ((child && child.type && child.type.plotly_editor_traits) || {}) + .foldable + ) { return cloneElement(child, { key: index, folded: individualFoldStates[index] || false, diff --git a/src/components/containers/SubplotAccordion.js b/src/components/containers/SubplotAccordion.js new file mode 100644 index 000000000..4612e4448 --- /dev/null +++ b/src/components/containers/SubplotAccordion.js @@ -0,0 +1,154 @@ +import PlotlyFold from './PlotlyFold'; +import TraceRequiredPanel from './TraceRequiredPanel'; +import PropTypes from 'prop-types'; +import React, {Component} from 'react'; +import { + connectTraceToPlot, + connectCartesianSubplotToLayout, + connectNonCartesianSubplotToLayout, +} from 'lib'; +import {TRACE_TO_AXIS, SUBPLOT_TO_ATTR} from 'lib/constants'; + +const TraceFold = connectTraceToPlot(PlotlyFold); +const NonCartesianSubplotFold = connectNonCartesianSubplotToLayout(PlotlyFold); +const CartesianSubplotFold = connectCartesianSubplotToLayout(PlotlyFold); + +class SubplotAccordion extends Component { + render() { + const {data = [], layout = {}} = this.context; + const {children, messageIfEmptyFold} = this.props; + const subplotFolds = []; + + const allCartesianAxisCombinations = data.reduce((acc, curVal, inx) => { + if (TRACE_TO_AXIS.cartesian.some(c => c === curVal.type)) { + const xaxis = 'xaxis' + (curVal.xaxis ? curVal.xaxis.substring(1) : ''); + const yaxis = 'yaxis' + (curVal.yaxis ? curVal.yaxis.substring(1) : ''); + + const existingComboIndex = acc.findIndex( + t => t.xaxis === xaxis && t.yaxis === yaxis + ); + if (existingComboIndex === -1) { + acc.push({ + xaxis: xaxis, + yaxis: yaxis, + xaxisName: curVal.xaxis || 'x1', + yaxisName: curVal.yaxis || 'y1', + index: [inx], + }); + } else { + acc[existingComboIndex].index.push(inx); + } + } + return acc; + }, []); + + allCartesianAxisCombinations.forEach( + d => + (subplotFolds[d.index[0]] = ( + + {children} + + )) + ); + + // For each key in layout, find all traces that belong to this subplot. + // E.g. if layout attr is 'ternary', find all traces that are of type + // that has subplot ternary, if layout attr is 'ternary2', find traces + // of right type that have attr 'subplot': 'ternary' in their data. + + /** + Example: + { + "data": [ + { + "type": "scatterternary", + "mode": "markers", + }, + { + "type": "scatterternary", + "mode": "markers", + "subplot": "ternary2" + } + ], + "layout": { + "ternary": {}, + "ternary2": {}, + }, + } + */ + + Object.keys(layout).forEach(layoutKey => { + const traceIndexes = []; + if ( + ['geo', 'mapbox', 'polar', 'gl3d', 'ternary'].some(subplotType => { + const trIndex = + SUBPLOT_TO_ATTR[subplotType].layout === layoutKey + ? data.findIndex(trace => + TRACE_TO_AXIS[subplotType].some(tt => tt === trace.type) + ) + : data.findIndex( + trace => + trace[SUBPLOT_TO_ATTR[subplotType].data] === layoutKey + ); + if (trIndex !== -1) { + traceIndexes.push(trIndex); + } + return layoutKey.startsWith(SUBPLOT_TO_ATTR[subplotType].layout); + }) + ) { + subplotFolds[traceIndexes[0]] = ( + + {children} + + ); + } + }); + + data.forEach((d, i) => { + if ((d.type === 'pie' && d.values) || d.type === 'table') { + subplotFolds[i] = ( + + {children} + + ); + } + }); + + return {subplotFolds}; + } +} + +SubplotAccordion.contextTypes = { + fullData: PropTypes.array, + data: PropTypes.array, + layout: PropTypes.object, + localize: PropTypes.func, +}; + +SubplotAccordion.propTypes = { + children: PropTypes.node, + messageIfEmptyFold: PropTypes.string, +}; + +export default SubplotAccordion; diff --git a/src/components/containers/__tests__/Fold-test.js b/src/components/containers/__tests__/Fold-test.js index 0e72fea0c..c846e8f4f 100644 --- a/src/components/containers/__tests__/Fold-test.js +++ b/src/components/containers/__tests__/Fold-test.js @@ -46,6 +46,10 @@ describe('', () => { .simulate('click'); const payload = beforeDeleteTrace.mock.calls[0][0]; - expect(payload).toEqual({traceIndexes: [0]}); + expect(payload).toEqual({ + axesToBeGarbageCollected: [], + subplotToBeGarbageCollected: null, + traceIndexes: [0], + }); }); }); diff --git a/src/components/containers/index.js b/src/components/containers/index.js index 53ede3124..926611282 100644 --- a/src/components/containers/index.js +++ b/src/components/containers/index.js @@ -9,6 +9,7 @@ import PlotlyFold, {Fold} from './PlotlyFold'; import MenuPanel from './MenuPanel'; import PlotlyPanel, {Panel} from './PlotlyPanel'; import PlotlySection, {Section} from './PlotlySection'; +import SubplotAccordion from './SubplotAccordion'; import TraceAccordion from './TraceAccordion'; import TransformAccordion from './TransformAccordion'; import TraceMarkerSection from './TraceMarkerSection'; @@ -32,6 +33,7 @@ export { Panel, PlotlySection, Section, + SubplotAccordion, TraceAccordion, TransformAccordion, TraceMarkerSection, diff --git a/src/components/fields/AxesCreator.js b/src/components/fields/AxesCreator.js index 73f128780..cb4e294bc 100644 --- a/src/components/fields/AxesCreator.js +++ b/src/components/fields/AxesCreator.js @@ -1,7 +1,7 @@ import Dropdown from './Dropdown'; import Info from './Info'; import PropTypes from 'prop-types'; -import React, {Component, Fragment} from 'react'; +import React, {Component} from 'react'; import {EDITOR_ACTIONS} from 'lib/constants'; import Button from '../widgets/Button'; import {PlusIcon} from 'plotly-icons'; @@ -11,6 +11,7 @@ import { getAxisTitle, axisIdToAxisName, } from 'lib'; +import {PlotlySection} from 'components'; class UnconnectedAxisCreator extends Component { canAddAxis() { @@ -21,62 +22,61 @@ class UnconnectedAxisCreator extends Component { ); } - updateAxis() { + addAndUpdateAxis() { const {attr, updateContainer} = this.props; - const {onUpdate, fullLayout} = this.context; + const { + onUpdate, + fullLayout: {_subplots: subplots}, + } = this.context; + const lastAxisNumber = + Number(subplots[attr][subplots[attr].length - 1].charAt(1)) || 1; updateContainer({ - [attr]: attr.charAt(0) + (fullLayout._subplots[attr].length + 1), + [attr]: attr.charAt(0) + (lastAxisNumber + 1), }); + let side = null; 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', - }, - }, - }); + side = 'right'; + } else if (attr === 'xaxis') { + side = 'top'; } - 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', - }, + onUpdate({ + type: EDITOR_ACTIONS.UPDATE_LAYOUT, + payload: { + update: { + [`${attr + (lastAxisNumber + 1)}.side`]: side, + [`${attr + (lastAxisNumber + 1)}.overlaying`]: !( + attr === 'yaxis' || attr === 'xaxis' + ) + ? null + : subplots[attr][subplots[attr].length - 1], }, - }); - } + }, + }); } - recalcAxes(update) { + updateAxis(update) { const currentAxisId = this.props.fullContainer[this.props.attr]; + const axesToBeGarbageCollected = []; // When we select another axis, make sure no unused axes are left - const tracesNeedingAxisAdjustment = this.context.fullData.some( - t => - t[this.props.attr] === currentAxisId && - t.index !== this.props.fullContainer.index - ) - ? null - : this.context.fullData.filter( - trace => - Number(trace[this.props.attr].slice(1)) > - Number(currentAxisId.slice(1)) - ); + if ( + currentAxisId !== update && + !this.context.fullData.some( + trace => + trace[this.props.attr] === currentAxisId && + trace.index !== this.props.fullContainer.index + ) + ) { + axesToBeGarbageCollected.push(currentAxisId); + } this.context.onUpdate({ type: EDITOR_ACTIONS.UPDATE_TRACES, payload: { - tracesNeedingAxisAdjustment, - axisAttrToAdjust: this.props.attr, + axesToBeGarbageCollected, update: {[this.props.attr]: update}, traceIndexes: [this.props.fullContainer.index], }, @@ -86,7 +86,11 @@ class UnconnectedAxisCreator extends Component { render() { const icon = ; const extraComponent = this.canAddAxis() ? ( -