Skip to content

Commit e2e2f39

Browse files
Shape and Image panels (#332)
1 parent 3b59d22 commit e2e2f39

File tree

21 files changed

+555
-9
lines changed

21 files changed

+555
-9
lines changed

README.md

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ This module's entry point is a React component called `<PlotlyEditor />` which c
3232

3333
## Connecting `<PlotlyEditor />` to `<Plot />`
3434

35-
The binding between `<PlotlyEditor />` and `<Plot />` works a little differently that in most React apps because plotly.js mutates its properties. This is mapped onto React's one-way dataflow model via event handlers and shared revision numbers which trigger re-renders of mutated state. The following subset of the [simple example](https://github.com/plotly/react-plotly.js-editor/tree/master/examples/simple) shows how this works using a parent component to store state, but the principle is the same with a different state-manage approach, as shown in the [redux example](https://github.com/plotly/react-plotly.js-editor/tree/master/examples/simple):
35+
The binding between `<PlotlyEditor />` and `<Plot />` works a little differently that in most React apps because plotly.js mutates its properties. This is mapped onto React's one-way dataflow model via event handlers and shared revision numbers which trigger re-renders of mutated state. The following subset of the [simple example](https://github.com/plotly/react-plotly.js-editor/tree/master/examples/simple) shows how this works using a parent component to store state, but the principle is the same with a different state-manage approach, as shown in the [redux example](https://github.com/plotly/react-plotly.js-editor/tree/master/examples/redux):
3636

3737
```javascript
3838
import PlotlyEditor from 'react-plotly.js-editor';
@@ -86,10 +86,13 @@ class App extends Component {
8686

8787
## Development Setup
8888

89+
This repo contains a [dev app](https://github.com/plotly/react-plotly.js-editor/tree/master/dev) that depends on the components locally and is configured for hot reloading, for easy local development. A `jest`-based test suite is also included.
90+
8991
```
9092
npm install
9193
npm start
92-
# start hacking
94+
# hacking happens here
95+
npm test
9396
```
9497

9598
## Built-in Components
@@ -146,7 +149,9 @@ Simple component that takes in props and renders.
146149
* `<LayoutPanel />`: `<Panel />` whose children are connected to the `layout` figure key
147150
* `<TraceRequiredPanel />`: `<LayoutPanel />` renders `<PanelEmpty />` if no trace data is set
148151
* `<AnnotationAccordion />`: `<Panel />` whose children are replicated into `<Folds />` connected to annotations via `connectAnnotationToLayout()`. For use in a `<LayoutPanel />`.
149-
* `<AxesFold />`: `<Fold />` whose children are bound to axis-specific keys. For use in a `<LayoutPanel />` in concert with `<AxesSelector />` (see below).
152+
* `<ShapeAccordion />`: `<Panel />` whose children are replicated into `<Folds />` connected to shapes via `connectShapeToLayout()`. For use in a `<LayoutPanel />`.
153+
* `<ImageAccordion />`: `<Panel />` whose children are replicated into `<Folds />` connected to images via `connectImageToLayout()`. For use in a `<LayoutPanel />`.
154+
* `<AxesFold />`: `<Fold />` whose children are bound to axis-specific keys. For use in a `<LayoutPanel />`; and automatically contains an `<AxesSelector />` (see below).
150155
* `<TraceMarkerSection />`: `<Section />` with trace-specific name handling. For use in containers bound to traces e.g. as children of `<TraceAccordion />`.
151156

152157
### Special-Purpose Fields
@@ -159,6 +164,7 @@ For use in containers bound to traces e.g. as children of `<TraceAccordion />`:
159164
* `<LineShapeSelector />`: renders as a `<Dropdown />` useful for `data[].line.shape`
160165
* `<SymbolSelector />`: renders as a `<Dropdown />` useful for `data[].marker.symbol`
161166
* `<LayoutNumericFraction />` and `<LayoutNumericFractionInverse />`: renders as a `<Numeric />` for use in trace-connected containers where normal `<Numerics />` would be bound to the `data` key instead of the `layout` key in the figure e.g. `layout.bargap` or `layout.barwidth`.
167+
* `<PositioningRef />`: renders as a `<Dropdown />` useful for `layout.*.xref/yref` where the allowable values are `paper|[axis]`
162168

163169
For use in containers bound to layout:
164170

@@ -167,7 +173,7 @@ For use in containers bound to layout:
167173

168174
For use in containers bound to axes:
169175

170-
* `<AxesSelector />`: renders as a `<Radio />` to select one or all axes. Must be in a container bound to a figure via `connectAxesToPlot()` such as `<AxesFold />` and sets that container's context such that its children are bound to either all axes or just the selected one.
176+
* `<AxesSelector />`: renders as a `<Radio />` to select one or all axes. Must be in a container bound to a figure via `connectAxesToPlot()` and sets that container's context such that its children are bound to either all axes or just the selected one. `<AxesFold>`s automatically contain this component.
171177
* `<AxesRange />`: numeric with visibility coupled to `layout.*axis.autorange`
172178

173179
For use in containers bound to annotations e.g. as children of `<AnnotationAccordion />`:
@@ -179,10 +185,12 @@ For use in containers bound to annotations e.g. as children of `<AnnotationAccor
179185
### Connector functions
180186

181187
* `connectToContainer( Component )`: returns a field component that can be bound to a figure value via the `attr` prop.
182-
* `connectTraceToPlot( Container )`: returns a wrapped container component that can be bound to a figure trace such that its children are bound to that trace's figure entry under the `data` key, e.g. `<TraceAccordion />` below.
188+
* `connectTraceToPlot( Container )`: returns a wrapped container component that can be bound to a figure trace such that its children are bound to that trace's figure entry under the `data` key, e.g. `<TraceAccordion />` above.
183189
* `connectLayoutToPlot( Container )`: returns a wrapped container component that can be bound to a figure such that its children are bound to that figure's layout under the `layout` key.
184-
* `connectAxesToLayout( Container )`: returns a wrapped container component that should contain an `<AxesSelector />` field (see below) and can be bound to a figure such that its children are bound to that figure's axes entries under the `layout.*axis` keys.
185-
* `connectAnnotationToLayout( Container )`: returns a wrapped container component that can be bound to a figure annotation such that its children are bound to that annotation's figure entry under the `layout.annotations` key, e.g. `<AnnotationAccordion />` below.
190+
* `connectAxesToLayout( Container )`: returns a wrapped container component that should contain an `<AxesSelector />` field (see above) and can be bound to a figure such that its children are bound to that figure's axes entries under the `layout.*axis` keys.
191+
* `connectAnnotationToLayout( Container )`: returns a wrapped container component that can be bound to a figure annotation such that its children are bound to that annotation's figure entry under the `layout.annotations` key, e.g. the `<Fold>`s in `<AnnotationAccordion />` above.
192+
* `connectShapeToLayout( Container )`: returns a wrapped container component that can be bound to a shape such that its children are bound to that shape's figure entry under the `layout.shapes` key, e.g. the `<Fold>`s in `<ShapeAccordion />` above.
193+
* `connectImagesToLayout( Container )`: returns a wrapped container component that can be bound to an image such that its children are bound to that image's figure entry under the `layout.image` key, e.g. the `<Fold>`s in `<ImageAccordion />` above.
186194

187195
## Mapbox Access Tokens
188196

dev/App.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ class App extends Component {
8686
/>
8787
<div className="app__main" style={{width: '100%', height: '100%'}}>
8888
<Plot
89-
config={{mapboxAccessToken: ACCESS_TOKENS.MAPBOX}}
89+
config={{mapboxAccessToken: ACCESS_TOKENS.MAPBOX, editable: true}}
9090
data={this.state.graphDiv.data}
9191
debug
9292
layout={this.state.graphDiv.layout}

examples/demo/src/App.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ class App extends Component {
7373
<div className="app__container plotly-editor--theme-provider">
7474
<div className="app">
7575
<PlotlyEditor
76+
config={{editable: true}}
7677
graphDiv={this.state.graphDiv}
7778
onUpdate={this.handleEditorUpdate.bind(this)}
7879
revision={this.state.editorRevision}

examples/simple/src/App.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ class App extends Component {
5353
<Plot
5454
debug
5555
useResizeHandler
56+
config={{editable: true}}
5657
data={this.state.graphDiv.data}
5758
layout={this.state.graphDiv.layout}
5859
onUpdate={this.handlePlotUpdate.bind(this)}

src/DefaultEditor.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {
88
StyleAxesPanel,
99
StyleLegendPanel,
1010
StyleNotesPanel,
11+
StyleShapesPanel,
12+
StyleImagesPanel,
1113
StyleTracesPanel,
1214
StyleColorbarsPanel,
1315
} from './default_panels';
@@ -21,6 +23,8 @@ const DefaultEditor = ({localize: _}) => (
2123
<StyleAxesPanel group={_('Style')} name={_('Axes')} />
2224
<StyleLegendPanel group={_('Style')} name={_('Legend')} />
2325
<StyleColorbarsPanel group={_('Style')} name={_('Color Bars')} />
26+
<StyleShapesPanel group={_('Style')} name={_('Shapes')} />
27+
<StyleImagesPanel group={_('Style')} name={_('Images')} />
2428
</PanelMenuWrapper>
2529
);
2630

src/PlotlyEditor.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,36 @@ class PlotlyEditor extends Component {
145145
}
146146
break;
147147

148+
case EDITOR_ACTIONS.DELETE_SHAPE:
149+
if (isNumeric(payload.shapeIndex)) {
150+
if (this.props.beforeDeleteShape) {
151+
this.props.beforeDeleteShape(payload);
152+
}
153+
graphDiv.layout.shapes.splice(payload.shapeIndex, 1);
154+
if (this.props.afterDeleteShape) {
155+
this.props.afterDeleteShape(payload);
156+
}
157+
if (this.props.onUpdate) {
158+
this.props.onUpdate();
159+
}
160+
}
161+
break;
162+
163+
case EDITOR_ACTIONS.DELETE_IMAGE:
164+
if (isNumeric(payload.imageIndex)) {
165+
if (this.props.beforeDeleteImage) {
166+
this.props.beforeDeleteImage(payload);
167+
}
168+
graphDiv.layout.images.splice(payload.imageIndex, 1);
169+
if (this.props.afterDeleteImage) {
170+
this.props.afterDeleteImage(payload);
171+
}
172+
if (this.props.onUpdate) {
173+
this.props.onUpdate();
174+
}
175+
}
176+
break;
177+
148178
default:
149179
throw new Error('must specify an action type to handleEditorUpdate');
150180
}
@@ -170,11 +200,15 @@ class PlotlyEditor extends Component {
170200
PlotlyEditor.propTypes = {
171201
afterAddTrace: PropTypes.func,
172202
afterDeleteAnnotation: PropTypes.func,
203+
afterDeleteShape: PropTypes.func,
204+
afterDeleteImage: PropTypes.func,
173205
afterDeleteTrace: PropTypes.func,
174206
afterUpdateLayout: PropTypes.func,
175207
afterUpdateTraces: PropTypes.func,
176208
beforeAddTrace: PropTypes.func,
177209
beforeDeleteAnnotation: PropTypes.func,
210+
beforeDeleteShape: PropTypes.func,
211+
beforeDeleteImage: PropTypes.func,
178212
beforeDeleteTrace: PropTypes.func,
179213
beforeUpdateLayout: PropTypes.func,
180214
beforeUpdateTraces: PropTypes.func,

src/components/containers/AnnotationAccordion.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class AnnotationAccordion extends Component {
3535
}
3636

3737
const key = `annotations[${annotationIndex}]`;
38-
const value = {text: 'new text'};
38+
const value = {text: _('new text')};
3939

4040
if (updateContainer) {
4141
updateContainer({[key]: value});
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import Fold from './Fold';
2+
import TraceRequiredPanel from './TraceRequiredPanel';
3+
import PropTypes from 'prop-types';
4+
import React, {Component} from 'react';
5+
import {connectImageToLayout, localize} from 'lib';
6+
7+
const ImageFold = connectImageToLayout(Fold);
8+
9+
class ImageAccordion extends Component {
10+
render() {
11+
const {layout: {images = []}} = this.context;
12+
const {canAdd, children, localize: _} = this.props;
13+
14+
const content =
15+
images.length &&
16+
images.map((ann, i) => (
17+
<ImageFold key={i} imageIndex={i} name={ann.text} canDelete={canAdd}>
18+
{children}
19+
</ImageFold>
20+
));
21+
22+
const addAction = {
23+
label: _('Image'),
24+
handler: ({layout, updateContainer}) => {
25+
let imageIndex;
26+
if (Array.isArray(layout.images)) {
27+
imageIndex = layout.images.length;
28+
} else {
29+
imageIndex = 0;
30+
}
31+
32+
const key = `images[${imageIndex}]`;
33+
const value = {
34+
text: `${_('Image')} ${imageIndex}`,
35+
sizex: 0.1,
36+
sizey: 0.1,
37+
x: 0.5,
38+
y: 0.5,
39+
};
40+
41+
if (updateContainer) {
42+
updateContainer({[key]: value});
43+
}
44+
},
45+
};
46+
47+
return (
48+
<TraceRequiredPanel addAction={canAdd ? addAction : null}>
49+
{content ? content : null}
50+
</TraceRequiredPanel>
51+
);
52+
}
53+
}
54+
55+
ImageAccordion.contextTypes = {
56+
layout: PropTypes.object,
57+
};
58+
59+
ImageAccordion.propTypes = {
60+
children: PropTypes.node,
61+
canAdd: PropTypes.bool,
62+
localize: PropTypes.func,
63+
};
64+
65+
export default localize(ImageAccordion);
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import Fold from './Fold';
2+
import TraceRequiredPanel from './TraceRequiredPanel';
3+
import PropTypes from 'prop-types';
4+
import React, {Component} from 'react';
5+
import {connectShapeToLayout, localize} from 'lib';
6+
7+
const ShapeFold = connectShapeToLayout(Fold);
8+
9+
class ShapeAccordion extends Component {
10+
render() {
11+
const {layout: {shapes = []}} = this.context;
12+
const {canAdd, children, localize: _} = this.props;
13+
14+
const content =
15+
shapes.length &&
16+
shapes.map((ann, i) => (
17+
<ShapeFold key={i} shapeIndex={i} name={ann.text} canDelete={canAdd}>
18+
{children}
19+
</ShapeFold>
20+
));
21+
22+
const addAction = {
23+
label: _('Shape'),
24+
handler: ({layout, updateContainer}) => {
25+
let shapeIndex;
26+
if (Array.isArray(layout.shapes)) {
27+
shapeIndex = layout.shapes.length;
28+
} else {
29+
shapeIndex = 0;
30+
}
31+
32+
const key = `shapes[${shapeIndex}]`;
33+
const value = {
34+
text: `${_('Shape')} ${shapeIndex}`,
35+
line: {color: '#444444'},
36+
fillcolor: '#7F7F7F',
37+
opacity: 0.3,
38+
};
39+
40+
if (updateContainer) {
41+
updateContainer({[key]: value});
42+
}
43+
},
44+
};
45+
46+
return (
47+
<TraceRequiredPanel addAction={canAdd ? addAction : null}>
48+
{content ? content : null}
49+
</TraceRequiredPanel>
50+
);
51+
}
52+
}
53+
54+
ShapeAccordion.contextTypes = {
55+
layout: PropTypes.object,
56+
};
57+
58+
ShapeAccordion.propTypes = {
59+
children: PropTypes.node,
60+
canAdd: PropTypes.bool,
61+
localize: PropTypes.func,
62+
};
63+
64+
export default localize(ShapeAccordion);

src/components/containers/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import AnnotationAccordion from './AnnotationAccordion';
2+
import ShapeAccordion from './ShapeAccordion';
3+
import ImageAccordion from './ImageAccordion';
24
import AxesFold from './AxesFold';
35
import Fold from './Fold';
46
import MenuPanel from './MenuPanel';
@@ -12,6 +14,8 @@ import SingleSidebarItem from './SingleSidebarItem';
1214

1315
export {
1416
AnnotationAccordion,
17+
ShapeAccordion,
18+
ImageAccordion,
1519
MenuPanel,
1620
Fold,
1721
Panel,

0 commit comments

Comments
 (0)