Skip to content

Commit 188fc34

Browse files
Transforms (#437)
1 parent 80fc812 commit 188fc34

23 files changed

+489
-39
lines changed

dev/App.js

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import 'react-select/dist/react-select.css';
66
import brace from 'brace'; // eslint-disable-line no-unused-vars
77
import AceEditor from 'react-ace';
88
import Select from 'react-select';
9-
import PlotlyEditor, {DefaultEditor, Panel} from '../src';
9+
import PlotlyEditor, {DefaultEditor, Panel, GraphTransformsPanel} from '../src';
1010
import Inspector from 'react-inspector';
1111
import 'brace/mode/json';
1212
import 'brace/theme/textmate';
@@ -15,13 +15,21 @@ import 'brace/theme/textmate';
1515
import ACCESS_TOKENS from '../accessTokens';
1616

1717
const dataSources = {
18-
ints: [1, 2, 3, 4, 5], // eslint-disable-line no-magic-numbers
19-
'jagged ints': [2, 1, 3, 5, 4], // eslint-disable-line no-magic-numbers
18+
ints: [1, 2, 3, 4, 5, 6], // eslint-disable-line no-magic-numbers
19+
'jagged ints': [2, 1, 3, 5, 4, 6], // eslint-disable-line no-magic-numbers
20+
'toggle ints': [1, -1, 1, -1, 1, -1], // eslint-disable-line no-magic-numbers
2021
'big ints': [1000, 10100, 10000, 20000, 100000], // eslint-disable-line no-magic-numbers
21-
dates: ['2010-01-01', '2010-07-01', '2011-01-01', '2011-07-01', '2012-01-01'],
22-
months: ['January', 'February', 'March', 'April', 'May'],
23-
colors: ['red', 'orange', 'yellow', 'green', 'blue'],
24-
justblue: ['blue'],
22+
dates: [
23+
'2010-01-01',
24+
'2010-07-01',
25+
'2011-01-01',
26+
'2011-07-01',
27+
'2012-01-01',
28+
'2012-06-01',
29+
],
30+
months: ['January', 'February', 'March', 'April', 'May', 'June'],
31+
colors: ['red', 'orange', 'yellow', 'green', 'blue', 'indigo'],
32+
'blue and red': ['blue', 'red'],
2533
};
2634
const dataSourceOptions = Object.keys(dataSources).map(name => ({
2735
value: name,
@@ -56,13 +64,14 @@ class App extends Component {
5664
}
5765

5866
loadMock(mockIndex) {
59-
fetch(
60-
'https://api.github.com/repos/plotly/plotly.js/contents/test/image/mocks/' +
61-
this.state.mocks[mockIndex],
62-
{
63-
headers: new Headers({Accept: 'application/vnd.github.v3.raw'}),
64-
}
65-
)
67+
const mockName = this.state.mocks[mockIndex];
68+
const prefix =
69+
mockName[0] === '/'
70+
? ''
71+
: 'https://api.github.com/repos/plotly/plotly.js/contents/test/image/mocks/';
72+
fetch(prefix + mockName, {
73+
headers: new Headers({Accept: 'application/vnd.github.v3.raw'}),
74+
})
6675
.then(response => response.json())
6776
.then(figure => {
6877
const {data, layout, frames} = figure;
@@ -109,6 +118,7 @@ class App extends Component {
109118
advancedTraceTypeSelector
110119
>
111120
<DefaultEditor>
121+
<GraphTransformsPanel group="Dev" name="Transforms" />
112122
<Panel group="Dev" name="JSON">
113123
<div className="mocks">
114124
<Select
@@ -126,14 +136,13 @@ class App extends Component {
126136
placeholder={'Search for a mock'}
127137
/>
128138
</div>
129-
<br />
130139
<button
140+
className="devbtn"
131141
onClick={this.loadJSON}
132142
style={{background: this.state.json_error ? 'pink' : 'white'}}
133143
>
134144
Save
135145
</button>
136-
<br />
137146
<AceEditor
138147
mode="json"
139148
theme="textmate"
@@ -152,10 +161,12 @@ class App extends Component {
152161
exec: this.loadJSON,
153162
},
154163
]}
164+
editorProps={{$blockScrolling: true}}
155165
/>
156166
</Panel>
157167
<Panel group="Dev" name="Inspector">
158168
<button
169+
className="devbtn"
159170
onClick={() => {
160171
const gd = document.getElementById('gd') || {};
161172
this.setState({
@@ -168,7 +179,6 @@ class App extends Component {
168179
>
169180
Refresh
170181
</button>
171-
<br />
172182
<div style={{height: '80vh'}}>
173183
<Inspector
174184
data={{_full: this.state.full}}

dev/mocks.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
[
2+
"/mocks/aggregate.json",
23
"0.json",
34
"1.json",
45
"10.json",

dev/mocks/aggregate.json

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"data": [
3+
{
4+
"type": "scatter",
5+
"x": [
6+
"Moe",
7+
"Larry",
8+
"Curly",
9+
"Moe",
10+
"Larry",
11+
"Curly",
12+
"Moe",
13+
"Larry",
14+
"Curly",
15+
"Moe",
16+
"Larry",
17+
"Curly"
18+
],
19+
"y": [1, 6, 2, 8, 2, 9, 4, 5, 1, 5, 2, 8],
20+
"mode": "markers",
21+
"transforms": [
22+
{
23+
"enabled" :true,
24+
"type": "aggregate",
25+
"groups": [
26+
"Moe",
27+
"Larry",
28+
"Curly",
29+
"Moe",
30+
"Larry",
31+
"Curly",
32+
"Moe",
33+
"Larry",
34+
"Curly",
35+
"Moe",
36+
"Larry",
37+
"Curly"
38+
],
39+
"aggregations": [{"target": "y", "func": "avg", "enabled": true}]
40+
}
41+
]
42+
}
43+
]
44+
}

dev/styles.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,8 @@ body {
1212
.mocks * {
1313
z-index: 100;
1414
}
15+
16+
.devbtn {
17+
margin: 10px;
18+
background: white;
19+
}

src/EditorControls.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,24 @@ class EditorControls extends Component {
239239
}
240240
break;
241241

242+
case EDITOR_ACTIONS.DELETE_TRANSFORM:
243+
if (isNumeric(payload.transformIndex)) {
244+
for (let i = 0; i < graphDiv.data.length; i++) {
245+
if (graphDiv.data[i].uid === payload.traceUid) {
246+
graphDiv.data[i].transforms.splice(payload.transformIndex, 1);
247+
if (this.props.onUpdate) {
248+
this.props.onUpdate(
249+
graphDiv.data.slice(),
250+
graphDiv.layout,
251+
graphDiv._transitionData._frames
252+
);
253+
}
254+
break;
255+
}
256+
}
257+
}
258+
break;
259+
242260
default:
243261
throw new Error('must specify an action type to handleEditorUpdate');
244262
}

src/components/containers/ModalBox.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import classnames from 'classnames';
44

55
export default class ModalBox extends Component {
66
render() {
7-
const {backgroundDark, children, onClose} = this.props;
7+
const {backgroundDark, children, onClose, relative} = this.props;
88
const modalboxClass = classnames('modalbox', {
99
'modalbox--dark': backgroundDark,
10+
'modalbox--relative': relative,
1011
});
1112
return (
1213
<div className={modalboxClass}>
@@ -19,6 +20,7 @@ export default class ModalBox extends Component {
1920

2021
ModalBox.propTypes = {
2122
backgroundDark: PropTypes.bool,
23+
relative: PropTypes.bool,
2224
children: PropTypes.node,
2325
onClose: PropTypes.func,
2426
};

src/components/containers/PanelHeader.js

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,20 @@ import PropTypes from 'prop-types';
33
import React, {Component} from 'react';
44
import {PlusIcon, ResizeUpIcon, ResizeDownIcon} from 'plotly-icons';
55
import {localize} from 'lib';
6+
import ModalBox from './ModalBox';
67

78
class PanelHeader extends Component {
9+
constructor() {
10+
super();
11+
this.state = {addPanelOpen: false};
12+
13+
this.togglePanel = this.togglePanel.bind(this);
14+
}
15+
16+
togglePanel() {
17+
this.setState({addPanelOpen: !this.state.addPanelOpen});
18+
}
19+
820
render() {
921
const {
1022
children,
@@ -43,10 +55,29 @@ class PanelHeader extends Component {
4355
<Button
4456
variant="primary"
4557
className="js-add-button"
46-
onClick={() => addAction.handler(this.context)}
58+
onClick={
59+
Array.isArray(addAction.handler)
60+
? this.togglePanel
61+
: () => addAction.handler(this.context)
62+
}
4763
icon={icon}
4864
label={addAction.label}
49-
/>{' '}
65+
/>
66+
{this.state.addPanelOpen && (
67+
<ModalBox onClose={this.togglePanel} relative>
68+
{addAction.handler.map(({label, handler}) => (
69+
<p
70+
key={label}
71+
onClick={() => {
72+
handler(this.context);
73+
this.togglePanel();
74+
}}
75+
>
76+
{label}
77+
</p>
78+
))}
79+
</ModalBox>
80+
)}
5081
</div>
5182
) : null}
5283
</div>

src/components/containers/RangeSelectorAccordion.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ class RangeSelectorAccordion extends Component {
2727
<RangeSelectorFold
2828
key={i}
2929
rangeselectorIndex={i}
30-
buttonIndex={i}
3130
name={btn.label}
3231
canDelete={true}
3332
>
@@ -40,11 +39,15 @@ class RangeSelectorAccordion extends Component {
4039
handler: context => {
4140
const {fullContainer, updateContainer} = context;
4241
if (updateContainer) {
43-
const shapeIndex = Array.isArray(fullContainer.rangeselector.buttons)
42+
const rangeselectorIndex = Array.isArray(
43+
fullContainer.rangeselector.buttons
44+
)
4445
? fullContainer.rangeselector.buttons.length
4546
: 0;
4647

47-
updateContainer({[`rangeselector.buttons[${shapeIndex}]`]: {}});
48+
updateContainer({
49+
[`rangeselector.buttons[${rangeselectorIndex}]`]: {},
50+
});
4851
}
4952
},
5053
};

src/components/containers/Section.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ class Section extends Component {
5959
}
6060
return (
6161
<div className="section">
62-
<SectionHeader name={this.props.name} />
62+
{this.props.name && <SectionHeader name={this.props.name} />}
6363
{this.children}
6464
</div>
6565
);
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import Fold from './Fold';
2+
import Panel from './Panel';
3+
import PropTypes from 'prop-types';
4+
import React, {Component} from 'react';
5+
import {connectTransformToTrace, localize} from 'lib';
6+
7+
const TransformFold = connectTransformToTrace(Fold);
8+
9+
class TransformAccordion extends Component {
10+
render() {
11+
const {fullContainer: {transforms = []}} = this.context;
12+
const {children, localize: _} = this.props;
13+
14+
const transformTypes = [
15+
{label: _('Filter'), type: 'filter'},
16+
{label: _('Split'), type: 'groupby'},
17+
{label: _('Aggregate'), type: 'aggregate'},
18+
];
19+
20+
const filteredTransforms = transforms.filter(({type}) => Boolean(type));
21+
const content =
22+
filteredTransforms.length &&
23+
filteredTransforms.map((tr, i) => (
24+
<TransformFold
25+
key={i}
26+
transformIndex={i}
27+
name={transformTypes.filter(({type}) => type === tr.type)[0].label}
28+
canDelete={true}
29+
>
30+
{children}
31+
</TransformFold>
32+
));
33+
34+
const addAction = {
35+
label: _('Transform'),
36+
handler: transformTypes.map(({label, type}) => {
37+
return {
38+
label,
39+
handler: context => {
40+
const {fullContainer, updateContainer} = context;
41+
if (updateContainer) {
42+
const transformIndex = Array.isArray(fullContainer.transforms)
43+
? fullContainer.transforms.length
44+
: 0;
45+
const key = `transforms[${transformIndex}]`;
46+
updateContainer({[key]: {type}});
47+
}
48+
},
49+
};
50+
}),
51+
};
52+
53+
return <Panel addAction={addAction}>{content ? content : null}</Panel>;
54+
}
55+
}
56+
57+
TransformAccordion.contextTypes = {
58+
fullContainer: PropTypes.object,
59+
};
60+
61+
TransformAccordion.propTypes = {
62+
children: PropTypes.node,
63+
localize: PropTypes.func,
64+
};
65+
66+
export default localize(TransformAccordion);

src/components/containers/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import MenuPanel from './MenuPanel';
1010
import Panel from './Panel';
1111
import Section from './Section';
1212
import TraceAccordion from './TraceAccordion';
13+
import TransformAccordion from './TransformAccordion';
1314
import TraceMarkerSection from './TraceMarkerSection';
1415
import {LayoutPanel, TraceTypeSection} from './derived';
1516
import TraceRequiredPanel from './TraceRequiredPanel';
@@ -30,6 +31,7 @@ export {
3031
Panel,
3132
Section,
3233
TraceAccordion,
34+
TransformAccordion,
3335
TraceMarkerSection,
3436
TraceRequiredPanel,
3537
LayoutPanel,

0 commit comments

Comments
 (0)