From 76c22fdffd61cc4dd4152865ebee4e1d15173842 Mon Sep 17 00:00:00 2001 From: dmt0 Date: Wed, 25 Jul 2018 20:05:51 -0400 Subject: [PATCH 01/13] subplots panel initial --- src/DefaultEditor.js | 2 + src/components/containers/PlotlyPanel.js | 10 +- src/components/containers/SubplotAccordion.js | 129 ++++++++++++++++ src/components/containers/index.js | 2 + src/components/fields/derived.js | 4 +- src/components/index.js | 2 + src/default_panels/GraphSubplotsPanel.js | 22 +++ src/default_panels/StyleAxesPanel.js | 6 +- src/default_panels/index.js | 2 + src/index.js | 6 +- src/lib/connectCartesianSubplotToLayout.js | 139 ++++++++++++++++++ src/lib/connectNonCartesianSubplotToLayout.js | 124 ++++++++++++++++ src/lib/constants.js | 9 ++ src/lib/index.js | 4 + 14 files changed, 455 insertions(+), 6 deletions(-) create mode 100644 src/components/containers/SubplotAccordion.js create mode 100644 src/default_panels/GraphSubplotsPanel.js create mode 100644 src/lib/connectCartesianSubplotToLayout.js create mode 100644 src/lib/connectNonCartesianSubplotToLayout.js 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/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..4a7c98ca9 --- /dev/null +++ b/src/components/containers/SubplotAccordion.js @@ -0,0 +1,129 @@ +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, AXIS_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} + + )) + ); + + Object.keys(layout).forEach(layoutKey => { + const traceIndexes = []; + if ( + ['geo', 'mapbox', 'polar', 'gl3d', 'ternary'].some(traceType => { + const trIndex = + (traceType === 'gl3d' ? 'scene' : traceType) === layoutKey + ? data.findIndex(trace => + TRACE_TO_AXIS[traceType].some(tt => tt === trace.type) + ) + : data.findIndex( + trace => trace[AXIS_TO_ATTR[traceType]] === layoutKey + ); + if (trIndex !== -1) { + traceIndexes.push(trIndex); + } + return layoutKey.startsWith( + traceType === 'gl3d' ? 'scene' : traceType + ); + }) + ) { + 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/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/derived.js b/src/components/fields/derived.js index d46047ce6..90122b155 100644 --- a/src/components/fields/derived.js +++ b/src/components/fields/derived.js @@ -235,8 +235,8 @@ UnconnectedNumericFraction.defaultProps = { const numericFractionModifyPlotProps = (props, context, plotProps) => { const {attrMeta, fullValue, updatePlot} = plotProps; - const min = attrMeta.min || 0; - const max = attrMeta.max || 1; + const min = (attrMeta && attrMeta.min) || 0; + const max = (attrMeta && attrMeta.max) || 1; if (isNumeric(fullValue)) { plotProps.fullValue = Math.round(100 * (fullValue - min) / (max - min)); } diff --git a/src/components/index.js b/src/components/index.js index f0c230571..22eb11b60 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -65,6 +65,7 @@ import { PlotlyPanel, PlotlySection, Section, + SubplotAccordion, TraceAccordion, TraceMarkerSection, TraceRequiredPanel, @@ -134,6 +135,7 @@ export { AxesCreator, SymbolSelector, TextEditor, + SubplotAccordion, TraceAccordion, TraceMarkerSection, TraceOrientation, diff --git a/src/default_panels/GraphSubplotsPanel.js b/src/default_panels/GraphSubplotsPanel.js new file mode 100644 index 000000000..504f0b953 --- /dev/null +++ b/src/default_panels/GraphSubplotsPanel.js @@ -0,0 +1,22 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import {NumericFraction, SubplotAccordion} from '../components'; + +const GraphSubplotsPanel = (props, {localize: _}) => ( + + + + + + + + + + +); + +GraphSubplotsPanel.contextTypes = { + localize: PropTypes.func, +}; + +export default GraphSubplotsPanel; diff --git a/src/default_panels/StyleAxesPanel.js b/src/default_panels/StyleAxesPanel.js index 27fd88be4..9b2c68e2c 100644 --- a/src/default_panels/StyleAxesPanel.js +++ b/src/default_panels/StyleAxesPanel.js @@ -87,7 +87,11 @@ class StyleAxesPanel extends Component { name={_('Anchor')} traceTypes={TRACE_TO_AXIS.cartesian} > - + diff --git a/src/default_panels/index.js b/src/default_panels/index.js index 515b8d812..a6aa1ca59 100644 --- a/src/default_panels/index.js +++ b/src/default_panels/index.js @@ -10,6 +10,7 @@ import StyleImagesPanel from './StyleImagesPanel'; import StyleTracesPanel from './StyleTracesPanel'; import StyleColorbarsPanel from './StyleColorbarsPanel'; import StyleUpdateMenusPanel from './StyleUpdateMenusPanel'; +import GraphSubplotsPanel from './GraphSubplotsPanel'; export { GraphCreatePanel, @@ -24,4 +25,5 @@ export { StyleTracesPanel, StyleColorbarsPanel, StyleUpdateMenusPanel, + GraphSubplotsPanel, }; diff --git a/src/index.js b/src/index.js index 1c9ad5961..28d56565e 100644 --- a/src/index.js +++ b/src/index.js @@ -2,6 +2,8 @@ import PlotlyEditor from './PlotlyEditor'; import DefaultEditor from './DefaultEditor'; import EditorControls from './EditorControls'; import { + connectCartesianSubplotToLayout, + connectNonCartesianSubplotToLayout, connectAnnotationToLayout, connectShapeToLayout, connectAggregationToTransform, @@ -97,7 +99,6 @@ export { ArrowSelector, TransformAccordion, AxesFold, - connectAggregationToTransform, AxesRange, NTicks, DTicks, @@ -150,11 +151,14 @@ export { TraceMarkerSection, TraceRequiredPanel, TraceSelector, + connectCartesianSubplotToLayout, + connectNonCartesianSubplotToLayout, connectAnnotationToLayout, connectShapeToLayout, connectImageToLayout, connectAxesToLayout, connectTransformToTrace, + connectAggregationToTransform, connectLayoutToPlot, connectToContainer, connectRangeSelectorToAxis, diff --git a/src/lib/connectCartesianSubplotToLayout.js b/src/lib/connectCartesianSubplotToLayout.js new file mode 100644 index 000000000..fd2b4b6f1 --- /dev/null +++ b/src/lib/connectCartesianSubplotToLayout.js @@ -0,0 +1,139 @@ +import React, {Component} from 'react'; +import PropTypes from 'prop-types'; +import { + getDisplayName, + plotlyTraceToCustomTrace, + renderTraceIcon, +} from '../lib'; + +export default function connectCartesianSubplotToLayout(WrappedComponent) { + class SubplotConnectedComponent extends Component { + constructor(props, context) { + super(props, context); + + this.updateSubplot = this.updateSubplot.bind(this); + this.setLocals(props, context); + } + + componentWillReceiveProps(nextProps, nextContext) { + this.setLocals(nextProps, nextContext); + } + + setLocals(props, context) { + const {xaxis, yaxis, traceIndexes} = props; + const {container, fullContainer, data, fullData} = context; + + this.container = { + xaxis: container[xaxis], + yaxis: container[yaxis], + }; + this.fullContainer = { + xaxis: fullContainer[xaxis], + yaxis: fullContainer[yaxis], + }; + + const trace = traceIndexes.length > 0 ? data[traceIndexes[0]] : {}; + let fullTrace = {}; + for (let i = 0; i < fullData.length; i++) { + if (traceIndexes[0] === fullData[i]._fullInput.index) { + /* + * Fit transforms are custom transforms in our custom plotly.js bundle, + * they are different from others as they create an extra trace in the + * data array. When plotly.js runs supplyTraceDefaults (before the + * transforms code executes) it stores the result in _fullInput, + * so that we have a reference to what the original, corrected input was. + * Then the transform code runs, our figure changes accordingly, but + * we're still able to use the original input as it's in _fullInput. + * This is the desired behaviour for our transforms usually, + * but it is not useful for fits, as the transform code adds some styles + * that are useful for the trace, so really for fits we'd like to read + * from _fullData, not _fullInput. Here we're setting _fullInput to + * _fullData as that is where the rest of our code expects to find its + * values. + */ + if ( + trace.transforms && + trace.transforms.every(t => t.type === 'fit') + ) { + fullData[i]._fullInput = fullData[i]; + } + + fullTrace = fullData[i]._fullInput; + + break; + } + } + + if (trace && fullTrace) { + this.icon = renderTraceIcon(plotlyTraceToCustomTrace(trace)); + this.name = fullTrace.name; + } + } + + getChildContext() { + return { + getValObject: attr => + !this.context.getValObject + ? null + : this.context.getValObject( + attr + .replace('xaxis', this.props.xaxis) + .replace('yaxis', this.props.yaxis) + ), + updateContainer: this.updateSubplot, + deleteContainer: this.deleteSubplot, + container: this.container, + fullContainer: this.fullContainer, + }; + } + + updateSubplot(update) { + const newUpdate = {}; + for (const key in update) { + const newKey = key + .replace('xaxis', this.props.xaxis) + .replace('yaxis', this.props.yaxis); + newUpdate[newKey] = update[key]; + } + this.context.updateContainer(newUpdate); + } + + render() { + return ( + + ); + } + } + + SubplotConnectedComponent.displayName = `SubplotConnected${getDisplayName( + WrappedComponent + )}`; + + SubplotConnectedComponent.propTypes = { + xaxis: PropTypes.string.isRequired, + yaxis: PropTypes.string.isRequired, + }; + + SubplotConnectedComponent.contextTypes = { + container: PropTypes.object, + fullContainer: PropTypes.object, + data: PropTypes.array, + fullData: PropTypes.array, + onUpdate: PropTypes.func, + updateContainer: PropTypes.func, + getValObject: PropTypes.func, + }; + + SubplotConnectedComponent.childContextTypes = { + updateContainer: PropTypes.func, + deleteContainer: PropTypes.func, + container: PropTypes.object, + fullContainer: PropTypes.object, + getValObject: PropTypes.func, + }; + + const {plotly_editor_traits} = WrappedComponent; + SubplotConnectedComponent.plotly_editor_traits = plotly_editor_traits; + + return SubplotConnectedComponent; +} diff --git a/src/lib/connectNonCartesianSubplotToLayout.js b/src/lib/connectNonCartesianSubplotToLayout.js new file mode 100644 index 000000000..738b60b6c --- /dev/null +++ b/src/lib/connectNonCartesianSubplotToLayout.js @@ -0,0 +1,124 @@ +import React, {Component} from 'react'; +import PropTypes from 'prop-types'; +import { + getDisplayName, + plotlyTraceToCustomTrace, + renderTraceIcon, +} from '../lib'; + +export default function connectNonCartesianSubplotToLayout(WrappedComponent) { + class SubplotConnectedComponent extends Component { + constructor(props, context) { + super(props, context); + + this.updateSubplot = this.updateSubplot.bind(this); + this.setLocals(props, context); + } + + componentWillReceiveProps(nextProps, nextContext) { + this.setLocals(nextProps, nextContext); + } + + setLocals(props, context) { + const {subplot, traceIndexes} = props; + const {container, fullContainer, data, fullData} = context; + + this.container = container[subplot] || {}; + this.fullContainer = fullContainer[subplot] || {}; + + const trace = traceIndexes.length > 0 ? data[traceIndexes[0]] : {}; + let fullTrace = {}; + for (let i = 0; i < fullData.length; i++) { + if (traceIndexes[0] === fullData[i]._fullInput.index) { + /* + * Fit transforms are custom transforms in our custom plotly.js bundle, + * they are different from others as they create an extra trace in the + * data array. When plotly.js runs supplyTraceDefaults (before the + * transforms code executes) it stores the result in _fullInput, + * so that we have a reference to what the original, corrected input was. + * Then the transform code runs, our figure changes accordingly, but + * we're still able to use the original input as it's in _fullInput. + * This is the desired behaviour for our transforms usually, + * but it is not useful for fits, as the transform code adds some styles + * that are useful for the trace, so really for fits we'd like to read + * from _fullData, not _fullInput. Here we're setting _fullInput to + * _fullData as that is where the rest of our code expects to find its + * values. + */ + if ( + trace.transforms && + trace.transforms.every(t => t.type === 'fit') + ) { + fullData[i]._fullInput = fullData[i]; + } + + fullTrace = fullData[i]._fullInput; + + break; + } + } + + if (trace && fullTrace) { + this.icon = renderTraceIcon(plotlyTraceToCustomTrace(trace)); + this.name = fullTrace.name; + } + } + + getChildContext() { + return { + getValObject: attr => + !this.context.getValObject + ? null + : this.context.getValObject(`${this.props.subplot}.${attr}`), + updateContainer: this.updateSubplot, + container: this.container, + fullContainer: this.fullContainer, + }; + } + + updateSubplot(update) { + const newUpdate = {}; + for (const key in update) { + newUpdate[`${this.props.subplot}.${key}`] = update[key]; + } + this.context.updateContainer(newUpdate); + } + + render() { + return ( + + ); + } + } + + SubplotConnectedComponent.displayName = `SubplotConnected${getDisplayName( + WrappedComponent + )}`; + + SubplotConnectedComponent.propTypes = { + subplot: PropTypes.string.isRequired, + }; + + SubplotConnectedComponent.contextTypes = { + container: PropTypes.object, + fullContainer: PropTypes.object, + data: PropTypes.array, + fullData: PropTypes.array, + onUpdate: PropTypes.func, + updateContainer: PropTypes.func, + getValObject: PropTypes.func, + }; + + SubplotConnectedComponent.childContextTypes = { + updateContainer: PropTypes.func, + deleteContainer: PropTypes.func, + container: PropTypes.object, + fullContainer: PropTypes.object, + getValObject: PropTypes.func, + }; + + const {plotly_editor_traits} = WrappedComponent; + SubplotConnectedComponent.plotly_editor_traits = plotly_editor_traits; + + return SubplotConnectedComponent; +} diff --git a/src/lib/constants.js b/src/lib/constants.js index be1b4812c..f11b7e970 100644 --- a/src/lib/constants.js +++ b/src/lib/constants.js @@ -76,6 +76,15 @@ export const TRACE_TO_AXIS = { polar: ['scatterpolar', 'scatterpolargl'], }; +export const AXIS_TO_ATTR = { + cartesian: ['xaxis', 'yaxis'], + ternary: 'subplot', + gl3d: 'scene', + geo: 'geo', + mapbox: 'subplot', + polar: 'subplot', +}; + export const TRANSFORMS_LIST = ['filter', 'groupby', 'aggregate']; export const COLORS = { diff --git a/src/lib/index.js b/src/lib/index.js index 9402208a2..1a3145cdf 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -1,4 +1,6 @@ import bem from './bem'; +import connectCartesianSubplotToLayout from './connectCartesianSubplotToLayout'; +import connectNonCartesianSubplotToLayout from './connectNonCartesianSubplotToLayout'; import connectAnnotationToLayout from './connectAnnotationToLayout'; import connectShapeToLayout from './connectShapeToLayout'; import connectSliderToLayout from './connectSliderToLayout'; @@ -216,6 +218,8 @@ export { camelCase, pascalCase, clamp, + connectCartesianSubplotToLayout, + connectNonCartesianSubplotToLayout, connectAnnotationToLayout, connectShapeToLayout, connectSliderToLayout, From b1e0e4b6622e81f505b72e7a8844a8e1e58d5b69 Mon Sep 17 00:00:00 2001 From: Nicolas Kruchten Date: Thu, 26 Jul 2018 14:48:19 -0400 Subject: [PATCH 02/13] prototype rectangle positioner --- package.json | 2 + src/components/fields/RectanglePositioner.js | 111 +++++++++++++++++++ src/components/fields/index.js | 3 +- src/components/index.js | 2 + src/default_panels/GraphSubplotsPanel.js | 11 +- src/index.js | 2 + 6 files changed, 125 insertions(+), 6 deletions(-) create mode 100644 src/components/fields/RectanglePositioner.js 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/components/fields/RectanglePositioner.js b/src/components/fields/RectanglePositioner.js new file mode 100644 index 000000000..4552481ca --- /dev/null +++ b/src/components/fields/RectanglePositioner.js @@ -0,0 +1,111 @@ +import Field from './Field'; +import PropTypes from 'prop-types'; +import React, {Component} from 'react'; +import {connectToContainer} from 'lib'; +import {NumericFraction} from './derived'; +import ResizableRect from 'react-resizable-rotatable-draggable'; + +class UnconnectedRectanglePositioner extends Component { + constructor(props, context) { + super(props, context); + this.sendUpdate = this.sendUpdate.bind(this); + } + + sendUpdate({x, y, width, height, fieldWidthPx, fieldHeightPx}) { + const x0 = x / fieldWidthPx; + const x1 = (width + x) / fieldWidthPx; + const y0 = (fieldHeightPx - (height + y)) / fieldHeightPx; + const y1 = (fieldHeightPx - y) / fieldHeightPx; + + if (x0 >= 0 && y0 >= 0 && y1 <= 1 && x1 <= 1) { + this.context.updateContainer({ + 'domain.x[0]': x0, + 'domain.x[1]': x1, + 'domain.y[0]': y0, + 'domain.y[1]': y1, + }); + } + } + + render() { + const {attr} = this.props; + const { + localize: _, + fullContainer: { + domain: {x, y}, + }, + fullLayout: {width: plotWidthPx, height: plotHeightPx}, + } = this.context; + const aspectRatio = 1; //plotHeightPx / plotWidthPx; + const maxWidth = 300; + const fieldWidthPx = aspectRatio > 1 ? maxWidth : maxWidth / aspectRatio; + const fieldHeightPx = aspectRatio < 1 ? maxWidth : maxWidth * aspectRatio; + + const width = fieldWidthPx * (x[1] - x[0]); + const height = fieldHeightPx * (y[1] - y[0]); + const left = fieldWidthPx * x[0]; + const top = fieldHeightPx * (1 - y[1]); + + return ( + +
+ { + this.sendUpdate({ + fieldWidthPx, + fieldHeightPx, + width, + height, + x: left + deltaX, + y: top + deltaY, + }); + }} + onResize={style => { + this.sendUpdate({ + fieldWidthPx, + fieldHeightPx, + width: style.width, + height: style.height, + x: style.left, + y: style.top, + }); + }} + /> +
+ + + + +
+ ); + } +} + +UnconnectedRectanglePositioner.propTypes = { + fullValue: PropTypes.any, + updatePlot: PropTypes.func, + ...Field.propTypes, +}; + +UnconnectedRectanglePositioner.contextTypes = { + localize: PropTypes.func, + updateContainer: PropTypes.func, + fullContainer: PropTypes.object, + fullLayout: PropTypes.object, +}; + +export default connectToContainer(UnconnectedRectanglePositioner); diff --git a/src/components/fields/index.js b/src/components/fields/index.js index 140e2125a..60ac3718e 100644 --- a/src/components/fields/index.js +++ b/src/components/fields/index.js @@ -23,7 +23,7 @@ import MarkerSize from './MarkerSize'; import MarkerColor from './MarkerColor'; import VisibilitySelect from './VisibilitySelect'; import MultiColorPicker from './MultiColorPicker'; - +import RectanglePositioner from './RectanglePositioner'; import { AnnotationArrowRef, AnnotationRef, @@ -95,4 +95,5 @@ export { MarkerColor, MultiColorPicker, VisibilitySelect, + RectanglePositioner, }; diff --git a/src/components/index.js b/src/components/index.js index 22eb11b60..877af571d 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -46,6 +46,7 @@ import { MarkerColor, MultiColorPicker, VisibilitySelect, + RectanglePositioner, } from './fields'; import { @@ -149,4 +150,5 @@ export { MarkerColor, MultiColorPicker, VisibilitySelect, + RectanglePositioner, }; diff --git a/src/default_panels/GraphSubplotsPanel.js b/src/default_panels/GraphSubplotsPanel.js index 504f0b953..7b3096e8e 100644 --- a/src/default_panels/GraphSubplotsPanel.js +++ b/src/default_panels/GraphSubplotsPanel.js @@ -1,13 +1,14 @@ import React from 'react'; import PropTypes from 'prop-types'; -import {NumericFraction, SubplotAccordion} from '../components'; +import { + NumericFraction, + SubplotAccordion, + RectanglePositioner, +} from '../components'; const GraphSubplotsPanel = (props, {localize: _}) => ( - - - - + diff --git a/src/index.js b/src/index.js index 28d56565e..bbae7d04b 100644 --- a/src/index.js +++ b/src/index.js @@ -73,6 +73,7 @@ import { TraceMarkerSection, TraceRequiredPanel, TraceSelector, + RectanglePositioner, } from './components'; import { @@ -169,6 +170,7 @@ export { walkObject, EditorControls, DefaultEditor, + RectanglePositioner, }; export default PlotlyEditor; From 6259573e3eb9e64f507dac1ddf801f49b4fd538e Mon Sep 17 00:00:00 2001 From: Nicolas Kruchten Date: Tue, 31 Jul 2018 11:49:49 -0400 Subject: [PATCH 03/13] grid-snap --- src/components/fields/RectanglePositioner.js | 56 ++++++++++++-------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/src/components/fields/RectanglePositioner.js b/src/components/fields/RectanglePositioner.js index 4552481ca..2c820d334 100644 --- a/src/components/fields/RectanglePositioner.js +++ b/src/components/fields/RectanglePositioner.js @@ -5,6 +5,9 @@ import {connectToContainer} from 'lib'; import {NumericFraction} from './derived'; import ResizableRect from 'react-resizable-rotatable-draggable'; +const maxWidth = 300; +const gridRes = 8; + class UnconnectedRectanglePositioner extends Component { constructor(props, context) { super(props, context); @@ -17,14 +20,21 @@ class UnconnectedRectanglePositioner extends Component { const y0 = (fieldHeightPx - (height + y)) / fieldHeightPx; const y1 = (fieldHeightPx - y) / fieldHeightPx; - if (x0 >= 0 && y0 >= 0 && y1 <= 1 && x1 <= 1) { - this.context.updateContainer({ - 'domain.x[0]': x0, - 'domain.x[1]': x1, - 'domain.y[0]': y0, - 'domain.y[1]': y1, - }); + const snap = v => Math.round(v * gridRes) / gridRes; + + const payload = {}; + + if (x0 >= 0 && x1 <= 1) { + payload['domain.x[0]'] = snap(x0); + payload['domain.x[1]'] = snap(x1); } + + if (y0 >= 0 && y1 <= 1) { + payload['domain.y[0]'] = snap(y0); + payload['domain.y[1]'] = snap(y1); + } + + this.context.updateContainer(payload); } render() { @@ -36,10 +46,9 @@ class UnconnectedRectanglePositioner extends Component { }, fullLayout: {width: plotWidthPx, height: plotHeightPx}, } = this.context; - const aspectRatio = 1; //plotHeightPx / plotWidthPx; - const maxWidth = 300; - const fieldWidthPx = aspectRatio > 1 ? maxWidth : maxWidth / aspectRatio; - const fieldHeightPx = aspectRatio < 1 ? maxWidth : maxWidth * aspectRatio; + const aspectRatio = plotHeightPx / plotWidthPx; + const fieldWidthPx = Math.min(maxWidth, maxWidth / aspectRatio); + const fieldHeightPx = Math.min(maxWidth, maxWidth * aspectRatio); const width = fieldWidthPx * (x[1] - x[0]); const height = fieldHeightPx * (y[1] - y[0]); @@ -56,6 +65,20 @@ class UnconnectedRectanglePositioner extends Component { position: 'relative', }} > + {Array(gridRes * gridRes) + .fill(0) + .map((v, i) => ( +
+ ))} { - this.sendUpdate({ - fieldWidthPx, - fieldHeightPx, - width, - height, - x: left + deltaX, - y: top + deltaY, - }); - }} onResize={style => { this.sendUpdate({ fieldWidthPx, From 0a5ea61b07f11399fd407e4fd76ac1f105bb4575 Mon Sep 17 00:00:00 2001 From: dmt0 Date: Wed, 1 Aug 2018 17:32:52 -0400 Subject: [PATCH 04/13] rectangle positioner for cartesian --- src/components/fields/RectanglePositioner.js | 35 ++++++++++++-------- src/default_panels/GraphSubplotsPanel.js | 11 ++---- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/components/fields/RectanglePositioner.js b/src/components/fields/RectanglePositioner.js index 2c820d334..850806a22 100644 --- a/src/components/fields/RectanglePositioner.js +++ b/src/components/fields/RectanglePositioner.js @@ -12,6 +12,12 @@ class UnconnectedRectanglePositioner extends Component { constructor(props, context) { super(props, context); this.sendUpdate = this.sendUpdate.bind(this); + this.attr = this.props.cartesian + ? { + x: ['xaxis.domain[0]', 'xaxis.domain[1]'], + y: ['yaxis.domain[0]', 'yaxis.domain[1]'], + } + : {x: ['domain.x[0]', 'domain.x[1]'], y: ['domain.y[0]', 'domain.y[1]']}; } sendUpdate({x, y, width, height, fieldWidthPx, fieldHeightPx}) { @@ -25,27 +31,27 @@ class UnconnectedRectanglePositioner extends Component { const payload = {}; if (x0 >= 0 && x1 <= 1) { - payload['domain.x[0]'] = snap(x0); - payload['domain.x[1]'] = snap(x1); + payload[this.attr.x[0]] = snap(x0); + payload[this.attr.x[1]] = snap(x1); } if (y0 >= 0 && y1 <= 1) { - payload['domain.y[0]'] = snap(y0); - payload['domain.y[1]'] = snap(y1); + payload[this.attr.y[0]] = snap(y0); + payload[this.attr.y[1]] = snap(y1); } this.context.updateContainer(payload); } render() { - const {attr} = this.props; + const {attr, cartesian} = this.props; const { localize: _, - fullContainer: { - domain: {x, y}, - }, + fullContainer, fullLayout: {width: plotWidthPx, height: plotHeightPx}, } = this.context; + const x = cartesian ? fullContainer.xaxis.domain : fullContainer.domain.x; + const y = cartesian ? fullContainer.yaxis.domain : fullContainer.domain.y; const aspectRatio = plotHeightPx / plotWidthPx; const fieldWidthPx = Math.min(maxWidth, maxWidth / aspectRatio); const fieldHeightPx = Math.min(maxWidth, maxWidth * aspectRatio); @@ -74,8 +80,8 @@ class UnconnectedRectanglePositioner extends Component { width: fieldWidthPx / gridRes - 1, height: fieldHeightPx / gridRes - 1, float: 'left', - borderTop: i < gridRes ? '0' : '1px solid yellow', - borderLeft: i % gridRes ? '1px solid yellow' : '0', + borderTop: i < gridRes ? '0' : '1px solid lightgray', + borderLeft: i % gridRes ? '1px solid lightgray' : '0', }} /> ))} @@ -100,10 +106,10 @@ class UnconnectedRectanglePositioner extends Component { }} />
- - - - + + + + ); } @@ -112,6 +118,7 @@ class UnconnectedRectanglePositioner extends Component { UnconnectedRectanglePositioner.propTypes = { fullValue: PropTypes.any, updatePlot: PropTypes.func, + cartesian: PropTypes.bool, ...Field.propTypes, }; diff --git a/src/default_panels/GraphSubplotsPanel.js b/src/default_panels/GraphSubplotsPanel.js index 7b3096e8e..9a3c39b26 100644 --- a/src/default_panels/GraphSubplotsPanel.js +++ b/src/default_panels/GraphSubplotsPanel.js @@ -1,18 +1,11 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { - NumericFraction, - SubplotAccordion, - RectanglePositioner, -} from '../components'; +import {SubplotAccordion, RectanglePositioner} from '../components'; const GraphSubplotsPanel = (props, {localize: _}) => ( - - - - + ); From 00ebb3a5e91ee1a30776661fdcae18901e79fab2 Mon Sep 17 00:00:00 2001 From: dmt0 Date: Tue, 31 Jul 2018 17:21:37 -0400 Subject: [PATCH 05/13] fix for Axes to Use section appearing randomly --- src/components/fields/AxesCreator.js | 7 ++++--- src/default_panels/GraphCreatePanel.js | 4 +--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/fields/AxesCreator.js b/src/components/fields/AxesCreator.js index 73f128780..374014ade 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() { @@ -159,12 +160,12 @@ class UnconnectedAxesCreator extends Component { } return ( - + {controls} {_('You can style and position your axes in the Style > Axes Panel')} - + ); } } diff --git a/src/default_panels/GraphCreatePanel.js b/src/default_panels/GraphCreatePanel.js index 566705ade..7c73a7dac 100644 --- a/src/default_panels/GraphCreatePanel.js +++ b/src/default_panels/GraphCreatePanel.js @@ -106,9 +106,7 @@ const GraphCreatePanel = (props, {localize: _}) => { /> - - - + From 0e2e7605668214d6246adf574ec31e44bdc215b0 Mon Sep 17 00:00:00 2001 From: dmt0 Date: Wed, 1 Aug 2018 11:47:57 -0400 Subject: [PATCH 06/13] axis creation for combinations of different cartesian traces --- src/components/fields/AxesCreator.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/fields/AxesCreator.js b/src/components/fields/AxesCreator.js index 374014ade..e6e3b8937 100644 --- a/src/components/fields/AxesCreator.js +++ b/src/components/fields/AxesCreator.js @@ -126,16 +126,16 @@ const AxisCreator = connectToContainer(UnconnectedAxisCreator); class UnconnectedAxesCreator extends Component { render() { - const isFirstTraceOfType = - this.context.data.filter(d => d.type === this.props.container.type) + const axisType = traceTypeToAxisType(this.props.container.type); + const isFirstTraceOfAxisType = + this.context.data.filter(d => traceTypeToAxisType(d.type) === axisType) .length === 1; - if (isFirstTraceOfType) { + if (isFirstTraceOfAxisType) { return null; } const {fullLayout, localize: _} = this.context; - const axisType = traceTypeToAxisType(this.props.container.type); const controls = []; function getOptions(axisType) { From 74f459371062bdd30945b2d7ac91fbb1a3d18e0d Mon Sep 17 00:00:00 2001 From: dmt0 Date: Wed, 1 Aug 2018 15:25:36 -0400 Subject: [PATCH 07/13] widgets for multiple subplots for non-cartesian traces --- src/components/fields/AxesCreator.js | 4 +- src/components/fields/SubplotCreator.js | 183 ++++++++++++++++++++++++ src/components/fields/index.js | 2 + src/components/index.js | 2 + src/default_panels/GraphCreatePanel.js | 2 + src/lib/getAllAxes.js | 21 +-- src/lib/getAllSubplots.js | 81 +++++++++++ src/lib/index.js | 7 + 8 files changed, 291 insertions(+), 11 deletions(-) create mode 100644 src/components/fields/SubplotCreator.js create mode 100644 src/lib/getAllSubplots.js diff --git a/src/components/fields/AxesCreator.js b/src/components/fields/AxesCreator.js index e6e3b8937..4f5961ad0 100644 --- a/src/components/fields/AxesCreator.js +++ b/src/components/fields/AxesCreator.js @@ -163,7 +163,9 @@ class UnconnectedAxesCreator extends Component { {controls} - {_('You can style and position your axes in the Style > Axes Panel')} + {_( + 'You can style and position your axes in the Graph > Subplots Panel' + )} ); diff --git a/src/components/fields/SubplotCreator.js b/src/components/fields/SubplotCreator.js new file mode 100644 index 000000000..d87499c19 --- /dev/null +++ b/src/components/fields/SubplotCreator.js @@ -0,0 +1,183 @@ +import Dropdown from './Dropdown'; +import Info from './Info'; +import PropTypes from 'prop-types'; +import React, {Component} from 'react'; +import {EDITOR_ACTIONS, AXIS_TO_ATTR} from 'lib/constants'; +import Button from '../widgets/Button'; +import {PlusIcon} from 'plotly-icons'; +import {connectToContainer, traceTypeToAxisType, getSubplotTitle} from 'lib'; +import {PlotlySection} from 'components'; + +class UnconnectedSingleSubplotCreator 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 + ); + } + + addAndUpdateSubplot() { + const {attr, layoutAttr, updateContainer} = this.props; + const { + fullLayout: {_subplots: subplots}, + } = this.context; + const lastSubplotNumber = + Number( + subplots[layoutAttr][subplots[layoutAttr].length - 1].split( + layoutAttr === 'gl3d' ? 'scene' : layoutAttr + )[1] + ) || 1; + + updateContainer({ + [attr]: + (layoutAttr === 'gl3d' ? 'scene' : layoutAttr) + + (lastSubplotNumber + 1), + }); + } + + updateSubplot(update) { + const currentSubplotId = this.props.fullContainer[ + AXIS_TO_ATTR[this.props.attr] + ]; + let subplotToBeGarbageCollected = null; + + // When we select another subplot, make sure no unused axes are left + if ( + currentSubplotId !== update && + !this.context.fullData.some( + trace => + trace[AXIS_TO_ATTR[this.props.attr]] === currentSubplotId && + trace.index !== this.props.fullContainer.index + ) + ) { + subplotToBeGarbageCollected = currentSubplotId; + } + + this.context.onUpdate({ + type: EDITOR_ACTIONS.UPDATE_TRACES, + payload: { + subplotToBeGarbageCollected, + update: {[this.props.attr]: update}, + traceIndexes: [this.props.fullContainer.index], + }, + }); + } + + render() { + const icon = ; + const extraComponent = this.canAddAxis() ? ( +