@khanacademy/perseus-editor 0.0.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.babelrc.js +1 -1
- package/CHANGELOG.md +26 -0
- package/dist/es/index.css +1020 -0
- package/dist/es/index.js +9 -1
- package/dist/es/index.js.map +1 -1
- package/dist/index.css +1020 -0
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/strings.js +6 -1
- package/package.json +16 -4
- package/src/README.md +7 -0
- package/src/__stories__/editor.stories.js +33 -0
- package/src/__stories__/section-control-button.stories.jsx +27 -0
- package/src/__testdata__/input-number_testdata.js +28 -0
- package/src/__tests__/editor_test.jsx +123 -0
- package/src/__tests__/i18n_test.jsx +585 -0
- package/src/__tests__/traversal_test.jsx +378 -0
- package/src/all-editors.js +85 -0
- package/src/all-editors_test.js +17 -0
- package/src/all-widgets.js +10 -0
- package/src/article-editor.jsx +437 -0
- package/src/components/__stories__/blur-input.stories.jsx +24 -0
- package/src/components/blur-input.jsx +62 -0
- package/src/components/checkbox.jsx +194 -0
- package/src/components/device-framer.jsx +91 -0
- package/src/components/drag-target.jsx +88 -0
- package/src/components/dropdown-option.jsx +318 -0
- package/src/components/form-wrapped-text-field.jsx +248 -0
- package/src/components/graph-settings.jsx +631 -0
- package/src/components/hover-behavior.jsx +178 -0
- package/src/components/json-editor.jsx +121 -0
- package/src/components/link.jsx +173 -0
- package/src/components/simple-button.jsx +86 -0
- package/src/components/sortable.jsx +186 -0
- package/src/components/util.js +39 -0
- package/src/components/viewport-resizer.jsx +73 -0
- package/src/diffs/__stories__/perseus-diff-wrapper.jsx +8 -0
- package/src/diffs/__stories__/structured-item-diff.stories.jsx +855 -0
- package/src/diffs/__stories__/tags-diff.stories.jsx +38 -0
- package/src/diffs/__stories__/text-diff.stories.jsx +37 -0
- package/src/diffs/__tests__/split-diff_test.js +34 -0
- package/src/diffs/__tests__/string-array-diff_test.js +74 -0
- package/src/diffs/__tests__/structured-item-diff_test.js +432 -0
- package/src/diffs/__tests__/widget-diff-performer_test.js +44 -0
- package/src/diffs/article-diff.jsx +70 -0
- package/src/diffs/item-diff.jsx +77 -0
- package/src/diffs/renderer-diff.jsx +143 -0
- package/src/diffs/split-diff.jsx +30 -0
- package/src/diffs/string-array-diff.jsx +75 -0
- package/src/diffs/structured-item-diff.jsx +269 -0
- package/src/diffs/tags-diff.jsx +50 -0
- package/src/diffs/text-diff.jsx +194 -0
- package/src/diffs/widget-diff-performer.jsx +74 -0
- package/src/diffs/widget-diff.jsx +291 -0
- package/src/editor-page.jsx +351 -0
- package/src/editor.jsx +1364 -0
- package/src/hint-editor.jsx +447 -0
- package/src/i18n.jsx +139 -0
- package/src/iframe-content-renderer.jsx +201 -0
- package/src/index.js +27 -8
- package/src/item-editor.jsx +159 -0
- package/src/item-extras-editor.jsx +150 -0
- package/src/katex-error-view.jsx +87 -0
- package/src/multirenderer-editor.jsx +1074 -0
- package/src/section-control-button.jsx +53 -0
- package/src/stateful-article-editor.jsx +176 -0
- package/src/stateful-editor-page.jsx +90 -0
- package/src/styles/perseus-editor.less +1339 -0
- package/src/testing-widgets.js +12 -0
- package/src/util/register-all-widgets-and-editors-for-testing.js +15 -0
- package/src/util/register-all-widgets-and-editors-for-testing_test.js +18 -0
- package/src/widgets/__stories__/label-image-editor.stories.jsx +97 -0
- package/src/widgets/categorizer-editor.jsx +103 -0
- package/src/widgets/cs-program-editor.jsx +258 -0
- package/src/widgets/definition-editor.jsx +75 -0
- package/src/widgets/dropdown-editor.jsx +201 -0
- package/src/widgets/example-graphie-widget-editor.jsx +70 -0
- package/src/widgets/example-graphie-widget.jsx +175 -0
- package/src/widgets/example-widget-editor.jsx +60 -0
- package/src/widgets/example-widget.jsx +153 -0
- package/src/widgets/explanation-editor.jsx +89 -0
- package/src/widgets/expression-editor.jsx +560 -0
- package/src/widgets/graded-group-editor.jsx +162 -0
- package/src/widgets/graded-group-set-editor.jsx +101 -0
- package/src/widgets/grapher-editor.jsx +154 -0
- package/src/widgets/group-editor.jsx +83 -0
- package/src/widgets/iframe-editor.jsx +200 -0
- package/src/widgets/image-editor.jsx +326 -0
- package/src/widgets/input-number-editor.jsx +269 -0
- package/src/widgets/interaction/constraint-editor.jsx +147 -0
- package/src/widgets/interaction-editor.jsx +1389 -0
- package/src/widgets/interactive-graph-editor.jsx +322 -0
- package/src/widgets/label-image/__stories__/answer-choices.stories.jsx +81 -0
- package/src/widgets/label-image/__stories__/behavior.stories.jsx +32 -0
- package/src/widgets/label-image/__stories__/marker.stories.jsx +76 -0
- package/src/widgets/label-image/__stories__/question-markers.stories.jsx +106 -0
- package/src/widgets/label-image/__stories__/select-image.stories.jsx +60 -0
- package/src/widgets/label-image/answer-choices.jsx +231 -0
- package/src/widgets/label-image/behavior.jsx +92 -0
- package/src/widgets/label-image/marker.jsx +292 -0
- package/src/widgets/label-image/question-markers.jsx +181 -0
- package/src/widgets/label-image/select-image.jsx +86 -0
- package/src/widgets/label-image-editor.jsx +257 -0
- package/src/widgets/lights-puzzle-editor.jsx +267 -0
- package/src/widgets/matcher-editor.jsx +147 -0
- package/src/widgets/matrix-editor.jsx +155 -0
- package/src/widgets/measurer-editor.jsx +245 -0
- package/src/widgets/molecule-editor.jsx +63 -0
- package/src/widgets/number-line-editor.jsx +498 -0
- package/src/widgets/numeric-input-editor.jsx +554 -0
- package/src/widgets/orderer-editor.jsx +166 -0
- package/src/widgets/passage-editor.jsx +115 -0
- package/src/widgets/passage-ref-editor.jsx +77 -0
- package/src/widgets/passage-ref-target-editor.jsx +47 -0
- package/src/widgets/plotter-editor.jsx +537 -0
- package/src/widgets/radio/editor.jsx +398 -0
- package/src/widgets/reaction-diagram-editor.jsx +127 -0
- package/src/widgets/sequence-editor.jsx +205 -0
- package/src/widgets/simple-markdown-tester-editor.jsx +84 -0
- package/src/widgets/simple-markdown-tester.jsx +91 -0
- package/src/widgets/simulator-editor.jsx +144 -0
- package/src/widgets/sorter-editor.jsx +100 -0
- package/src/widgets/table-editor.jsx +164 -0
- package/src/widgets/transformer-editor.jsx +1006 -0
- package/src/widgets/unit-editor.jsx +293 -0
- package/src/widgets/video-editor.jsx +74 -0
- package/src/__tests__/index.test.js +0 -15
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
/* eslint-disable @babel/no-invalid-this, one-var, react/no-unsafe, react/sort-comp */
|
|
2
|
+
// @flow
|
|
3
|
+
import {number as knumber} from "@khanacademy/kmath";
|
|
4
|
+
import {
|
|
5
|
+
components,
|
|
6
|
+
Dependencies,
|
|
7
|
+
PlotterWidget,
|
|
8
|
+
Util,
|
|
9
|
+
} from "@khanacademy/perseus";
|
|
10
|
+
import PropTypes from "prop-types";
|
|
11
|
+
import * as React from "react";
|
|
12
|
+
import ReactDOM from "react-dom";
|
|
13
|
+
import _ from "underscore";
|
|
14
|
+
|
|
15
|
+
import BlurInput from "../components/blur-input.jsx";
|
|
16
|
+
|
|
17
|
+
const {InfoTip, NumberInput, RangeInput, TextListEditor} = components;
|
|
18
|
+
const Plotter = PlotterWidget.widget;
|
|
19
|
+
|
|
20
|
+
const BAR = "bar",
|
|
21
|
+
LINE = "line",
|
|
22
|
+
PIC = "pic",
|
|
23
|
+
HISTOGRAM = "histogram",
|
|
24
|
+
DOTPLOT = "dotplot";
|
|
25
|
+
|
|
26
|
+
// Return a copy of array with length n, padded with given value
|
|
27
|
+
function padArray(array, n, value) {
|
|
28
|
+
const copy = _.clone(array);
|
|
29
|
+
copy.length = n;
|
|
30
|
+
for (let i = array.length; i < n; i++) {
|
|
31
|
+
copy[i] = value;
|
|
32
|
+
}
|
|
33
|
+
return copy;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const editorDefaults = {
|
|
37
|
+
scaleY: 1,
|
|
38
|
+
maxY: 10,
|
|
39
|
+
snapsPerLine: 2,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const widgetPropTypes = {
|
|
43
|
+
type: PropTypes.oneOf([BAR, LINE, PIC, HISTOGRAM, DOTPLOT]),
|
|
44
|
+
labels: PropTypes.arrayOf(PropTypes.string),
|
|
45
|
+
categories: PropTypes.arrayOf(
|
|
46
|
+
PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
|
47
|
+
),
|
|
48
|
+
|
|
49
|
+
scaleY: PropTypes.number,
|
|
50
|
+
maxY: PropTypes.number,
|
|
51
|
+
snapsPerLine: PropTypes.number,
|
|
52
|
+
|
|
53
|
+
picSize: PropTypes.number,
|
|
54
|
+
pixBoxHeight: PropTypes.number,
|
|
55
|
+
picUrl: PropTypes.string,
|
|
56
|
+
|
|
57
|
+
plotDimensions: PropTypes.arrayOf(PropTypes.number),
|
|
58
|
+
labelInterval: PropTypes.number,
|
|
59
|
+
starting: PropTypes.arrayOf(PropTypes.number),
|
|
60
|
+
correct: PropTypes.arrayOf(PropTypes.number),
|
|
61
|
+
static: PropTypes.bool,
|
|
62
|
+
onChange: PropTypes.func,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const formatNumber = (num) => "$" + knumber.round(num, 2) + "$";
|
|
66
|
+
|
|
67
|
+
type Props = $FlowFixMe;
|
|
68
|
+
type State = $FlowFixMe;
|
|
69
|
+
|
|
70
|
+
class PlotterEditor extends React.Component<Props, State> {
|
|
71
|
+
static propTypes = widgetPropTypes;
|
|
72
|
+
static widgetName: string = "plotter";
|
|
73
|
+
|
|
74
|
+
static defaultProps: Props = {
|
|
75
|
+
...editorDefaults,
|
|
76
|
+
correct: [1],
|
|
77
|
+
starting: [1],
|
|
78
|
+
|
|
79
|
+
type: BAR,
|
|
80
|
+
labels: ["", ""],
|
|
81
|
+
categories: [""],
|
|
82
|
+
|
|
83
|
+
picSize: 30,
|
|
84
|
+
picBoxHeight: 36,
|
|
85
|
+
plotDimensions: [275, 200],
|
|
86
|
+
labelInterval: 1,
|
|
87
|
+
|
|
88
|
+
get picUrl() {
|
|
89
|
+
const staticUrl = Dependencies.getDependencies().staticUrl;
|
|
90
|
+
if (staticUrl) {
|
|
91
|
+
return staticUrl("/images/badges/earth-small.png");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return null;
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
state: State = {
|
|
99
|
+
editing: this.props.static ? "starting" : "correct",
|
|
100
|
+
pic: null,
|
|
101
|
+
loadedUrl: null,
|
|
102
|
+
minX: null,
|
|
103
|
+
maxX: null,
|
|
104
|
+
tickStep: null,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// TODO(jangmi, CP-3288): Remove usage of `UNSAFE_componentWillMount`
|
|
108
|
+
UNSAFE_componentWillMount() {
|
|
109
|
+
this.fetchPic(this.props.picUrl);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
UNSAFE_componentWillReceiveProps(nextProps: $FlowFixMe) {
|
|
113
|
+
this.fetchPic(nextProps.picUrl);
|
|
114
|
+
|
|
115
|
+
if (nextProps.static) {
|
|
116
|
+
this.setState({
|
|
117
|
+
editing: "starting",
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
fetchPic: (string) => $FlowFixMe = (url) => {
|
|
123
|
+
if (this.state.loadedUrl !== url) {
|
|
124
|
+
const pic = new Image();
|
|
125
|
+
pic.src = url;
|
|
126
|
+
pic.onload = () => {
|
|
127
|
+
this.setState({
|
|
128
|
+
pic: pic,
|
|
129
|
+
loadedUrl: url,
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
render(): React.Node {
|
|
136
|
+
const setFromScale = _.contains(
|
|
137
|
+
[LINE, HISTOGRAM, DOTPLOT],
|
|
138
|
+
this.props.type,
|
|
139
|
+
);
|
|
140
|
+
const canChangeSnaps = !_.contains([PIC, DOTPLOT], this.props.type);
|
|
141
|
+
const props = {
|
|
142
|
+
trackInteraction: () => {},
|
|
143
|
+
...this.props,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<div className="perseus-widget-plotter-editor">
|
|
148
|
+
<div>
|
|
149
|
+
Chart type:{" "}
|
|
150
|
+
{_.map(
|
|
151
|
+
[BAR, LINE, PIC, HISTOGRAM, DOTPLOT],
|
|
152
|
+
function (type) {
|
|
153
|
+
return (
|
|
154
|
+
<label key={type}>
|
|
155
|
+
<input
|
|
156
|
+
type="radio"
|
|
157
|
+
name="chart-type"
|
|
158
|
+
checked={this.props.type === type}
|
|
159
|
+
onChange={_.partial(
|
|
160
|
+
this.changeType,
|
|
161
|
+
type,
|
|
162
|
+
)}
|
|
163
|
+
/>
|
|
164
|
+
{type}
|
|
165
|
+
</label>
|
|
166
|
+
);
|
|
167
|
+
},
|
|
168
|
+
this,
|
|
169
|
+
)}
|
|
170
|
+
</div>
|
|
171
|
+
<div>
|
|
172
|
+
Labels:{" "}
|
|
173
|
+
{_.map(
|
|
174
|
+
["x", "y"],
|
|
175
|
+
function (axis, i) {
|
|
176
|
+
return (
|
|
177
|
+
<label key={axis}>
|
|
178
|
+
{axis + ":"}
|
|
179
|
+
<input
|
|
180
|
+
type="text"
|
|
181
|
+
onChange={_.partial(
|
|
182
|
+
this.changeLabel,
|
|
183
|
+
i,
|
|
184
|
+
)}
|
|
185
|
+
defaultValue={this.props.labels[i]}
|
|
186
|
+
/>
|
|
187
|
+
</label>
|
|
188
|
+
);
|
|
189
|
+
},
|
|
190
|
+
this,
|
|
191
|
+
)}
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
{setFromScale && (
|
|
195
|
+
<div className="set-from-scale-box">
|
|
196
|
+
<span className="categories-title">
|
|
197
|
+
Set Categories From Scale
|
|
198
|
+
</span>
|
|
199
|
+
<div>
|
|
200
|
+
<label>
|
|
201
|
+
Tick Step:{" "}
|
|
202
|
+
<NumberInput
|
|
203
|
+
placeholder={1}
|
|
204
|
+
useArrowKeys={true}
|
|
205
|
+
value={this.state.tickStep}
|
|
206
|
+
onChange={this.handleChangeTickStep}
|
|
207
|
+
/>
|
|
208
|
+
</label>
|
|
209
|
+
<InfoTip>
|
|
210
|
+
<p>The difference between adjacent ticks.</p>
|
|
211
|
+
</InfoTip>
|
|
212
|
+
</div>
|
|
213
|
+
<div>
|
|
214
|
+
<label>
|
|
215
|
+
Range:{" "}
|
|
216
|
+
<RangeInput
|
|
217
|
+
placeholder={[0, 10]}
|
|
218
|
+
useArrowKeys={true}
|
|
219
|
+
value={[this.state.minX, this.state.maxX]}
|
|
220
|
+
onChange={this.handleChangeRange}
|
|
221
|
+
/>
|
|
222
|
+
</label>
|
|
223
|
+
</div>
|
|
224
|
+
<div>
|
|
225
|
+
<button onClick={this.setCategoriesFromScale}>
|
|
226
|
+
Set Categories{" "}
|
|
227
|
+
</button>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
)}
|
|
231
|
+
<div>
|
|
232
|
+
<label>
|
|
233
|
+
Label Interval:{" "}
|
|
234
|
+
<NumberInput
|
|
235
|
+
useArrowKeys={true}
|
|
236
|
+
value={this.props.labelInterval}
|
|
237
|
+
onChange={this.changeLabelInterval}
|
|
238
|
+
/>
|
|
239
|
+
</label>
|
|
240
|
+
<InfoTip>
|
|
241
|
+
<p>
|
|
242
|
+
Which ticks to display the labels for. For instance,
|
|
243
|
+
setting this to "4" will only show every 4th label
|
|
244
|
+
(plus the last one)
|
|
245
|
+
</p>
|
|
246
|
+
</InfoTip>
|
|
247
|
+
</div>
|
|
248
|
+
{this.props.type === PIC && (
|
|
249
|
+
<div>
|
|
250
|
+
<label>
|
|
251
|
+
Picture:{" "}
|
|
252
|
+
<BlurInput
|
|
253
|
+
className="pic-url"
|
|
254
|
+
value={this.props.picUrl}
|
|
255
|
+
onChange={this.changePicUrl}
|
|
256
|
+
/>
|
|
257
|
+
<InfoTip>
|
|
258
|
+
<p>
|
|
259
|
+
Use the default picture of Earth, or insert
|
|
260
|
+
the URL for a different picture using the
|
|
261
|
+
"Add image" function.
|
|
262
|
+
</p>
|
|
263
|
+
</InfoTip>
|
|
264
|
+
</label>
|
|
265
|
+
{this.state.pic &&
|
|
266
|
+
this.state.pic.width !== this.state.pic.height && (
|
|
267
|
+
<p className="warning">
|
|
268
|
+
<b>Warning</b>: You are using a picture
|
|
269
|
+
which is not square. This means the image
|
|
270
|
+
will get distorted. You should probably crop
|
|
271
|
+
it to be square.
|
|
272
|
+
</p>
|
|
273
|
+
)}
|
|
274
|
+
</div>
|
|
275
|
+
)}
|
|
276
|
+
<div>
|
|
277
|
+
<label>
|
|
278
|
+
Categories:{" "}
|
|
279
|
+
<TextListEditor
|
|
280
|
+
// eslint-disable-next-line react/no-string-refs
|
|
281
|
+
ref="categories"
|
|
282
|
+
layout="horizontal"
|
|
283
|
+
options={this.props.categories}
|
|
284
|
+
onChange={this.changeCategories}
|
|
285
|
+
/>
|
|
286
|
+
</label>
|
|
287
|
+
</div>
|
|
288
|
+
<div>
|
|
289
|
+
<label>
|
|
290
|
+
Scale (y):{" "}
|
|
291
|
+
<input
|
|
292
|
+
type="text"
|
|
293
|
+
onChange={this.changeScale}
|
|
294
|
+
defaultValue={this.props.scaleY}
|
|
295
|
+
/>
|
|
296
|
+
</label>
|
|
297
|
+
</div>
|
|
298
|
+
<div>
|
|
299
|
+
<label>
|
|
300
|
+
Max y:{" "}
|
|
301
|
+
<input
|
|
302
|
+
type="text"
|
|
303
|
+
// eslint-disable-next-line react/no-string-refs
|
|
304
|
+
ref="maxY"
|
|
305
|
+
onChange={this.changeMax}
|
|
306
|
+
defaultValue={this.props.maxY}
|
|
307
|
+
/>
|
|
308
|
+
</label>
|
|
309
|
+
</div>
|
|
310
|
+
{canChangeSnaps && (
|
|
311
|
+
<div>
|
|
312
|
+
<label>
|
|
313
|
+
Snaps per line:{" "}
|
|
314
|
+
<input
|
|
315
|
+
type="text"
|
|
316
|
+
onChange={this.changeSnaps}
|
|
317
|
+
defaultValue={this.props.snapsPerLine}
|
|
318
|
+
/>
|
|
319
|
+
</label>
|
|
320
|
+
<InfoTip>
|
|
321
|
+
<p>
|
|
322
|
+
Creates the specified number of divisions
|
|
323
|
+
between the horizontal lines. Fewer snaps
|
|
324
|
+
between lines makes the graph easier for the
|
|
325
|
+
student to create correctly.
|
|
326
|
+
</p>
|
|
327
|
+
</InfoTip>
|
|
328
|
+
</div>
|
|
329
|
+
)}
|
|
330
|
+
<div>
|
|
331
|
+
Editing values:{" "}
|
|
332
|
+
{["correct", "starting"].map((editing) => (
|
|
333
|
+
<label key={editing}>
|
|
334
|
+
<input
|
|
335
|
+
type="radio"
|
|
336
|
+
disabled={
|
|
337
|
+
editing === "correct" && this.props.static
|
|
338
|
+
}
|
|
339
|
+
checked={
|
|
340
|
+
this.props.static
|
|
341
|
+
? editing === "starting"
|
|
342
|
+
: this.state.editing === editing
|
|
343
|
+
}
|
|
344
|
+
onChange={(e) => this.changeEditing(editing)}
|
|
345
|
+
/>
|
|
346
|
+
{editing}
|
|
347
|
+
</label>
|
|
348
|
+
))}
|
|
349
|
+
<InfoTip>
|
|
350
|
+
<p>
|
|
351
|
+
Use this toggle to switch between editing the
|
|
352
|
+
correct answer (what the student will be graded on)
|
|
353
|
+
and the starting values (what the student will see
|
|
354
|
+
plotted when they start the problem). Note: These
|
|
355
|
+
cannot be the same.
|
|
356
|
+
</p>
|
|
357
|
+
<p>
|
|
358
|
+
In static mode, the starting values are rendered out
|
|
359
|
+
to the displayed widget.
|
|
360
|
+
</p>
|
|
361
|
+
</InfoTip>
|
|
362
|
+
</div>
|
|
363
|
+
<Plotter
|
|
364
|
+
{...props}
|
|
365
|
+
starting={this.props[this.state.editing]}
|
|
366
|
+
onChange={this.handlePlotterChange}
|
|
367
|
+
/>
|
|
368
|
+
</div>
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
handleChangeTickStep: (number) => void = (value) => {
|
|
373
|
+
this.setState({
|
|
374
|
+
tickStep: value,
|
|
375
|
+
});
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
handleChangeRange: ([number, number]) => void = (newValue) => {
|
|
379
|
+
this.setState({
|
|
380
|
+
minX: newValue[0],
|
|
381
|
+
maxX: newValue[1],
|
|
382
|
+
});
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
changeLabelInterval: (number) => void = (value) => {
|
|
386
|
+
this.props.onChange({
|
|
387
|
+
labelInterval: value,
|
|
388
|
+
});
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
handlePlotterChange: ($FlowFixMe) => void = (newProps) => {
|
|
392
|
+
const props = {};
|
|
393
|
+
props[this.state.editing] = newProps.values;
|
|
394
|
+
this.props.onChange(props);
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
changeType: ($FlowFixMe) => void = (type) => {
|
|
398
|
+
let categories;
|
|
399
|
+
if (type === HISTOGRAM) {
|
|
400
|
+
// Switching to histogram, add a label (0) to the left
|
|
401
|
+
categories = [formatNumber(0)].concat(this.props.categories);
|
|
402
|
+
this.props.onChange({type: type, categories: categories});
|
|
403
|
+
} else if (this.props.type === HISTOGRAM) {
|
|
404
|
+
// Switching from histogram, remove a label from the left
|
|
405
|
+
categories = this.props.categories.slice(1);
|
|
406
|
+
this.props.onChange({type: type, categories: categories});
|
|
407
|
+
} else {
|
|
408
|
+
this.props.onChange({type: type});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (categories) {
|
|
412
|
+
// eslint-disable-next-line react/no-string-refs
|
|
413
|
+
const node = ReactDOM.findDOMNode(this.refs.categories);
|
|
414
|
+
// $FlowFixMe[incompatible-use]
|
|
415
|
+
// $FlowFixMe[prop-missing]
|
|
416
|
+
node.value = categories.join(", ");
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
changeLabel: (number, $FlowFixMe) => void = (i, e) => {
|
|
421
|
+
const labels = _.clone(this.props.labels);
|
|
422
|
+
labels[i] = e.target.value;
|
|
423
|
+
this.props.onChange({labels: labels});
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
changePicUrl: (string) => void = (value) => {
|
|
427
|
+
// We don't need the labels and other data in the plotter, so just
|
|
428
|
+
// extract the raw image and use that.
|
|
429
|
+
// TODO(emily): Maybe indicate that such a change has happened?
|
|
430
|
+
const url = Util.getRealImageUrl(value);
|
|
431
|
+
|
|
432
|
+
this.props.onChange({picUrl: url});
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
changeCategories: ($FlowFixMe) => void = (categories) => {
|
|
436
|
+
let n = categories.length;
|
|
437
|
+
if (this.props.type === HISTOGRAM) {
|
|
438
|
+
// Histograms with n labels/categories have n - 1 buckets
|
|
439
|
+
n--;
|
|
440
|
+
}
|
|
441
|
+
const value = this.props.scaleY;
|
|
442
|
+
|
|
443
|
+
this.props.onChange({
|
|
444
|
+
categories: categories,
|
|
445
|
+
correct: padArray(this.props.correct, n, value),
|
|
446
|
+
starting: padArray(this.props.starting, n, value),
|
|
447
|
+
});
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
changeScale: ($FlowFixMe) => void = (e) => {
|
|
451
|
+
const oldScale = this.props.scaleY;
|
|
452
|
+
const newScale = +e.target.value || editorDefaults.scaleY;
|
|
453
|
+
|
|
454
|
+
const scale = function (value) {
|
|
455
|
+
return (value * newScale) / oldScale;
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
const maxY = scale(this.props.maxY);
|
|
459
|
+
|
|
460
|
+
this.props.onChange({
|
|
461
|
+
scaleY: newScale,
|
|
462
|
+
maxY: maxY,
|
|
463
|
+
correct: _.map(this.props.correct, scale),
|
|
464
|
+
starting: _.map(this.props.starting, scale),
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// $FlowFixMe[prop-missing]
|
|
468
|
+
// $FlowFixMe[incompatible-use]
|
|
469
|
+
ReactDOM.findDOMNode(this.refs.maxY).value = maxY; // eslint-disable-line react/no-string-refs
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
changeMax: ($FlowFixMe) => void = (e) => {
|
|
473
|
+
this.props.onChange({
|
|
474
|
+
maxY: +e.target.value || editorDefaults.maxY,
|
|
475
|
+
});
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
changeSnaps: ($FlowFixMe) => void = (e) => {
|
|
479
|
+
this.props.onChange({
|
|
480
|
+
snapsPerLine: +e.target.value || editorDefaults.snapsPerLine,
|
|
481
|
+
});
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
changeEditing: (string) => void = (editing) => {
|
|
485
|
+
this.setState({editing: editing});
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
setCategoriesFromScale: () => void = () => {
|
|
489
|
+
const scale = this.state.tickStep || 1;
|
|
490
|
+
const min = this.state.minX || 0;
|
|
491
|
+
const max = this.state.maxX || 0;
|
|
492
|
+
const length = Math.floor((max - min) / scale) * scale;
|
|
493
|
+
|
|
494
|
+
let categories;
|
|
495
|
+
if (this.props.type === HISTOGRAM || this.props.type === DOTPLOT) {
|
|
496
|
+
// Ranges for histogram and dotplot labels should start at zero
|
|
497
|
+
categories = _.range(0, length + scale, scale);
|
|
498
|
+
} else {
|
|
499
|
+
categories = _.range(scale, length + scale, scale);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
categories = _.map(categories, (num) => num + min);
|
|
503
|
+
categories = _.map(categories, formatNumber);
|
|
504
|
+
|
|
505
|
+
this.changeCategories(categories);
|
|
506
|
+
|
|
507
|
+
// eslint-disable-next-line react/no-string-refs
|
|
508
|
+
const node = ReactDOM.findDOMNode(this.refs.categories);
|
|
509
|
+
|
|
510
|
+
// $FlowFixMe[incompatible-use]
|
|
511
|
+
// $FlowFixMe[prop-missing]
|
|
512
|
+
node.value = categories.join(", ");
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
serialize: () => $FlowFixMe = () => {
|
|
516
|
+
const json = _.pick(
|
|
517
|
+
this.props,
|
|
518
|
+
"correct",
|
|
519
|
+
"starting",
|
|
520
|
+
"type",
|
|
521
|
+
"labels",
|
|
522
|
+
"categories",
|
|
523
|
+
"scaleY",
|
|
524
|
+
"maxY",
|
|
525
|
+
"snapsPerLine",
|
|
526
|
+
"labelInterval",
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
if (this.props.type === PIC) {
|
|
530
|
+
json.picUrl = this.props.picUrl;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return json;
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
export default PlotterEditor;
|