@khanacademy/perseus 17.8.0 → 18.0.1
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/CHANGELOG.md +40 -0
- package/dist/es/index.css +0 -4
- package/dist/es/index.js +1422 -1021
- package/dist/es/index.js.map +1 -1
- package/dist/index.css +0 -4
- package/dist/index.d.ts +4 -2
- package/dist/index.js +1321 -954
- package/dist/index.js.map +1 -1
- package/dist/perseus-types.d.ts +4 -3
- package/dist/renderer.d.ts +6 -6
- package/dist/server-item-renderer.d.ts +2 -4
- package/dist/strings.js +1 -1
- package/dist/types.d.ts +19 -2
- package/dist/util/drawing-transform.d.ts +21 -0
- package/dist/util/extract-perseus-data.d.ts +26 -0
- package/dist/util/graph-bounds.d.ts +9 -0
- package/dist/util/graphie.d.ts +2 -0
- package/dist/util/interval.d.ts +1 -0
- package/dist/util/parse-perseus-json.d.ts +10 -0
- package/dist/util/tex.d.ts +1 -3
- package/dist/widgets/deprecated-standin/index.d.ts +4 -4
- package/dist/widgets/passage.d.ts +3 -3
- package/dist/widgets.d.ts +16 -6
- package/package.json +9 -9
- package/src/__tests__/extract-perseus-data.test.ts +1027 -0
- package/src/index.ts +6 -1
- package/src/init.ts +1 -1
- package/src/perseus-types.ts +4 -3
- package/src/renderer.tsx +12 -9
- package/src/server-item-renderer.tsx +2 -4
- package/src/styles/widgets/image.less +0 -3
- package/src/types.ts +19 -16
- package/src/util/drawing-transform.test.ts +67 -0
- package/src/util/drawing-transform.ts +102 -0
- package/src/util/extract-perseus-data.ts +539 -0
- package/src/util/graph-bounds.ts +23 -0
- package/src/util/graphie.ts +1167 -1258
- package/src/util/interactive.ts +2 -2
- package/src/util/interval.ts +4 -0
- package/src/util/parse-perseus-json.test.ts +10 -0
- package/src/util/parse-perseus-json.ts +13 -0
- package/src/util/tex.ts +11 -84
- package/src/widgets/__stories__/image.stories.tsx +12 -26
- package/src/widgets/__tests__/__snapshots__/grapher.test.ts.snap +72 -288
- package/src/widgets/__tests__/__snapshots__/interaction.test.ts.snap +5 -20
- package/src/widgets/__tests__/__snapshots__/interactive-graph.test.ts.snap +32 -128
- package/src/widgets/__tests__/__snapshots__/number-line.test.ts.snap +22 -88
- package/src/widgets/__tests__/__snapshots__/plotter.test.tsx.snap +12 -48
- package/src/widgets/deprecated-standin/__stories__/deprecated-standin.stories.tsx +8 -12
- package/src/widgets/deprecated-standin/__tests__/__snapshots__/deprecated-standin.test.ts.snap +6 -7
- package/src/widgets/deprecated-standin/index.tsx +8 -7
- package/src/widgets/interactive-graph.tsx +3 -2
- package/src/widgets/label-image/answer-pill.tsx +6 -1
- package/src/widgets/passage.tsx +5 -7
- package/src/widgets.ts +34 -10
- package/tsconfig-build.tsbuildinfo +1 -1
package/src/util/graphie.ts
CHANGED
|
@@ -13,31 +13,17 @@ import {Errors, Log} from "../logging/log";
|
|
|
13
13
|
import {PerseusError} from "../perseus-error";
|
|
14
14
|
|
|
15
15
|
import KhanColors from "./colors";
|
|
16
|
+
import {DrawingTransform} from "./drawing-transform";
|
|
17
|
+
import {GraphBounds} from "./graph-bounds";
|
|
16
18
|
import KhanMath from "./math";
|
|
17
19
|
import Tex from "./tex";
|
|
18
20
|
|
|
21
|
+
import type {Interval} from "./interval";
|
|
19
22
|
import type {Coord} from "../interactive2/types";
|
|
20
23
|
|
|
21
24
|
const {processMath} = Tex;
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
* theta], with theta in degrees, or in radians if angleInRadians is
|
|
25
|
-
* specified.
|
|
26
|
-
*/
|
|
27
|
-
function cartToPolar(coord: any, angleInRadians: any) {
|
|
28
|
-
const r = Math.sqrt(Math.pow(coord[0], 2) + Math.pow(coord[1], 2));
|
|
29
|
-
let theta = Math.atan2(coord[1], coord[0]);
|
|
30
|
-
// convert angle range from [-pi, pi] to [0, 2pi]
|
|
31
|
-
if (theta < 0) {
|
|
32
|
-
theta += 2 * Math.PI;
|
|
33
|
-
}
|
|
34
|
-
if (!angleInRadians) {
|
|
35
|
-
theta = (theta * 180) / Math.PI;
|
|
36
|
-
}
|
|
37
|
-
return [r, theta];
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function polar(r: Array<never>, th: number) {
|
|
26
|
+
export function polar(r: number | Coord, th: number) {
|
|
41
27
|
if (typeof r === "number") {
|
|
42
28
|
r = [r, r];
|
|
43
29
|
}
|
|
@@ -102,191 +88,574 @@ const GraphUtils: any = {
|
|
|
102
88
|
graphs: {},
|
|
103
89
|
};
|
|
104
90
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const labelDirections = {
|
|
113
|
-
center: [-0.5, -0.5],
|
|
114
|
-
above: [-0.5, -1.0],
|
|
115
|
-
"above right": [0.0, -1.0],
|
|
116
|
-
right: [0.0, -0.5],
|
|
117
|
-
"below right": [0.0, 0.0],
|
|
118
|
-
below: [-0.5, 0.0],
|
|
119
|
-
"below left": [-1.0, 0.0],
|
|
120
|
-
left: [-1.0, -0.5],
|
|
121
|
-
"above left": [-1.0, -1.0],
|
|
122
|
-
} as const;
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Safari applies some SVG-specific styles to things that are not SVGs, so we
|
|
126
|
-
* need to exclude those styles from things that are not SVGs.
|
|
127
|
-
*
|
|
128
|
-
* To see this behavior in action, open https://codepen.io/anon/pen/zENEoa in
|
|
129
|
-
* Safari.
|
|
130
|
-
*
|
|
131
|
-
* Usage `$.extend({}, someStyles, SVG_SPECIFIC_STYLE_MASK)`
|
|
132
|
-
*/
|
|
133
|
-
const SVG_SPECIFIC_STYLE_MASK = {
|
|
134
|
-
"stroke-width": null,
|
|
135
|
-
} as const;
|
|
136
|
-
|
|
137
|
-
GraphUtils.createGraphie = function (el: any) {
|
|
138
|
-
let xScale = 40;
|
|
139
|
-
let yScale = 40;
|
|
140
|
-
let xRange;
|
|
141
|
-
let yRange;
|
|
142
|
-
|
|
143
|
-
$(el).css("position", "relative");
|
|
144
|
-
const raphael = Raphael(el);
|
|
145
|
-
|
|
146
|
-
// For a sometimes-reproducible IE8 bug; doesn't affect SVG browsers at all
|
|
147
|
-
$(el).children("div").css("position", "absolute");
|
|
148
|
-
|
|
91
|
+
class Graphie {
|
|
92
|
+
el: HTMLElement;
|
|
93
|
+
#bounds?: GraphBounds;
|
|
94
|
+
#drawingTransform?: DrawingTransform;
|
|
95
|
+
raphael?: any;
|
|
96
|
+
isMobile = false;
|
|
149
97
|
// Set up some reasonable defaults
|
|
150
|
-
|
|
98
|
+
currentStyle: any = {
|
|
151
99
|
"stroke-width": 2,
|
|
152
100
|
fill: "none",
|
|
153
101
|
};
|
|
154
102
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
103
|
+
range?: [Interval, Interval];
|
|
104
|
+
scale?: Coord;
|
|
105
|
+
dimensions?: Coord;
|
|
106
|
+
// TODO(benchristel): xpixels and ypixels are never used by this class, but
|
|
107
|
+
// other code reaches in to access them :(
|
|
108
|
+
// Refactor to accessor methods that look at this.dimensions.
|
|
109
|
+
xpixels?: number;
|
|
110
|
+
ypixels?: number;
|
|
111
|
+
|
|
112
|
+
constructor(el: HTMLElement) {
|
|
113
|
+
this.el = el;
|
|
114
|
+
$(el).css("position", "relative");
|
|
115
|
+
this.raphael = Raphael(el);
|
|
116
|
+
|
|
117
|
+
// For a sometimes-reproducible IE8 bug; doesn't affect SVG browsers at all
|
|
118
|
+
$(el).children("div").css("position", "absolute");
|
|
119
|
+
}
|
|
164
120
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
121
|
+
init(options: {
|
|
122
|
+
range?: [Interval, Interval];
|
|
123
|
+
scale?: number | Coord;
|
|
124
|
+
isMobile: boolean;
|
|
125
|
+
}) {
|
|
126
|
+
let scale = options.scale || [40, 40];
|
|
127
|
+
scale = typeof scale === "number" ? [scale, scale] : scale;
|
|
128
|
+
|
|
129
|
+
if (options.range == null) {
|
|
130
|
+
throw new PerseusError(
|
|
131
|
+
"range should be specified in graph init",
|
|
132
|
+
Errors.Internal,
|
|
133
|
+
);
|
|
168
134
|
}
|
|
169
135
|
|
|
170
|
-
|
|
171
|
-
const y = point[1];
|
|
172
|
-
return [(x - xRange[0]) * xScale, (yRange[1] - y) * yScale];
|
|
173
|
-
};
|
|
136
|
+
this.#bounds = new GraphBounds(...options.range);
|
|
174
137
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
138
|
+
this.#drawingTransform = new DrawingTransform(
|
|
139
|
+
this.raphael,
|
|
140
|
+
scale,
|
|
141
|
+
this.bounds(),
|
|
142
|
+
);
|
|
179
143
|
|
|
180
|
-
const
|
|
181
|
-
const y = point[1];
|
|
182
|
-
return [x / xScale + xRange[0], yRange[1] - y / yScale];
|
|
183
|
-
};
|
|
144
|
+
const [w, h] = this.drawingTransform().canvasDimensions();
|
|
184
145
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
146
|
+
$(this.el).css({
|
|
147
|
+
width: w,
|
|
148
|
+
height: h,
|
|
149
|
+
});
|
|
189
150
|
|
|
190
|
-
|
|
191
|
-
|
|
151
|
+
this.range = options.range;
|
|
152
|
+
this.scale = scale;
|
|
153
|
+
// TODO(benchristel): I don't think dimensions is used. Can we
|
|
154
|
+
// remove it?
|
|
155
|
+
this.dimensions = [w, h];
|
|
156
|
+
this.xpixels = w;
|
|
157
|
+
this.ypixels = h;
|
|
192
158
|
|
|
193
|
-
|
|
194
|
-
const $span = $(span);
|
|
195
|
-
const direction = $span.data("labelDirection");
|
|
196
|
-
let [width, height] = size;
|
|
197
|
-
// This can happen when a span
|
|
198
|
-
// is invisible but we still want to update the CSS. At worst, we will
|
|
199
|
-
// be off by a few pixels instead of in a different position entirely.
|
|
200
|
-
if (width === 0 && height === 0) {
|
|
201
|
-
[width, height] = [1, 1];
|
|
202
|
-
Log.log("Label size was 0x0 in graphie.js; using 1x1 instead");
|
|
203
|
-
}
|
|
204
|
-
$span.css("visibility", "");
|
|
159
|
+
this.isMobile = options.isMobile;
|
|
205
160
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const y = Math.sin(direction);
|
|
161
|
+
return this;
|
|
162
|
+
}
|
|
209
163
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
164
|
+
// Initializes graphie settings for a graph and draws the basic graph
|
|
165
|
+
// features (axes, grid, tick marks, and axis labels)
|
|
166
|
+
// Options expected are:
|
|
167
|
+
// - range: [[a, b], [c, d]] or [a, b]
|
|
168
|
+
// - scale: [a, b] or number
|
|
169
|
+
// - gridOpacity: number (0 - 1)
|
|
170
|
+
// - gridStep: [a, b] or number (relative to units)
|
|
171
|
+
// - tickStep: [a, b] or number (relative to grid steps)
|
|
172
|
+
// - tickLen: [a, b] or number (in pixels)
|
|
173
|
+
// - labelStep: [a, b] or number (relative to tick steps)
|
|
174
|
+
// - yLabelFormat: fn to format label string for y-axis
|
|
175
|
+
// - xLabelFormat: fn to format label string for x-axis
|
|
176
|
+
graphInit(options: any) {
|
|
177
|
+
options = options || {};
|
|
214
178
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
};
|
|
179
|
+
$.each(options, function (prop, val: any) {
|
|
180
|
+
// allow options to be specified by a single number for shorthand if
|
|
181
|
+
// the horizontal and vertical components are the same
|
|
182
|
+
if (
|
|
183
|
+
// @ts-expect-error - TS2339 - Property 'match' does not exist on type 'string | number | symbol'.
|
|
184
|
+
!prop.match(/.*Opacity$/) &&
|
|
185
|
+
prop !== "range" &&
|
|
186
|
+
typeof val === "number"
|
|
187
|
+
) {
|
|
188
|
+
options[prop] = [val, val];
|
|
189
|
+
}
|
|
227
190
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
191
|
+
// allow symmetric ranges to be specified by the absolute values
|
|
192
|
+
if (prop === "range" || prop === "gridRange") {
|
|
193
|
+
if (val.constructor === Array) {
|
|
194
|
+
// but don't mandate symmetric ranges
|
|
195
|
+
if (val[0].constructor !== Array) {
|
|
196
|
+
options[prop] = [
|
|
197
|
+
[-val[0], val[0]],
|
|
198
|
+
[-val[1], val[1]],
|
|
199
|
+
];
|
|
200
|
+
}
|
|
201
|
+
} else if (typeof val === "number") {
|
|
202
|
+
options[prop] = [
|
|
203
|
+
[-val, val],
|
|
204
|
+
[-val, val],
|
|
205
|
+
];
|
|
206
|
+
}
|
|
232
207
|
}
|
|
233
|
-
|
|
234
|
-
return (
|
|
235
|
-
(i === 0 ? "M" : "L") +
|
|
236
|
-
KhanMath.bound(scaled[0]) +
|
|
237
|
-
" " +
|
|
238
|
-
KhanMath.bound(scaled[1])
|
|
239
|
-
);
|
|
240
|
-
}).join("");
|
|
241
|
-
};
|
|
208
|
+
});
|
|
242
209
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
210
|
+
const range = options.range || [
|
|
211
|
+
[-10, 10],
|
|
212
|
+
[-10, 10],
|
|
213
|
+
];
|
|
214
|
+
const gridRange = options.gridRange || options.range;
|
|
215
|
+
const scale = options.scale || [20, 20];
|
|
216
|
+
const grid = options.grid != null ? options.grid : true;
|
|
217
|
+
const gridOpacity = options.gridOpacity || 0.1;
|
|
218
|
+
const gridStep = options.gridStep || [1, 1];
|
|
219
|
+
const axes = options.axes != null ? options.axes : true;
|
|
220
|
+
const axisArrows = options.axisArrows || "";
|
|
221
|
+
const axisOpacity = options.axisOpacity || 1.0;
|
|
222
|
+
const axisCenter = options.axisCenter || [
|
|
223
|
+
Math.min(Math.max(range[0][0], 0), range[0][1]),
|
|
224
|
+
Math.min(Math.max(range[1][0], 0), range[1][1]),
|
|
225
|
+
];
|
|
226
|
+
const axisLabels =
|
|
227
|
+
options.axisLabels != null ? options.axisLabels : false;
|
|
228
|
+
const ticks = options.ticks != null ? options.ticks : true;
|
|
229
|
+
const tickStep = options.tickStep || [2, 2];
|
|
230
|
+
const tickLen = options.tickLen || [5, 5];
|
|
231
|
+
const tickOpacity = options.tickOpacity || 1.0;
|
|
232
|
+
const labels = options.labels || options.labelStep || false;
|
|
233
|
+
const labelStep = options.labelStep || [1, 1];
|
|
234
|
+
const labelOpacity = options.labelOpacity || 1.0;
|
|
235
|
+
let unityLabels = options.unityLabels || false;
|
|
236
|
+
const labelFormat =
|
|
237
|
+
options.labelFormat ||
|
|
238
|
+
function (a: any) {
|
|
239
|
+
return a;
|
|
240
|
+
};
|
|
241
|
+
let xLabelFormat = options.xLabelFormat || labelFormat;
|
|
242
|
+
let yLabelFormat = options.yLabelFormat || labelFormat;
|
|
243
|
+
const realRange = [
|
|
244
|
+
[
|
|
245
|
+
range[0][0] - (range[0][0] > 0 ? 1 : 0),
|
|
246
|
+
range[0][1] + (range[0][1] < 0 ? 1 : 0),
|
|
247
|
+
],
|
|
248
|
+
[
|
|
249
|
+
range[1][0] - (range[1][0] > 0 ? 1 : 0),
|
|
250
|
+
range[1][1] + (range[1][1] < 0 ? 1 : 0),
|
|
251
|
+
],
|
|
252
|
+
] as [Interval, Interval];
|
|
247
253
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const points = _.map(xRange, function (x) {
|
|
251
|
-
return [x, computeParabola(x)];
|
|
252
|
-
});
|
|
253
|
-
// @ts-expect-error - TS2554 - Expected 2 arguments, but got 1.
|
|
254
|
-
return svgPath(points);
|
|
254
|
+
if (!_.isArray(unityLabels)) {
|
|
255
|
+
unityLabels = [unityLabels, unityLabels];
|
|
255
256
|
}
|
|
256
257
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
);
|
|
258
|
+
const minusIgnorer = function (lf: any) {
|
|
259
|
+
return function (a) {
|
|
260
|
+
return (lf(a) + "").replace(/-(\d)/g, "\\llap{-}$1");
|
|
261
|
+
};
|
|
262
|
+
};
|
|
263
263
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const xPoint = xVertex + distToEdge;
|
|
264
|
+
xLabelFormat = minusIgnorer(xLabelFormat);
|
|
265
|
+
yLabelFormat = minusIgnorer(yLabelFormat);
|
|
267
266
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
267
|
+
this.init({
|
|
268
|
+
range: realRange,
|
|
269
|
+
scale: scale,
|
|
270
|
+
isMobile: options.isMobile,
|
|
271
|
+
});
|
|
271
272
|
|
|
272
|
-
//
|
|
273
|
-
|
|
273
|
+
// draw grid
|
|
274
|
+
if (grid) {
|
|
275
|
+
this.grid(gridRange[0], gridRange[1], {
|
|
276
|
+
stroke: options.isMobile ? KhanColors.GRAY_C : "#000000",
|
|
277
|
+
opacity: options.isMobile ? 1 : gridOpacity,
|
|
278
|
+
step: gridStep,
|
|
279
|
+
strokeWidth: options.isMobile ? 1 : 2,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
274
282
|
|
|
275
|
-
//
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
283
|
+
// draw axes
|
|
284
|
+
if (axes) {
|
|
285
|
+
// this is a slight hack until <-> arrowheads work
|
|
286
|
+
if (axisArrows === "<->" || axisArrows === true) {
|
|
287
|
+
const thisGraphie = this;
|
|
288
|
+
this.style(
|
|
289
|
+
{
|
|
290
|
+
stroke: options.isMobile
|
|
291
|
+
? KhanColors.GRAY_G
|
|
292
|
+
: "#000000",
|
|
293
|
+
opacity: options.isMobile ? 1 : axisOpacity,
|
|
294
|
+
strokeWidth: options.isMobile ? 1 : 2,
|
|
295
|
+
arrows: "->",
|
|
296
|
+
},
|
|
297
|
+
function () {
|
|
298
|
+
if (range[1][0] < 0 && range[1][1] > 0) {
|
|
299
|
+
thisGraphie.path([
|
|
300
|
+
axisCenter,
|
|
301
|
+
[gridRange[0][0], axisCenter[1]],
|
|
302
|
+
]);
|
|
303
|
+
thisGraphie.path([
|
|
304
|
+
axisCenter,
|
|
305
|
+
[gridRange[0][1], axisCenter[1]],
|
|
306
|
+
]);
|
|
307
|
+
}
|
|
308
|
+
if (range[0][0] < 0 && range[0][1] > 0) {
|
|
309
|
+
thisGraphie.path([
|
|
310
|
+
axisCenter,
|
|
311
|
+
[axisCenter[0], gridRange[1][0]],
|
|
312
|
+
]);
|
|
313
|
+
thisGraphie.path([
|
|
314
|
+
axisCenter,
|
|
315
|
+
[axisCenter[0], gridRange[1][1]],
|
|
316
|
+
]);
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
);
|
|
279
320
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
321
|
+
// also, we don't support "<-" arrows yet, but why you
|
|
322
|
+
// would want that on your graph is beyond me.
|
|
323
|
+
} else if (axisArrows === "->" || axisArrows === "") {
|
|
324
|
+
const thisGraphie = this;
|
|
325
|
+
this.style(
|
|
326
|
+
{
|
|
327
|
+
stroke: "#000000",
|
|
328
|
+
opacity: axisOpacity,
|
|
329
|
+
strokeWidth: 2,
|
|
330
|
+
arrows: axisArrows,
|
|
331
|
+
},
|
|
332
|
+
function () {
|
|
333
|
+
thisGraphie.path([
|
|
334
|
+
[gridRange[0][0], axisCenter[1]],
|
|
335
|
+
[gridRange[0][1], axisCenter[1]],
|
|
336
|
+
]);
|
|
337
|
+
thisGraphie.path([
|
|
338
|
+
[axisCenter[0], gridRange[1][0]],
|
|
339
|
+
[axisCenter[0], gridRange[1][1]],
|
|
340
|
+
]);
|
|
341
|
+
},
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (axisLabels && axisLabels.length === 2) {
|
|
346
|
+
this.label(
|
|
347
|
+
[gridRange[0][1], axisCenter[1]],
|
|
348
|
+
axisLabels[0],
|
|
349
|
+
"right",
|
|
350
|
+
);
|
|
351
|
+
this.label(
|
|
352
|
+
[axisCenter[0], gridRange[1][1]],
|
|
353
|
+
axisLabels[1],
|
|
354
|
+
"above",
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// draw tick marks
|
|
360
|
+
if (ticks) {
|
|
361
|
+
const halfWidthTicks = options.isMobile;
|
|
362
|
+
const thisGraphie = this;
|
|
363
|
+
this.style(
|
|
364
|
+
{
|
|
365
|
+
stroke: options.isMobile ? KhanColors.GRAY_G : "#000000",
|
|
366
|
+
opacity: options.isMobile ? 1 : tickOpacity,
|
|
367
|
+
strokeWidth: 1,
|
|
368
|
+
},
|
|
369
|
+
function () {
|
|
370
|
+
// horizontal axis
|
|
371
|
+
let step = gridStep[0] * tickStep[0];
|
|
372
|
+
let len = tickLen[0] / scale[1];
|
|
373
|
+
let start = gridRange[0][0];
|
|
374
|
+
let stop = gridRange[0][1];
|
|
375
|
+
|
|
376
|
+
if (range[1][0] < 0 && range[1][1] > 0) {
|
|
377
|
+
for (
|
|
378
|
+
let x = step + axisCenter[0];
|
|
379
|
+
x <= stop;
|
|
380
|
+
x += step
|
|
381
|
+
) {
|
|
382
|
+
if (x < stop || !axisArrows) {
|
|
383
|
+
thisGraphie.line(
|
|
384
|
+
[x, -len + axisCenter[1]],
|
|
385
|
+
[
|
|
386
|
+
x,
|
|
387
|
+
halfWidthTicks
|
|
388
|
+
? 0
|
|
389
|
+
: len + axisCenter[1],
|
|
390
|
+
],
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
for (
|
|
396
|
+
let x = -step + axisCenter[0];
|
|
397
|
+
x >= start;
|
|
398
|
+
x -= step
|
|
399
|
+
) {
|
|
400
|
+
if (x > start || !axisArrows) {
|
|
401
|
+
thisGraphie.line(
|
|
402
|
+
[x, -len + axisCenter[1]],
|
|
403
|
+
[
|
|
404
|
+
x,
|
|
405
|
+
halfWidthTicks
|
|
406
|
+
? 0
|
|
407
|
+
: len + axisCenter[1],
|
|
408
|
+
],
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// vertical axis
|
|
415
|
+
step = gridStep[1] * tickStep[1];
|
|
416
|
+
len = tickLen[1] / scale[0];
|
|
417
|
+
start = gridRange[1][0];
|
|
418
|
+
stop = gridRange[1][1];
|
|
419
|
+
|
|
420
|
+
if (range[0][0] < 0 && range[0][1] > 0) {
|
|
421
|
+
for (
|
|
422
|
+
let y = step + axisCenter[1];
|
|
423
|
+
y <= stop;
|
|
424
|
+
y += step
|
|
425
|
+
) {
|
|
426
|
+
if (y < stop || !axisArrows) {
|
|
427
|
+
// @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
|
|
428
|
+
this.line(
|
|
429
|
+
[-len + axisCenter[0], y],
|
|
430
|
+
[
|
|
431
|
+
halfWidthTicks
|
|
432
|
+
? 0
|
|
433
|
+
: len + axisCenter[0],
|
|
434
|
+
y,
|
|
435
|
+
],
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
for (
|
|
441
|
+
let y = -step + axisCenter[1];
|
|
442
|
+
y >= start;
|
|
443
|
+
y -= step
|
|
444
|
+
) {
|
|
445
|
+
if (y > start || !axisArrows) {
|
|
446
|
+
// @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
|
|
447
|
+
this.line(
|
|
448
|
+
[-len + axisCenter[0], y],
|
|
449
|
+
[
|
|
450
|
+
halfWidthTicks
|
|
451
|
+
? 0
|
|
452
|
+
: len + axisCenter[0],
|
|
453
|
+
y,
|
|
454
|
+
],
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
},
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// draw axis labels
|
|
464
|
+
if (labels) {
|
|
465
|
+
const thisGraphie = this;
|
|
466
|
+
this.style(
|
|
467
|
+
{
|
|
468
|
+
stroke: options.isMobile ? KhanColors.GRAY_G : "#000000",
|
|
469
|
+
opacity: options.isMobile ? 1 : labelOpacity,
|
|
470
|
+
},
|
|
471
|
+
function () {
|
|
472
|
+
// horizontal axis
|
|
473
|
+
let step = gridStep[0] * tickStep[0] * labelStep[0];
|
|
474
|
+
let start = gridRange[0][0];
|
|
475
|
+
let stop = gridRange[0][1];
|
|
476
|
+
const xAxisPosition = axisCenter[0] < 0 ? "above" : "below";
|
|
477
|
+
const yAxisPosition = axisCenter[0] < 0 ? "right" : "left";
|
|
478
|
+
const xShowZero =
|
|
479
|
+
axisCenter[0] === 0 && axisCenter[1] !== 0;
|
|
480
|
+
const yShowZero =
|
|
481
|
+
axisCenter[0] !== 0 && axisCenter[1] === 0;
|
|
482
|
+
const axisOffCenter =
|
|
483
|
+
axisCenter[0] !== 0 || axisCenter[1] !== 0;
|
|
484
|
+
const showUnityX = unityLabels[0] || axisOffCenter;
|
|
485
|
+
const showUnityY = unityLabels[1] || axisOffCenter;
|
|
486
|
+
|
|
487
|
+
// positive x-axis
|
|
488
|
+
for (
|
|
489
|
+
let x = (xShowZero ? 0 : step) + axisCenter[0];
|
|
490
|
+
x <= stop;
|
|
491
|
+
x += step
|
|
492
|
+
) {
|
|
493
|
+
if (x < stop || !axisArrows) {
|
|
494
|
+
thisGraphie.label(
|
|
495
|
+
[x, axisCenter[1]],
|
|
496
|
+
xLabelFormat(x),
|
|
497
|
+
xAxisPosition,
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// negative x-axis
|
|
503
|
+
for (
|
|
504
|
+
let x = -step * (showUnityX ? 1 : 2) + axisCenter[0];
|
|
505
|
+
x >= start;
|
|
506
|
+
x -= step
|
|
507
|
+
) {
|
|
508
|
+
if (x > start || !axisArrows) {
|
|
509
|
+
thisGraphie.label(
|
|
510
|
+
[x, axisCenter[1]],
|
|
511
|
+
xLabelFormat(x),
|
|
512
|
+
xAxisPosition,
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
step = gridStep[1] * tickStep[1] * labelStep[1];
|
|
518
|
+
start = gridRange[1][0];
|
|
519
|
+
stop = gridRange[1][1];
|
|
520
|
+
|
|
521
|
+
// positive y-axis
|
|
522
|
+
for (
|
|
523
|
+
let y = (yShowZero ? 0 : step) + axisCenter[1];
|
|
524
|
+
y <= stop;
|
|
525
|
+
y += step
|
|
526
|
+
) {
|
|
527
|
+
if (y < stop || !axisArrows) {
|
|
528
|
+
thisGraphie.label(
|
|
529
|
+
[axisCenter[0], y],
|
|
530
|
+
yLabelFormat(y),
|
|
531
|
+
yAxisPosition,
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// negative y-axis
|
|
537
|
+
for (
|
|
538
|
+
let y = -step * (showUnityY ? 1 : 2) + axisCenter[1];
|
|
539
|
+
y >= start;
|
|
540
|
+
y -= step
|
|
541
|
+
) {
|
|
542
|
+
if (y > start || !axisArrows) {
|
|
543
|
+
thisGraphie.label(
|
|
544
|
+
[axisCenter[0], y],
|
|
545
|
+
yLabelFormat(y),
|
|
546
|
+
yAxisPosition,
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
},
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
drawingTransform(): DrawingTransform {
|
|
556
|
+
if (this.#drawingTransform == null) {
|
|
557
|
+
throw new Error(
|
|
558
|
+
"Can't get drawingTransform of an uninitialized Graphie",
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
return this.#drawingTransform;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
bounds(): GraphBounds {
|
|
565
|
+
if (this.#bounds == null) {
|
|
566
|
+
throw new Error("Can't get bounds of an uninitialized Graphie");
|
|
567
|
+
}
|
|
568
|
+
return this.#bounds;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
style(attrs: any, fn: any) {
|
|
572
|
+
const processed = this.processAttributes(attrs);
|
|
573
|
+
|
|
574
|
+
if (typeof fn === "function") {
|
|
575
|
+
const oldStyle = this.currentStyle;
|
|
576
|
+
this.currentStyle = $.extend({}, this.currentStyle, processed);
|
|
577
|
+
const result = fn.call(this);
|
|
578
|
+
this.currentStyle = oldStyle;
|
|
579
|
+
return result;
|
|
580
|
+
}
|
|
581
|
+
$.extend(this.currentStyle, processed);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
label(point: any, text: any, direction: any, latex?: any) {}
|
|
585
|
+
|
|
586
|
+
grid(xr: any, yr: any, styleAttributes: any) {}
|
|
587
|
+
|
|
588
|
+
// path is a stub that gets overwritten with a function from drawingTools
|
|
589
|
+
// in createGraphie
|
|
590
|
+
path(points: any) {}
|
|
591
|
+
|
|
592
|
+
// line is a stub that gets overwritten with a function from drawingTools
|
|
593
|
+
// in createGraphie
|
|
594
|
+
line(start: any, end: any) {}
|
|
595
|
+
|
|
596
|
+
svgPath = (points: any, alreadyScaled) => {
|
|
597
|
+
return $.map(points, (point, i) => {
|
|
598
|
+
if (point === true) {
|
|
599
|
+
return "z";
|
|
600
|
+
}
|
|
601
|
+
const scaled = alreadyScaled ? point : this.scalePoint(point);
|
|
602
|
+
return (
|
|
603
|
+
(i === 0 ? "M" : "L") +
|
|
604
|
+
KhanMath.bound(scaled[0]) +
|
|
605
|
+
" " +
|
|
606
|
+
KhanMath.bound(scaled[1])
|
|
607
|
+
);
|
|
608
|
+
}).join("");
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
svgParabolaPath = (a: any, b: any, c: any) => {
|
|
612
|
+
const computeParabola = function (x) {
|
|
613
|
+
return (a * x + b) * x + c;
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
// If points are collinear, plot a line instead
|
|
617
|
+
if (a === 0) {
|
|
618
|
+
const points = [
|
|
619
|
+
[this.bounds().xMin, computeParabola(this.bounds().xMin)],
|
|
620
|
+
[this.bounds().xMax, computeParabola(this.bounds().xMax)],
|
|
621
|
+
];
|
|
622
|
+
// @ts-expect-error - TS2554 - Expected 2 arguments, but got 1.
|
|
623
|
+
return this.svgPath(points);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Calculate x coordinates of points on parabola
|
|
627
|
+
const xVertex = -b / (2 * a);
|
|
628
|
+
const distToEdge = Math.max(
|
|
629
|
+
Math.abs(xVertex - this.bounds().xMin),
|
|
630
|
+
Math.abs(xVertex - this.bounds().xMax),
|
|
631
|
+
);
|
|
632
|
+
|
|
633
|
+
// To guarantee that drawn parabola to spans the viewport, use a point
|
|
634
|
+
// on the edge of the graph furtherest from the vertex
|
|
635
|
+
const xPoint = xVertex + distToEdge;
|
|
636
|
+
|
|
637
|
+
// Compute parabola and other point on the curve
|
|
638
|
+
const vertex = [xVertex, computeParabola(xVertex)];
|
|
639
|
+
const point = [xPoint, computeParabola(xPoint)];
|
|
640
|
+
|
|
641
|
+
// Calculate SVG 'control' point, defined by spec
|
|
642
|
+
const control = [vertex[0], vertex[1] - (point[1] - vertex[1])];
|
|
643
|
+
|
|
644
|
+
// Calculate mirror points across parabola's axis of symmetry
|
|
645
|
+
const dx = Math.abs(vertex[0] - point[0]);
|
|
646
|
+
const left = [vertex[0] - dx, point[1]];
|
|
647
|
+
const right = [vertex[0] + dx, point[1]];
|
|
648
|
+
|
|
649
|
+
// Scale and bound
|
|
650
|
+
// @ts-expect-error - TS2345 - Argument of type '(point: number | Coord) => Coord' is not assignable to parameter of type 'Iteratee<any[][], any, any[]>'.
|
|
651
|
+
const points = _.map([left, control, right], this.scalePoint);
|
|
652
|
+
const values = _.map(_.flatten(points), KhanMath.bound);
|
|
653
|
+
return (
|
|
654
|
+
"M" +
|
|
655
|
+
values[0] +
|
|
656
|
+
"," +
|
|
657
|
+
values[1] +
|
|
658
|
+
" Q" +
|
|
290
659
|
values[2] +
|
|
291
660
|
"," +
|
|
292
661
|
values[3] +
|
|
@@ -297,7 +666,7 @@ GraphUtils.createGraphie = function (el: any) {
|
|
|
297
666
|
);
|
|
298
667
|
};
|
|
299
668
|
|
|
300
|
-
|
|
669
|
+
svgSinusoidPath = (a, b, c: any, d: any) => {
|
|
301
670
|
// Plot a sinusoid of the form: f(x) = a * sin(b * x - c) + d
|
|
302
671
|
const quarterPeriod = Math.abs(Math.PI / (2 * b));
|
|
303
672
|
|
|
@@ -309,7 +678,7 @@ GraphUtils.createGraphie = function (el: any) {
|
|
|
309
678
|
return a * b * Math.cos(c - b * x);
|
|
310
679
|
};
|
|
311
680
|
|
|
312
|
-
const coordsForOffset =
|
|
681
|
+
const coordsForOffset = (initial: number, i: number) => {
|
|
313
682
|
// Return the cubic coordinates (including the two anchor and two
|
|
314
683
|
// control points) for the ith portion of the sinusoid.
|
|
315
684
|
const x0 = initial + quarterPeriod * i;
|
|
@@ -332,16 +701,16 @@ GraphUtils.createGraphie = function (el: any) {
|
|
|
332
701
|
|
|
333
702
|
// Zip and scale
|
|
334
703
|
// @ts-expect-error - TS2345 - Argument of type '(point: number | Coord) => Coord' is not assignable to parameter of type 'Iteratee<any[][], any, any[]>'.
|
|
335
|
-
return _.map(_.zip(xCoords, yCoords), scalePoint);
|
|
704
|
+
return _.map(_.zip(xCoords, yCoords), this.scalePoint);
|
|
336
705
|
};
|
|
337
706
|
|
|
338
707
|
// How many quarter-periods do we need to span the graph?
|
|
339
|
-
const extent =
|
|
708
|
+
const extent = this.bounds().width();
|
|
340
709
|
const numQuarterPeriods = Math.ceil(extent / quarterPeriod) + 1;
|
|
341
710
|
|
|
342
|
-
// Find starting coordinate: first anchor point curve left of
|
|
711
|
+
// Find starting coordinate: first anchor point curve left of bounds.xMin
|
|
343
712
|
let initial = c / b;
|
|
344
|
-
const distToEdge = initial -
|
|
713
|
+
const distToEdge = initial - this.bounds().xMin;
|
|
345
714
|
initial -= quarterPeriod * Math.ceil(distToEdge / quarterPeriod);
|
|
346
715
|
|
|
347
716
|
// First portion of path is special-case, requiring move-to ('M')
|
|
@@ -383,34 +752,36 @@ GraphUtils.createGraphie = function (el: any) {
|
|
|
383
752
|
return path;
|
|
384
753
|
};
|
|
385
754
|
|
|
386
|
-
|
|
387
|
-
|
|
755
|
+
scalePoint = (point: number | Coord): Coord => {
|
|
756
|
+
return this.drawingTransform().scalePoint(point);
|
|
757
|
+
};
|
|
388
758
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
if (typeof scale === "number") {
|
|
393
|
-
scale = [scale, scale];
|
|
394
|
-
}
|
|
759
|
+
scaleVector = (point: number | Coord) => {
|
|
760
|
+
return this.drawingTransform().scaleVector(point);
|
|
761
|
+
};
|
|
395
762
|
|
|
396
|
-
|
|
397
|
-
|
|
763
|
+
unscalePoint = (point: Coord) => {
|
|
764
|
+
return this.drawingTransform().unscalePoint(point);
|
|
765
|
+
};
|
|
398
766
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
767
|
+
unscaleVector = (point: Coord) => {
|
|
768
|
+
return this.drawingTransform().unscaleVector(point);
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
processAttributes(attrs: any) {
|
|
772
|
+
const transformers = {
|
|
773
|
+
scale: (scale) => {
|
|
774
|
+
this.drawingTransform().setScale(scale);
|
|
404
775
|
},
|
|
405
776
|
|
|
406
|
-
clipRect:
|
|
777
|
+
clipRect: (pair) => {
|
|
407
778
|
const point = pair[0];
|
|
408
779
|
const size = pair[1];
|
|
409
780
|
point[1] += size[1]; // because our coordinates are flipped
|
|
410
781
|
|
|
411
782
|
return {
|
|
412
|
-
"clip-rect": scalePoint(point)
|
|
413
|
-
.concat(scaleVector(size))
|
|
783
|
+
"clip-rect": this.scalePoint(point)
|
|
784
|
+
.concat(this.scaleVector(size))
|
|
414
785
|
.join(" "),
|
|
415
786
|
};
|
|
416
787
|
},
|
|
@@ -419,16 +790,16 @@ GraphUtils.createGraphie = function (el: any) {
|
|
|
419
790
|
return {"stroke-width": parseFloat(val)};
|
|
420
791
|
},
|
|
421
792
|
|
|
422
|
-
rx:
|
|
423
|
-
return {rx: scaleVector([val, 0])[0]};
|
|
793
|
+
rx: (val) => {
|
|
794
|
+
return {rx: this.scaleVector([val, 0])[0]};
|
|
424
795
|
},
|
|
425
796
|
|
|
426
|
-
ry:
|
|
427
|
-
return {ry: scaleVector([0, val])[1]};
|
|
797
|
+
ry: (val) => {
|
|
798
|
+
return {ry: this.scaleVector([0, val])[1]};
|
|
428
799
|
},
|
|
429
800
|
|
|
430
|
-
r:
|
|
431
|
-
const scaled = scaleVector([val, val]);
|
|
801
|
+
r: (val) => {
|
|
802
|
+
const scaled = this.scaleVector([val, val]);
|
|
432
803
|
return {rx: scaled[0], ry: scaled[1]};
|
|
433
804
|
},
|
|
434
805
|
} as const;
|
|
@@ -450,8 +821,77 @@ GraphUtils.createGraphie = function (el: any) {
|
|
|
450
821
|
});
|
|
451
822
|
|
|
452
823
|
return processed;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
GraphUtils.Graphie = Graphie;
|
|
828
|
+
|
|
829
|
+
const labelDirections = {
|
|
830
|
+
center: [-0.5, -0.5],
|
|
831
|
+
above: [-0.5, -1.0],
|
|
832
|
+
"above right": [0.0, -1.0],
|
|
833
|
+
right: [0.0, -0.5],
|
|
834
|
+
"below right": [0.0, 0.0],
|
|
835
|
+
below: [-0.5, 0.0],
|
|
836
|
+
"below left": [-1.0, 0.0],
|
|
837
|
+
left: [-1.0, -0.5],
|
|
838
|
+
"above left": [-1.0, -1.0],
|
|
839
|
+
} as const;
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Safari applies some SVG-specific styles to things that are not SVGs, so we
|
|
843
|
+
* need to exclude those styles from things that are not SVGs.
|
|
844
|
+
*
|
|
845
|
+
* To see this behavior in action, open https://codepen.io/anon/pen/zENEoa in
|
|
846
|
+
* Safari.
|
|
847
|
+
*
|
|
848
|
+
* Usage `$.extend({}, someStyles, SVG_SPECIFIC_STYLE_MASK)`
|
|
849
|
+
*/
|
|
850
|
+
const SVG_SPECIFIC_STYLE_MASK = {
|
|
851
|
+
"stroke-width": null,
|
|
852
|
+
} as const;
|
|
853
|
+
|
|
854
|
+
GraphUtils.createGraphie = function (el: any) {
|
|
855
|
+
const thisGraphie = new Graphie(el);
|
|
856
|
+
|
|
857
|
+
const setLabelMargins = function (span: any, size: Array<any>) {
|
|
858
|
+
const $span = $(span);
|
|
859
|
+
const direction = $span.data("labelDirection");
|
|
860
|
+
let [width, height] = size;
|
|
861
|
+
// This can happen when a span
|
|
862
|
+
// is invisible but we still want to update the CSS. At worst, we will
|
|
863
|
+
// be off by a few pixels instead of in a different position entirely.
|
|
864
|
+
if (width === 0 && height === 0) {
|
|
865
|
+
[width, height] = [1, 1];
|
|
866
|
+
Log.log("Label size was 0x0 in graphie.js; using 1x1 instead");
|
|
867
|
+
}
|
|
868
|
+
$span.css("visibility", "");
|
|
869
|
+
|
|
870
|
+
if (typeof direction === "number") {
|
|
871
|
+
const x = Math.cos(direction);
|
|
872
|
+
const y = Math.sin(direction);
|
|
873
|
+
|
|
874
|
+
const scale = Math.min(
|
|
875
|
+
width / 2 / Math.abs(x),
|
|
876
|
+
height / 2 / Math.abs(y),
|
|
877
|
+
);
|
|
878
|
+
|
|
879
|
+
$span.css({
|
|
880
|
+
marginLeft: -width / 2 + x * scale,
|
|
881
|
+
marginTop: -height / 2 - y * scale,
|
|
882
|
+
});
|
|
883
|
+
} else {
|
|
884
|
+
const multipliers = labelDirections[direction || "center"];
|
|
885
|
+
$span.css({
|
|
886
|
+
marginLeft: Math.round(width * multipliers[0]),
|
|
887
|
+
marginTop: Math.round(height * multipliers[1]),
|
|
888
|
+
});
|
|
889
|
+
}
|
|
453
890
|
};
|
|
454
891
|
|
|
892
|
+
// `svgPath` is independent of graphie range, so we export it independently
|
|
893
|
+
GraphUtils.svgPath = thisGraphie.svgPath;
|
|
894
|
+
|
|
455
895
|
const addArrowheads = function arrows(path: any) {
|
|
456
896
|
const type = path.constructor.prototype;
|
|
457
897
|
|
|
@@ -463,9 +903,9 @@ GraphUtils.createGraphie = function (el: any) {
|
|
|
463
903
|
const w = path.attr("stroke-width");
|
|
464
904
|
const s = 0.6 + 0.4 * w;
|
|
465
905
|
const l = path.getTotalLength();
|
|
466
|
-
const set = raphael.set();
|
|
467
|
-
const head = raphael.path(
|
|
468
|
-
|
|
906
|
+
const set = thisGraphie.raphael.set();
|
|
907
|
+
const head = thisGraphie.raphael.path(
|
|
908
|
+
thisGraphie.isMobile
|
|
469
909
|
? "M-4,4 C-4,4 -0.25,0 -0.25,0 C-0.25,0 -4,-4 -4,-4"
|
|
470
910
|
: "M-3 4 C-2.75 2.5 0 0.25 0.75 0C0 -0.25 -2.75 -2.5 -3 -4",
|
|
471
911
|
);
|
|
@@ -482,13 +922,13 @@ GraphUtils.createGraphie = function (el: any) {
|
|
|
482
922
|
delete attrs.path;
|
|
483
923
|
|
|
484
924
|
let subpath = path.getSubpath(0, l - 0.75 * s);
|
|
485
|
-
subpath = raphael.path(subpath).attr(attrs);
|
|
925
|
+
subpath = thisGraphie.raphael.path(subpath).attr(attrs);
|
|
486
926
|
subpath.arrowheadsDrawn = true;
|
|
487
927
|
path.remove();
|
|
488
928
|
|
|
489
929
|
// For some unknown reason 0 doesn't work for the rotation
|
|
490
930
|
// origin so we use a tiny number.
|
|
491
|
-
head.rotate(angle,
|
|
931
|
+
head.rotate(angle, thisGraphie.isMobile ? 1e-5 : 0.75, 0)
|
|
492
932
|
.scale(s, s, 0.75, 0)
|
|
493
933
|
.translate(almostTheEnd.x, almostTheEnd.y)
|
|
494
934
|
.attr(attrs)
|
|
@@ -501,1117 +941,586 @@ GraphUtils.createGraphie = function (el: any) {
|
|
|
501
941
|
set.push(subpath);
|
|
502
942
|
set.push(head);
|
|
503
943
|
return set;
|
|
504
|
-
}
|
|
505
|
-
} else if (type === Raphael.st) {
|
|
506
|
-
for (let i = 0, l = path.items.length; i < l; i++) {
|
|
507
|
-
arrows(path.items[i]);
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
return path;
|
|
511
|
-
};
|
|
512
|
-
|
|
513
|
-
const drawingTools: any = {
|
|
514
|
-
circle: function (center, radius) {
|
|
515
|
-
return raphael.ellipse(
|
|
516
|
-
...scalePoint(center).concat(scaleVector([radius, radius])),
|
|
517
|
-
);
|
|
518
|
-
},
|
|
519
|
-
|
|
520
|
-
// (x, y) is coordinate of bottom left corner
|
|
521
|
-
rect: function (x, y, width, height) {
|
|
522
|
-
// Raphael needs (x, y) to be coordinate of upper left corner
|
|
523
|
-
const corner = scalePoint([x, y + height]);
|
|
524
|
-
const dims = scaleVector([width, height]);
|
|
525
|
-
const elem = raphael.rect(...corner.concat(dims));
|
|
526
|
-
|
|
527
|
-
if (graphie.isMobile) {
|
|
528
|
-
elem.node.style.shapeRendering = "crispEdges";
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
return elem;
|
|
532
|
-
},
|
|
533
|
-
|
|
534
|
-
ellipse: function (center, radii) {
|
|
535
|
-
return raphael.ellipse(
|
|
536
|
-
...scalePoint(center).concat(scaleVector(radii)),
|
|
537
|
-
);
|
|
538
|
-
},
|
|
539
|
-
|
|
540
|
-
fixedEllipse: function (
|
|
541
|
-
center: number | Coord,
|
|
542
|
-
// Different type than Coord, this is radiusX, radiusY
|
|
543
|
-
radii: number | [number, number],
|
|
544
|
-
maxScale: number,
|
|
545
|
-
padding: number,
|
|
546
|
-
) {
|
|
547
|
-
// Scale point and radius
|
|
548
|
-
const scaledPoint = scalePoint(center);
|
|
549
|
-
const scaledRadii = scaleVector(radii);
|
|
550
|
-
|
|
551
|
-
const width = 2 * scaledRadii[0] * maxScale + padding;
|
|
552
|
-
const height = 2 * scaledRadii[1] * maxScale + padding;
|
|
553
|
-
|
|
554
|
-
// Calculate absolute left, top
|
|
555
|
-
const left = scaledPoint[0] - width / 2;
|
|
556
|
-
const top = scaledPoint[1] - height / 2;
|
|
557
|
-
|
|
558
|
-
// Wrap in <div>
|
|
559
|
-
const wrapper = document.createElement("div");
|
|
560
|
-
$(wrapper).css({
|
|
561
|
-
position: "absolute",
|
|
562
|
-
width: width + "px",
|
|
563
|
-
height: height + "px",
|
|
564
|
-
left: left + "px",
|
|
565
|
-
top: top + "px",
|
|
566
|
-
});
|
|
567
|
-
// wrapper.setAttribute("data-graphie-type", "ellipse");
|
|
568
|
-
|
|
569
|
-
// Create Raphael canvas
|
|
570
|
-
const localRaphael = Raphael(wrapper, width, height);
|
|
571
|
-
const visibleShape = localRaphael.ellipse(
|
|
572
|
-
width / 2,
|
|
573
|
-
height / 2,
|
|
574
|
-
scaledRadii[0],
|
|
575
|
-
scaledRadii[1],
|
|
576
|
-
);
|
|
577
|
-
|
|
578
|
-
return {
|
|
579
|
-
wrapper: wrapper,
|
|
580
|
-
visibleShape: visibleShape,
|
|
581
|
-
};
|
|
582
|
-
},
|
|
583
|
-
|
|
584
|
-
arc: function (center, radius, startAngle, endAngle, sector) {
|
|
585
|
-
startAngle = ((startAngle % 360) + 360) % 360;
|
|
586
|
-
endAngle = ((endAngle % 360) + 360) % 360;
|
|
587
|
-
|
|
588
|
-
const cent = scalePoint(center);
|
|
589
|
-
const radii = scaleVector(radius);
|
|
590
|
-
const startVector = polar(radius, startAngle);
|
|
591
|
-
const endVector = polar(radius, endAngle);
|
|
592
|
-
|
|
593
|
-
const startPoint = scalePoint([
|
|
594
|
-
center[0] + startVector[0],
|
|
595
|
-
center[1] + startVector[1],
|
|
596
|
-
]);
|
|
597
|
-
const endPoint = scalePoint([
|
|
598
|
-
center[0] + endVector[0],
|
|
599
|
-
center[1] + endVector[1],
|
|
600
|
-
]);
|
|
601
|
-
|
|
602
|
-
const largeAngle =
|
|
603
|
-
(((endAngle - startAngle) % 360) + 360) % 360 > 180;
|
|
604
|
-
|
|
605
|
-
return raphael.path(
|
|
606
|
-
"M" +
|
|
607
|
-
startPoint.join(" ") +
|
|
608
|
-
"A" +
|
|
609
|
-
radii.join(" ") +
|
|
610
|
-
" 0 " + // ellipse rotation
|
|
611
|
-
(largeAngle ? 1 : 0) +
|
|
612
|
-
" 0 " + // sweep flag
|
|
613
|
-
endPoint.join(" ") +
|
|
614
|
-
(sector ? "L" + cent.join(" ") + "z" : ""),
|
|
615
|
-
);
|
|
616
|
-
},
|
|
617
|
-
|
|
618
|
-
path: function (points) {
|
|
619
|
-
// @ts-expect-error - TS2554 - Expected 2 arguments, but got 1.
|
|
620
|
-
const p = raphael.path(svgPath(points));
|
|
621
|
-
p.graphiePath = points;
|
|
622
|
-
|
|
623
|
-
return p;
|
|
624
|
-
},
|
|
625
|
-
|
|
626
|
-
fixedPath: function (points, center, createPath) {
|
|
627
|
-
points = _.map(points, scalePoint);
|
|
628
|
-
center = center ? scalePoint(center) : null;
|
|
629
|
-
createPath = createPath || svgPath;
|
|
630
|
-
|
|
631
|
-
const pathLeft = _.min(_.pluck(points, 0));
|
|
632
|
-
const pathRight = _.max(_.pluck(points, 0));
|
|
633
|
-
const pathTop = _.min(_.pluck(points, 1));
|
|
634
|
-
const pathBottom = _.max(_.pluck(points, 1));
|
|
635
|
-
|
|
636
|
-
// Apply padding to line
|
|
637
|
-
const padding = [4, 4];
|
|
638
|
-
|
|
639
|
-
// Calculate and apply additional offset
|
|
640
|
-
const extraOffset = [pathLeft, pathTop];
|
|
641
|
-
|
|
642
|
-
// Apply padding and offset to points
|
|
643
|
-
points = _.map(points, function (point) {
|
|
644
|
-
return kvector.add(
|
|
645
|
-
kvector.subtract(point, extraOffset),
|
|
646
|
-
kvector.scale(padding, 0.5),
|
|
647
|
-
);
|
|
648
|
-
});
|
|
649
|
-
|
|
650
|
-
// Calculate <div> dimensions
|
|
651
|
-
const width = pathRight - pathLeft + padding[0];
|
|
652
|
-
const height = pathBottom - pathTop + padding[1];
|
|
653
|
-
const left = extraOffset[0] - padding[0] / 2;
|
|
654
|
-
const top = extraOffset[1] - padding[1] / 2;
|
|
655
|
-
|
|
656
|
-
// Create <div>
|
|
657
|
-
const wrapper = document.createElement("div");
|
|
658
|
-
$(wrapper).css({
|
|
659
|
-
position: "absolute",
|
|
660
|
-
width: width + "px",
|
|
661
|
-
height: height + "px",
|
|
662
|
-
left: left + "px",
|
|
663
|
-
top: top + "px",
|
|
664
|
-
// If user specified a center, set it
|
|
665
|
-
// NOTE(kevinb): jQuery doesn't like that `transformOrigin `could be `null`
|
|
666
|
-
// so we cast to `any` here.
|
|
667
|
-
transformOrigin: center
|
|
668
|
-
? width / 2 +
|
|
669
|
-
center[0] +
|
|
670
|
-
"px " +
|
|
671
|
-
(height / 2 + center[1]) +
|
|
672
|
-
"px"
|
|
673
|
-
: null,
|
|
674
|
-
} as any);
|
|
675
|
-
|
|
676
|
-
// Create Raphael canvas
|
|
677
|
-
const localRaphael = Raphael(wrapper, width, height);
|
|
678
|
-
|
|
679
|
-
// Calculate path
|
|
680
|
-
const visibleShape = localRaphael.path(createPath(points));
|
|
681
|
-
|
|
682
|
-
return {
|
|
683
|
-
wrapper: wrapper,
|
|
684
|
-
visibleShape: visibleShape,
|
|
685
|
-
};
|
|
686
|
-
},
|
|
687
|
-
|
|
688
|
-
scaledPath: function (points) {
|
|
689
|
-
const p = raphael.path(svgPath(points, /* alreadyScaled */ true));
|
|
690
|
-
p.graphiePath = points;
|
|
691
|
-
return p;
|
|
692
|
-
},
|
|
693
|
-
|
|
694
|
-
line: function (start, end) {
|
|
695
|
-
const l = this.path([start, end]);
|
|
696
|
-
|
|
697
|
-
if (graphie.isMobile) {
|
|
698
|
-
l.node.style.shapeRendering = "crispEdges";
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
return l;
|
|
702
|
-
},
|
|
703
|
-
|
|
704
|
-
parabola: function (a, b, c) {
|
|
705
|
-
// Plot a parabola of the form: f(x) = (a * x + b) * x + c
|
|
706
|
-
return raphael.path(svgParabolaPath(a, b, c));
|
|
707
|
-
},
|
|
708
|
-
|
|
709
|
-
fixedLine: function (start, end, thickness) {
|
|
710
|
-
// Apply padding to line
|
|
711
|
-
const padding = [thickness, thickness];
|
|
712
|
-
|
|
713
|
-
// Scale points to get values in pixels
|
|
714
|
-
start = scalePoint(start);
|
|
715
|
-
end = scalePoint(end);
|
|
716
|
-
|
|
717
|
-
// Calculate and apply additional offset
|
|
718
|
-
const extraOffset = [
|
|
719
|
-
Math.min(start[0], end[0]),
|
|
720
|
-
Math.min(start[1], end[1]),
|
|
721
|
-
];
|
|
722
|
-
|
|
723
|
-
// Apply padding and offset to start, end points
|
|
724
|
-
start = kvector.add(
|
|
725
|
-
kvector.subtract(start, extraOffset),
|
|
726
|
-
kvector.scale(padding, 0.5),
|
|
727
|
-
);
|
|
728
|
-
end = kvector.add(
|
|
729
|
-
kvector.subtract(end, extraOffset),
|
|
730
|
-
kvector.scale(padding, 0.5),
|
|
731
|
-
);
|
|
732
|
-
|
|
733
|
-
// Calculate <div> dimensions
|
|
734
|
-
const left = extraOffset[0] - padding[0] / 2;
|
|
735
|
-
const top = extraOffset[1] - padding[1] / 2;
|
|
736
|
-
const width = Math.abs(start[0] - end[0]) + padding[0];
|
|
737
|
-
const height = Math.abs(start[1] - end[1]) + padding[1];
|
|
738
|
-
|
|
739
|
-
// Create <div>
|
|
740
|
-
const wrapper = document.createElement("div");
|
|
741
|
-
$(wrapper).css({
|
|
742
|
-
position: "absolute",
|
|
743
|
-
width: width + "px",
|
|
744
|
-
height: height + "px",
|
|
745
|
-
left: left + "px",
|
|
746
|
-
top: top + "px",
|
|
747
|
-
// Outsiders should feel like the line's 'origin' (i.e., for
|
|
748
|
-
// rotation) is the starting point
|
|
749
|
-
transformOrigin: start[0] + "px " + start[1] + "px",
|
|
750
|
-
});
|
|
751
|
-
|
|
752
|
-
// Create Raphael canvas
|
|
753
|
-
const localRaphael = Raphael(wrapper, width, height);
|
|
754
|
-
|
|
755
|
-
// Calculate path
|
|
756
|
-
const path =
|
|
757
|
-
"M" +
|
|
758
|
-
start[0] +
|
|
759
|
-
" " +
|
|
760
|
-
start[1] +
|
|
761
|
-
" " +
|
|
762
|
-
"L" +
|
|
763
|
-
end[0] +
|
|
764
|
-
" " +
|
|
765
|
-
end[1];
|
|
766
|
-
const visibleShape = localRaphael.path(path);
|
|
767
|
-
visibleShape.graphiePath = [start, end];
|
|
768
|
-
|
|
769
|
-
return {
|
|
770
|
-
wrapper: wrapper,
|
|
771
|
-
visibleShape: visibleShape,
|
|
772
|
-
};
|
|
773
|
-
},
|
|
774
|
-
|
|
775
|
-
sinusoid: function (a, b, c, d) {
|
|
776
|
-
// Plot a sinusoid of the form: f(x) = a * sin(b * x - c) + d
|
|
777
|
-
return raphael.path(svgSinusoidPath(a, b, c, d));
|
|
778
|
-
},
|
|
779
|
-
|
|
780
|
-
grid: function (xr, yr) {
|
|
781
|
-
const step: any = currentStyle.step || [1, 1];
|
|
782
|
-
const set = raphael.set();
|
|
783
|
-
|
|
784
|
-
let x = step[0] * Math.ceil(xr[0] / step[0]);
|
|
785
|
-
for (; x <= xr[1]; x += step[0]) {
|
|
786
|
-
set.push(this.line([x, yr[0]], [x, yr[1]]));
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
let y = step[1] * Math.ceil(yr[0] / step[1]);
|
|
790
|
-
for (; y <= yr[1]; y += step[1]) {
|
|
791
|
-
set.push(this.line([xr[0], y], [xr[1], y]));
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
return set;
|
|
795
|
-
},
|
|
796
|
-
|
|
797
|
-
label: function (point, text, direction, latex) {
|
|
798
|
-
latex = typeof latex === "undefined" || latex;
|
|
799
|
-
|
|
800
|
-
const $span = $("<span>").addClass("graphie-label");
|
|
801
|
-
|
|
802
|
-
const pad = currentStyle["label-distance"];
|
|
803
|
-
|
|
804
|
-
$span
|
|
805
|
-
.css(
|
|
806
|
-
$.extend(
|
|
807
|
-
{},
|
|
808
|
-
{
|
|
809
|
-
position: "absolute",
|
|
810
|
-
padding: (pad != null ? pad : 7) + "px",
|
|
811
|
-
color: "black",
|
|
812
|
-
},
|
|
813
|
-
),
|
|
814
|
-
)
|
|
815
|
-
.data("labelDirection", direction)
|
|
816
|
-
.appendTo(el);
|
|
817
|
-
|
|
818
|
-
// @ts-expect-error - TS2339 - Property 'setPosition' does not exist on type 'JQuery<HTMLElement>'.
|
|
819
|
-
$span.setPosition = function (point) {
|
|
820
|
-
const scaledPoint = scalePoint(point);
|
|
821
|
-
$span.css({
|
|
822
|
-
left: scaledPoint[0],
|
|
823
|
-
top: scaledPoint[1],
|
|
824
|
-
});
|
|
825
|
-
};
|
|
826
|
-
|
|
827
|
-
// @ts-expect-error - TS2339 - Property 'setPosition' does not exist on type 'JQuery<HTMLElement>'.
|
|
828
|
-
$span.setPosition(point);
|
|
829
|
-
|
|
830
|
-
const span = $span[0];
|
|
831
|
-
|
|
832
|
-
// @ts-expect-error - TS2339 - Property 'processMath' does not exist on type 'JQuery<HTMLElement>'.
|
|
833
|
-
$span.processMath = function (math, force) {
|
|
834
|
-
processMath(span, math, force, function () {
|
|
835
|
-
const width = span.scrollWidth;
|
|
836
|
-
const height = span.scrollHeight;
|
|
837
|
-
setLabelMargins(span, [width, height]);
|
|
838
|
-
});
|
|
839
|
-
};
|
|
840
|
-
|
|
841
|
-
// @ts-expect-error - TS2339 - Property 'processText' does not exist on type 'JQuery<HTMLElement>'.
|
|
842
|
-
$span.processText = function (text: any) {
|
|
843
|
-
$span.html(text);
|
|
844
|
-
const width = span.scrollWidth;
|
|
845
|
-
const height = span.scrollHeight;
|
|
846
|
-
setLabelMargins(span, [width, height]);
|
|
847
|
-
};
|
|
848
|
-
|
|
849
|
-
if (latex) {
|
|
850
|
-
// @ts-expect-error - TS2339 - Property 'processMath' does not exist on type 'JQuery<HTMLElement>'.
|
|
851
|
-
$span.processMath(text, /* force */ false);
|
|
852
|
-
} else {
|
|
853
|
-
// @ts-expect-error - TS2339 - Property 'processText' does not exist on type 'JQuery<HTMLElement>'.
|
|
854
|
-
$span.processText(text);
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
return $span;
|
|
858
|
-
},
|
|
859
|
-
|
|
860
|
-
plotParametric: function (fn, range, shade, fn2) {
|
|
861
|
-
// Note: fn2 should only be set if 'shade' is true, as it denotes
|
|
862
|
-
// the function between which fn should have its area shaded.
|
|
863
|
-
// In general, plotParametric shouldn't be used to shade the area
|
|
864
|
-
// between two arbitrary parametrics functions over an interval,
|
|
865
|
-
// as the method assumes that fn and fn2 are both of the form
|
|
866
|
-
// fn(t) = (t, fn'(t)) for some initial fn'.
|
|
867
|
-
fn2 =
|
|
868
|
-
fn2 ||
|
|
869
|
-
function (t) {
|
|
870
|
-
return [t, 0];
|
|
871
|
-
};
|
|
872
|
-
|
|
873
|
-
// We truncate to 500,000, since anything bigger causes
|
|
874
|
-
// overflow in the firefox svg renderer. This is safe
|
|
875
|
-
// since 500,000 is outside the viewport anyway. We
|
|
876
|
-
// write these functions the way we do to handle undefined.
|
|
877
|
-
const clipper = (xy) => {
|
|
878
|
-
if (Math.abs(xy[1]) > 500000) {
|
|
879
|
-
return [xy[0], Math.min(Math.max(xy[1], -500000), 500000)];
|
|
880
|
-
}
|
|
881
|
-
return xy;
|
|
882
|
-
};
|
|
883
|
-
const clippedFn = (x) => clipper(fn(x));
|
|
884
|
-
const clippedFn2 = (x: number) => clipper(fn2(x));
|
|
885
|
-
|
|
886
|
-
if (!currentStyle.strokeLinejoin) {
|
|
887
|
-
currentStyle.strokeLinejoin = "round";
|
|
888
|
-
}
|
|
889
|
-
if (!currentStyle.strokeLinecap) {
|
|
890
|
-
currentStyle.strokeLinecap = "round";
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
const min = range[0];
|
|
894
|
-
const max = range[1];
|
|
895
|
-
let step = (max - min) / (currentStyle["plot-points"] || 800);
|
|
896
|
-
if (step === 0) {
|
|
897
|
-
step = 1;
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
const paths = raphael.set();
|
|
901
|
-
let points = [];
|
|
902
|
-
let lastDiff = GraphUtils.coordDiff(
|
|
903
|
-
clippedFn(min),
|
|
904
|
-
clippedFn2(min),
|
|
905
|
-
);
|
|
906
|
-
|
|
907
|
-
let lastFlip = min;
|
|
908
|
-
for (let t = min; t <= max; t += step) {
|
|
909
|
-
const top = clippedFn(t);
|
|
910
|
-
const bottom = clippedFn2(t);
|
|
911
|
-
const diff = GraphUtils.coordDiff(top, bottom);
|
|
912
|
-
|
|
913
|
-
// Find points where it flips
|
|
914
|
-
// Create path that sketches area between the two functions
|
|
915
|
-
if (
|
|
916
|
-
// if there is an asymptote here, meaning that the graph
|
|
917
|
-
// switches signs and has a large difference
|
|
918
|
-
(diff[1] < 0 !== lastDiff[1] < 0 &&
|
|
919
|
-
Math.abs(diff[1] - lastDiff[1]) > 2 * yScale) ||
|
|
920
|
-
// or the function is undefined
|
|
921
|
-
isNaN(diff[1])
|
|
922
|
-
) {
|
|
923
|
-
// split the path at this point, and draw it
|
|
924
|
-
if (shade) {
|
|
925
|
-
// @ts-expect-error - TS2345 - Argument of type 'any' is not assignable to parameter of type 'never'.
|
|
926
|
-
points.push(top);
|
|
927
|
-
|
|
928
|
-
// backtrack to draw paired function
|
|
929
|
-
for (let u = t - step; u >= lastFlip; u -= step) {
|
|
930
|
-
// @ts-expect-error - TS2345 - Argument of type 'any' is not assignable to parameter of type 'never'.
|
|
931
|
-
points.push(clippedFn2(u));
|
|
932
|
-
}
|
|
933
|
-
lastFlip = t;
|
|
934
|
-
}
|
|
935
|
-
paths.push(this.path(points));
|
|
936
|
-
// restart the path, excluding this point
|
|
937
|
-
points = [];
|
|
938
|
-
if (shade) {
|
|
939
|
-
// @ts-expect-error - TS2345 - Argument of type 'any' is not assignable to parameter of type 'never'.
|
|
940
|
-
points.push(top);
|
|
941
|
-
}
|
|
942
|
-
} else {
|
|
943
|
-
// otherwise, just add the point to the path
|
|
944
|
-
// @ts-expect-error - TS2345 - Argument of type 'any' is not assignable to parameter of type 'never'.
|
|
945
|
-
points.push(top);
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
lastDiff = diff;
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
if (shade) {
|
|
952
|
-
// backtrack to draw paired function
|
|
953
|
-
for (let u = max - step; u >= lastFlip; u -= step) {
|
|
954
|
-
// @ts-expect-error - TS2345 - Argument of type 'any' is not assignable to parameter of type 'never'.
|
|
955
|
-
points.push(clippedFn2(u));
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
paths.push(this.path(points));
|
|
959
|
-
|
|
960
|
-
return paths;
|
|
961
|
-
},
|
|
962
|
-
|
|
963
|
-
plotPolar: function (fn, range) {
|
|
964
|
-
const min = range[0];
|
|
965
|
-
const max = range[1];
|
|
966
|
-
|
|
967
|
-
// There is probably a better heuristic for this
|
|
968
|
-
if (!currentStyle["plot-points"]) {
|
|
969
|
-
currentStyle["plot-points"] = 2 * (max - min) * xScale;
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
return this.plotParametric(function (th) {
|
|
973
|
-
return polar(fn(th), (th * 180) / Math.PI);
|
|
974
|
-
}, range);
|
|
975
|
-
},
|
|
976
|
-
|
|
977
|
-
plot: function (fn, range, swapAxes, shade, fn2) {
|
|
978
|
-
const min = range[0];
|
|
979
|
-
const max = range[1];
|
|
980
|
-
if (!currentStyle["plot-points"]) {
|
|
981
|
-
currentStyle["plot-points"] = 2 * (max - min) * xScale;
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
if (swapAxes) {
|
|
985
|
-
if (fn2) {
|
|
986
|
-
// TODO(charlie): support swapped axis area shading
|
|
987
|
-
throw new PerseusError(
|
|
988
|
-
"Can't shade area between functions with swapped axes.",
|
|
989
|
-
Errors.Internal,
|
|
990
|
-
);
|
|
991
|
-
}
|
|
992
|
-
return this.plotParametric(
|
|
993
|
-
function (y) {
|
|
994
|
-
return [fn(y), y];
|
|
995
|
-
},
|
|
996
|
-
range,
|
|
997
|
-
shade,
|
|
998
|
-
);
|
|
999
|
-
}
|
|
1000
|
-
if (fn2) {
|
|
1001
|
-
if (shade) {
|
|
1002
|
-
return this.plotParametric(
|
|
1003
|
-
function (x) {
|
|
1004
|
-
return [x, fn(x)];
|
|
1005
|
-
},
|
|
1006
|
-
range,
|
|
1007
|
-
shade,
|
|
1008
|
-
function (x) {
|
|
1009
|
-
return [x, fn2(x)];
|
|
1010
|
-
},
|
|
1011
|
-
);
|
|
1012
|
-
}
|
|
1013
|
-
throw new PerseusError(
|
|
1014
|
-
"fn2 should only be set when 'shade' is True.",
|
|
1015
|
-
Errors.Internal,
|
|
1016
|
-
);
|
|
1017
|
-
}
|
|
1018
|
-
return this.plotParametric(
|
|
1019
|
-
function (x) {
|
|
1020
|
-
return [x, fn(x)];
|
|
1021
|
-
},
|
|
1022
|
-
range,
|
|
1023
|
-
shade,
|
|
1024
|
-
);
|
|
1025
|
-
},
|
|
1026
|
-
|
|
1027
|
-
/**
|
|
1028
|
-
* Given a piecewise function, return a Raphael set of paths that
|
|
1029
|
-
* can be used to draw the function, e.g. using style().
|
|
1030
|
-
* Calls plotParametric.
|
|
1031
|
-
*
|
|
1032
|
-
* @param {[]} fnArray array of functions which when called
|
|
1033
|
-
* with a parameter i return the value of
|
|
1034
|
-
* the function at i
|
|
1035
|
-
* @param {[]} rangeArray array of ranges over which the
|
|
1036
|
-
* corresponding functions are defined
|
|
1037
|
-
* @return {Set<any>} set of paths
|
|
1038
|
-
*/
|
|
1039
|
-
plotPiecewise: function (fnArray, rangeArray) {
|
|
1040
|
-
const paths = raphael.set();
|
|
1041
|
-
const self = this;
|
|
1042
|
-
_.times(fnArray.length, function (i) {
|
|
1043
|
-
const fn = fnArray[i];
|
|
1044
|
-
const range = rangeArray[i];
|
|
1045
|
-
const fnPaths = self.plotParametric(function (x) {
|
|
1046
|
-
return [x, fn(x)];
|
|
1047
|
-
}, range);
|
|
1048
|
-
_.each(fnPaths, function (fnPath) {
|
|
1049
|
-
paths.push(fnPath);
|
|
1050
|
-
});
|
|
1051
|
-
});
|
|
944
|
+
}
|
|
945
|
+
} else if (type === Raphael.st) {
|
|
946
|
+
for (let i = 0, l = path.items.length; i < l; i++) {
|
|
947
|
+
arrows(path.items[i]);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
return path;
|
|
951
|
+
};
|
|
1052
952
|
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
*
|
|
1061
|
-
* @param {Array<[number, number]>} endpointArray
|
|
1062
|
-
* @return {Set<any>} set of circles
|
|
1063
|
-
*/
|
|
1064
|
-
plotEndpointCircles: function (endpointArray) {
|
|
1065
|
-
const circles = raphael.set();
|
|
1066
|
-
const self = this;
|
|
1067
|
-
|
|
1068
|
-
_.each(endpointArray, function (coord, i) {
|
|
1069
|
-
circles.push(self.circle(coord, 0.15));
|
|
1070
|
-
});
|
|
953
|
+
function circle(center, radius) {
|
|
954
|
+
return thisGraphie.raphael.ellipse(
|
|
955
|
+
...thisGraphie
|
|
956
|
+
.scalePoint(center)
|
|
957
|
+
.concat(thisGraphie.scaleVector([radius, radius])),
|
|
958
|
+
);
|
|
959
|
+
}
|
|
1071
960
|
|
|
1072
|
-
|
|
1073
|
-
|
|
961
|
+
// (x, y) is coordinate of bottom left corner
|
|
962
|
+
function rect(x, y, width, height) {
|
|
963
|
+
// Raphael needs (x, y) to be coordinate of upper left corner
|
|
964
|
+
const corner = thisGraphie.scalePoint([x, y + height]);
|
|
965
|
+
const dims = thisGraphie.scaleVector([width, height]);
|
|
966
|
+
const elem = thisGraphie.raphael.rect(...corner.concat(dims));
|
|
1074
967
|
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
const step = (max - min) / (currentStyle["plot-points"] || 800);
|
|
968
|
+
if (thisGraphie.isMobile) {
|
|
969
|
+
elem.node.style.shapeRendering = "crispEdges";
|
|
970
|
+
}
|
|
1079
971
|
|
|
1080
|
-
|
|
1081
|
-
|
|
972
|
+
return elem;
|
|
973
|
+
}
|
|
1082
974
|
|
|
1083
|
-
|
|
1084
|
-
|
|
975
|
+
function ellipse(center, radii) {
|
|
976
|
+
return thisGraphie.raphael.ellipse(
|
|
977
|
+
...thisGraphie
|
|
978
|
+
.scalePoint(center)
|
|
979
|
+
.concat(thisGraphie.scaleVector(radii)),
|
|
980
|
+
);
|
|
981
|
+
}
|
|
1085
982
|
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
983
|
+
function fixedEllipse(
|
|
984
|
+
center: number | Coord,
|
|
985
|
+
// Different type than Coord, this is radiusX, radiusY
|
|
986
|
+
radii: number | [number, number],
|
|
987
|
+
maxScale: number,
|
|
988
|
+
padding: number,
|
|
989
|
+
) {
|
|
990
|
+
// Scale point and radius
|
|
991
|
+
const scaledPoint = thisGraphie.scalePoint(center);
|
|
992
|
+
const scaledRadii = thisGraphie.scaleVector(radii);
|
|
993
|
+
|
|
994
|
+
const width = 2 * scaledRadii[0] * maxScale + padding;
|
|
995
|
+
const height = 2 * scaledRadii[1] * maxScale + padding;
|
|
996
|
+
|
|
997
|
+
// Calculate absolute left, top
|
|
998
|
+
const left = scaledPoint[0] - width / 2;
|
|
999
|
+
const top = scaledPoint[1] - height / 2;
|
|
1000
|
+
|
|
1001
|
+
// Wrap in <div>
|
|
1002
|
+
const wrapper = document.createElement("div");
|
|
1003
|
+
$(wrapper).css({
|
|
1004
|
+
position: "absolute",
|
|
1005
|
+
width: width + "px",
|
|
1006
|
+
height: height + "px",
|
|
1007
|
+
left: left + "px",
|
|
1008
|
+
top: top + "px",
|
|
1009
|
+
});
|
|
1010
|
+
// wrapper.setAttribute("data-graphie-type", "ellipse");
|
|
1011
|
+
|
|
1012
|
+
// Create Raphael canvas
|
|
1013
|
+
const localRaphael = Raphael(wrapper, width, height);
|
|
1014
|
+
const visibleShape = localRaphael.ellipse(
|
|
1015
|
+
width / 2,
|
|
1016
|
+
height / 2,
|
|
1017
|
+
scaledRadii[0],
|
|
1018
|
+
scaledRadii[1],
|
|
1019
|
+
);
|
|
1092
1020
|
|
|
1093
|
-
|
|
1094
|
-
|
|
1021
|
+
return {
|
|
1022
|
+
wrapper: wrapper,
|
|
1023
|
+
visibleShape: visibleShape,
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1095
1026
|
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1027
|
+
function arc(center, radius, startAngle, endAngle, sector) {
|
|
1028
|
+
startAngle = ((startAngle % 360) + 360) % 360;
|
|
1029
|
+
endAngle = ((endAngle % 360) + 360) % 360;
|
|
1099
1030
|
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1031
|
+
const cent = thisGraphie.scalePoint(center);
|
|
1032
|
+
const radii = thisGraphie.scaleVector(radius);
|
|
1033
|
+
const startVector = polar(radius, startAngle);
|
|
1034
|
+
const endVector = polar(radius, endAngle);
|
|
1103
1035
|
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1036
|
+
const startPoint = thisGraphie.scalePoint([
|
|
1037
|
+
center[0] + startVector[0],
|
|
1038
|
+
center[1] + startVector[1],
|
|
1039
|
+
]);
|
|
1040
|
+
const endPoint = thisGraphie.scalePoint([
|
|
1041
|
+
center[0] + endVector[0],
|
|
1042
|
+
center[1] + endVector[1],
|
|
1043
|
+
]);
|
|
1107
1044
|
|
|
1108
|
-
|
|
1109
|
-
yScale = scale[1];
|
|
1045
|
+
const largeAngle = (((endAngle - startAngle) % 360) + 360) % 360 > 180;
|
|
1110
1046
|
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
)
|
|
1116
|
-
|
|
1047
|
+
return thisGraphie.raphael.path(
|
|
1048
|
+
"M" +
|
|
1049
|
+
startPoint.join(" ") +
|
|
1050
|
+
"A" +
|
|
1051
|
+
radii.join(" ") +
|
|
1052
|
+
" 0 " + // ellipse rotation
|
|
1053
|
+
(largeAngle ? 1 : 0) +
|
|
1054
|
+
" 0 " + // sweep flag
|
|
1055
|
+
endPoint.join(" ") +
|
|
1056
|
+
(sector ? "L" + cent.join(" ") + "z" : ""),
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1117
1059
|
|
|
1118
|
-
|
|
1119
|
-
|
|
1060
|
+
function path(points) {
|
|
1061
|
+
// @ts-expect-error - TS2554 - Expected 2 arguments, but got 1.
|
|
1062
|
+
const p = thisGraphie.raphael.path(thisGraphie.svgPath(points));
|
|
1063
|
+
p.graphiePath = points;
|
|
1120
1064
|
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
raphael.setSize(w, h);
|
|
1065
|
+
return p;
|
|
1066
|
+
}
|
|
1124
1067
|
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1068
|
+
function fixedPath(points, center, createPath) {
|
|
1069
|
+
points = _.map(points, thisGraphie.scalePoint);
|
|
1070
|
+
center = center ? thisGraphie.scalePoint(center) : null;
|
|
1071
|
+
createPath = createPath || thisGraphie.svgPath;
|
|
1129
1072
|
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
this.ypixels = h;
|
|
1073
|
+
const pathLeft = _.min(_.pluck(points, 0));
|
|
1074
|
+
const pathRight = _.max(_.pluck(points, 0));
|
|
1075
|
+
const pathTop = _.min(_.pluck(points, 1));
|
|
1076
|
+
const pathBottom = _.max(_.pluck(points, 1));
|
|
1135
1077
|
|
|
1136
|
-
|
|
1078
|
+
// Apply padding to line
|
|
1079
|
+
const padding = [4, 4];
|
|
1137
1080
|
|
|
1138
|
-
|
|
1139
|
-
|
|
1081
|
+
// Calculate and apply additional offset
|
|
1082
|
+
const extraOffset = [pathLeft, pathTop];
|
|
1140
1083
|
|
|
1141
|
-
|
|
1142
|
-
|
|
1084
|
+
// Apply padding and offset to points
|
|
1085
|
+
points = _.map(points, function (point) {
|
|
1086
|
+
return kvector.add(
|
|
1087
|
+
kvector.subtract(point, extraOffset),
|
|
1088
|
+
kvector.scale(padding, 0.5),
|
|
1089
|
+
);
|
|
1090
|
+
});
|
|
1143
1091
|
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1092
|
+
// Calculate <div> dimensions
|
|
1093
|
+
const width = pathRight - pathLeft + padding[0];
|
|
1094
|
+
const height = pathBottom - pathTop + padding[1];
|
|
1095
|
+
const left = extraOffset[0] - padding[0] / 2;
|
|
1096
|
+
const top = extraOffset[1] - padding[1] / 2;
|
|
1097
|
+
|
|
1098
|
+
// Create <div>
|
|
1099
|
+
const wrapper = document.createElement("div");
|
|
1100
|
+
$(wrapper).css({
|
|
1101
|
+
position: "absolute",
|
|
1102
|
+
width: width + "px",
|
|
1103
|
+
height: height + "px",
|
|
1104
|
+
left: left + "px",
|
|
1105
|
+
top: top + "px",
|
|
1106
|
+
// If user specified a center, set it
|
|
1107
|
+
// NOTE(kevinb): jQuery doesn't like that `transformOrigin `could be `null`
|
|
1108
|
+
// so we cast to `any` here.
|
|
1109
|
+
transformOrigin: center
|
|
1110
|
+
? width / 2 +
|
|
1111
|
+
center[0] +
|
|
1112
|
+
"px " +
|
|
1113
|
+
(height / 2 + center[1]) +
|
|
1114
|
+
"px"
|
|
1115
|
+
: null,
|
|
1116
|
+
} as any);
|
|
1117
|
+
|
|
1118
|
+
// Create Raphael canvas
|
|
1119
|
+
const localRaphael = Raphael(wrapper, width, height);
|
|
1120
|
+
|
|
1121
|
+
// Calculate path
|
|
1122
|
+
const visibleShape = localRaphael.path(createPath(points));
|
|
1123
|
+
|
|
1124
|
+
return {
|
|
1125
|
+
wrapper: wrapper,
|
|
1126
|
+
visibleShape: visibleShape,
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1153
1129
|
|
|
1154
|
-
|
|
1155
|
-
|
|
1130
|
+
function scaledPath(points) {
|
|
1131
|
+
const p = thisGraphie.raphael.path(
|
|
1132
|
+
thisGraphie.svgPath(points, /* alreadyScaled */ true),
|
|
1133
|
+
);
|
|
1134
|
+
p.graphiePath = points;
|
|
1135
|
+
return p;
|
|
1136
|
+
}
|
|
1156
1137
|
|
|
1157
|
-
|
|
1158
|
-
|
|
1138
|
+
function line(start, end) {
|
|
1139
|
+
const l = path([start, end]);
|
|
1159
1140
|
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
svgParabolaPath: svgParabolaPath,
|
|
1164
|
-
svgSinusoidPath: svgSinusoidPath,
|
|
1165
|
-
});
|
|
1141
|
+
if (thisGraphie.isMobile) {
|
|
1142
|
+
l.node.style.shapeRendering = "crispEdges";
|
|
1143
|
+
}
|
|
1166
1144
|
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
const last = args[args.length - 1];
|
|
1170
|
-
const oldStyle = currentStyle;
|
|
1171
|
-
let result;
|
|
1145
|
+
return l;
|
|
1146
|
+
}
|
|
1172
1147
|
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
...processAttributes(last),
|
|
1178
|
-
};
|
|
1148
|
+
function parabola(a, b, c) {
|
|
1149
|
+
// Plot a parabola of the form: f(x) = (a * x + b) * x + c
|
|
1150
|
+
return thisGraphie.raphael.path(thisGraphie.svgParabolaPath(a, b, c));
|
|
1151
|
+
}
|
|
1179
1152
|
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
currentStyle = $.extend({}, currentStyle);
|
|
1153
|
+
function fixedLine(start, end, thickness) {
|
|
1154
|
+
// Apply padding to line
|
|
1155
|
+
const padding = [thickness, thickness];
|
|
1184
1156
|
|
|
1185
|
-
|
|
1186
|
-
|
|
1157
|
+
// Scale points to get values in pixels
|
|
1158
|
+
start = thisGraphie.scalePoint(start);
|
|
1159
|
+
end = thisGraphie.scalePoint(end);
|
|
1187
1160
|
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1161
|
+
// Calculate and apply additional offset
|
|
1162
|
+
const extraOffset = [
|
|
1163
|
+
Math.min(start[0], end[0]),
|
|
1164
|
+
Math.min(start[1], end[1]),
|
|
1165
|
+
];
|
|
1192
1166
|
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1167
|
+
// Apply padding and offset to start, end points
|
|
1168
|
+
start = kvector.add(
|
|
1169
|
+
kvector.subtract(start, extraOffset),
|
|
1170
|
+
kvector.scale(padding, 0.5),
|
|
1171
|
+
);
|
|
1172
|
+
end = kvector.add(
|
|
1173
|
+
kvector.subtract(end, extraOffset),
|
|
1174
|
+
kvector.scale(padding, 0.5),
|
|
1175
|
+
);
|
|
1202
1176
|
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1177
|
+
// Calculate <div> dimensions
|
|
1178
|
+
const left = extraOffset[0] - padding[0] / 2;
|
|
1179
|
+
const top = extraOffset[1] - padding[1] / 2;
|
|
1180
|
+
const width = Math.abs(start[0] - end[0]) + padding[0];
|
|
1181
|
+
const height = Math.abs(start[1] - end[1]) + padding[1];
|
|
1182
|
+
|
|
1183
|
+
// Create <div>
|
|
1184
|
+
const wrapper = document.createElement("div");
|
|
1185
|
+
$(wrapper).css({
|
|
1186
|
+
position: "absolute",
|
|
1187
|
+
width: width + "px",
|
|
1188
|
+
height: height + "px",
|
|
1189
|
+
left: left + "px",
|
|
1190
|
+
top: top + "px",
|
|
1191
|
+
// Outsiders should feel like the line's 'origin' (i.e., for
|
|
1192
|
+
// rotation) is the starting point
|
|
1193
|
+
transformOrigin: start[0] + "px " + start[1] + "px",
|
|
1194
|
+
});
|
|
1207
1195
|
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
// Options expected are:
|
|
1211
|
-
// - range: [[a, b], [c, d]] or [a, b]
|
|
1212
|
-
// - scale: [a, b] or number
|
|
1213
|
-
// - gridOpacity: number (0 - 1)
|
|
1214
|
-
// - gridStep: [a, b] or number (relative to units)
|
|
1215
|
-
// - tickStep: [a, b] or number (relative to grid steps)
|
|
1216
|
-
// - tickLen: [a, b] or number (in pixels)
|
|
1217
|
-
// - labelStep: [a, b] or number (relative to tick steps)
|
|
1218
|
-
// - yLabelFormat: fn to format label string for y-axis
|
|
1219
|
-
// - xLabelFormat: fn to format label string for x-axis
|
|
1220
|
-
// - smartLabelPositioning: true or false to ignore minus sign
|
|
1221
|
-
graphie.graphInit = function (options: any) {
|
|
1222
|
-
options = options || {};
|
|
1196
|
+
// Create Raphael canvas
|
|
1197
|
+
const localRaphael = Raphael(wrapper, width, height);
|
|
1223
1198
|
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
!prop.match(/.*Opacity$/) &&
|
|
1230
|
-
prop !== "range" &&
|
|
1231
|
-
typeof val === "number"
|
|
1232
|
-
) {
|
|
1233
|
-
options[prop] = [val, val];
|
|
1234
|
-
}
|
|
1199
|
+
// Calculate path
|
|
1200
|
+
const path =
|
|
1201
|
+
"M" + start[0] + " " + start[1] + " " + "L" + end[0] + " " + end[1];
|
|
1202
|
+
const visibleShape = localRaphael.path(path);
|
|
1203
|
+
visibleShape.graphiePath = [start, end];
|
|
1235
1204
|
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
options[prop] = [
|
|
1242
|
-
[-val[0], val[0]],
|
|
1243
|
-
[-val[1], val[1]],
|
|
1244
|
-
];
|
|
1245
|
-
}
|
|
1246
|
-
} else if (typeof val === "number") {
|
|
1247
|
-
options[prop] = [
|
|
1248
|
-
[-val, val],
|
|
1249
|
-
[-val, val],
|
|
1250
|
-
];
|
|
1251
|
-
}
|
|
1252
|
-
}
|
|
1253
|
-
});
|
|
1205
|
+
return {
|
|
1206
|
+
wrapper: wrapper,
|
|
1207
|
+
visibleShape: visibleShape,
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1254
1210
|
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
const grid = options.grid != null ? options.grid : true;
|
|
1262
|
-
const gridOpacity = options.gridOpacity || 0.1;
|
|
1263
|
-
const gridStep = options.gridStep || [1, 1];
|
|
1264
|
-
const axes = options.axes != null ? options.axes : true;
|
|
1265
|
-
const axisArrows = options.axisArrows || "";
|
|
1266
|
-
const axisOpacity = options.axisOpacity || 1.0;
|
|
1267
|
-
const axisCenter = options.axisCenter || [
|
|
1268
|
-
Math.min(Math.max(range[0][0], 0), range[0][1]),
|
|
1269
|
-
Math.min(Math.max(range[1][0], 0), range[1][1]),
|
|
1270
|
-
];
|
|
1271
|
-
const axisLabels =
|
|
1272
|
-
options.axisLabels != null ? options.axisLabels : false;
|
|
1273
|
-
const ticks = options.ticks != null ? options.ticks : true;
|
|
1274
|
-
const tickStep = options.tickStep || [2, 2];
|
|
1275
|
-
const tickLen = options.tickLen || [5, 5];
|
|
1276
|
-
const tickOpacity = options.tickOpacity || 1.0;
|
|
1277
|
-
const labels = options.labels || options.labelStep || false;
|
|
1278
|
-
const labelStep = options.labelStep || [1, 1];
|
|
1279
|
-
const labelOpacity = options.labelOpacity || 1.0;
|
|
1280
|
-
let unityLabels = options.unityLabels || false;
|
|
1281
|
-
const labelFormat =
|
|
1282
|
-
options.labelFormat ||
|
|
1283
|
-
function (a: any) {
|
|
1284
|
-
return a;
|
|
1285
|
-
};
|
|
1286
|
-
let xLabelFormat = options.xLabelFormat || labelFormat;
|
|
1287
|
-
let yLabelFormat = options.yLabelFormat || labelFormat;
|
|
1288
|
-
const smartLabelPositioning =
|
|
1289
|
-
options.smartLabelPositioning != null
|
|
1290
|
-
? options.smartLabelPositioning
|
|
1291
|
-
: true;
|
|
1292
|
-
const realRange = [
|
|
1293
|
-
[
|
|
1294
|
-
range[0][0] - (range[0][0] > 0 ? 1 : 0),
|
|
1295
|
-
range[0][1] + (range[0][1] < 0 ? 1 : 0),
|
|
1296
|
-
],
|
|
1297
|
-
[
|
|
1298
|
-
range[1][0] - (range[1][0] > 0 ? 1 : 0),
|
|
1299
|
-
range[1][1] + (range[1][1] < 0 ? 1 : 0),
|
|
1300
|
-
],
|
|
1301
|
-
];
|
|
1211
|
+
function sinusoid(a, b, c, d) {
|
|
1212
|
+
// Plot a sinusoid of the form: f(x) = a * sin(b * x - c) + d
|
|
1213
|
+
return thisGraphie.raphael.path(
|
|
1214
|
+
thisGraphie.svgSinusoidPath(a, b, c, d),
|
|
1215
|
+
);
|
|
1216
|
+
}
|
|
1302
1217
|
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1218
|
+
function grid(xr, yr) {
|
|
1219
|
+
const step: any = thisGraphie.currentStyle.step || [1, 1];
|
|
1220
|
+
const set = thisGraphie.raphael.set();
|
|
1306
1221
|
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
};
|
|
1312
|
-
};
|
|
1222
|
+
let x = step[0] * Math.ceil(xr[0] / step[0]);
|
|
1223
|
+
for (; x <= xr[1]; x += step[0]) {
|
|
1224
|
+
set.push(line([x, yr[0]], [x, yr[1]]));
|
|
1225
|
+
}
|
|
1313
1226
|
|
|
1314
|
-
|
|
1315
|
-
|
|
1227
|
+
let y = step[1] * Math.ceil(yr[0] / step[1]);
|
|
1228
|
+
for (; y <= yr[1]; y += step[1]) {
|
|
1229
|
+
set.push(line([xr[0], y], [xr[1], y]));
|
|
1316
1230
|
}
|
|
1317
1231
|
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
scale: scale,
|
|
1321
|
-
isMobile: options.isMobile,
|
|
1322
|
-
});
|
|
1232
|
+
return set;
|
|
1233
|
+
}
|
|
1323
1234
|
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
this.grid(gridRange[0], gridRange[1], {
|
|
1327
|
-
stroke: options.isMobile ? KhanColors.GRAY_C : "#000000",
|
|
1328
|
-
opacity: options.isMobile ? 1 : gridOpacity,
|
|
1329
|
-
step: gridStep,
|
|
1330
|
-
strokeWidth: options.isMobile ? 1 : 2,
|
|
1331
|
-
});
|
|
1332
|
-
}
|
|
1235
|
+
function label(point, text, direction, latex) {
|
|
1236
|
+
latex = typeof latex === "undefined" || latex;
|
|
1333
1237
|
|
|
1334
|
-
|
|
1335
|
-
if (axes) {
|
|
1336
|
-
// this is a slight hack until <-> arrowheads work
|
|
1337
|
-
if (axisArrows === "<->" || axisArrows === true) {
|
|
1338
|
-
this.style(
|
|
1339
|
-
{
|
|
1340
|
-
stroke: options.isMobile
|
|
1341
|
-
? KhanColors.GRAY_G
|
|
1342
|
-
: "#000000",
|
|
1343
|
-
opacity: options.isMobile ? 1 : axisOpacity,
|
|
1344
|
-
strokeWidth: options.isMobile ? 1 : 2,
|
|
1345
|
-
arrows: "->",
|
|
1346
|
-
},
|
|
1347
|
-
function () {
|
|
1348
|
-
if (range[1][0] < 0 && range[1][1] > 0) {
|
|
1349
|
-
// @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
|
|
1350
|
-
this.path([
|
|
1351
|
-
axisCenter,
|
|
1352
|
-
[gridRange[0][0], axisCenter[1]],
|
|
1353
|
-
]);
|
|
1354
|
-
// @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
|
|
1355
|
-
this.path([
|
|
1356
|
-
axisCenter,
|
|
1357
|
-
[gridRange[0][1], axisCenter[1]],
|
|
1358
|
-
]);
|
|
1359
|
-
}
|
|
1360
|
-
if (range[0][0] < 0 && range[0][1] > 0) {
|
|
1361
|
-
// @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
|
|
1362
|
-
this.path([
|
|
1363
|
-
axisCenter,
|
|
1364
|
-
[axisCenter[0], gridRange[1][0]],
|
|
1365
|
-
]);
|
|
1366
|
-
// @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
|
|
1367
|
-
this.path([
|
|
1368
|
-
axisCenter,
|
|
1369
|
-
[axisCenter[0], gridRange[1][1]],
|
|
1370
|
-
]);
|
|
1371
|
-
}
|
|
1372
|
-
},
|
|
1373
|
-
);
|
|
1238
|
+
const $span = $("<span>").addClass("graphie-label");
|
|
1374
1239
|
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1240
|
+
const pad = thisGraphie.currentStyle["label-distance"];
|
|
1241
|
+
|
|
1242
|
+
$span
|
|
1243
|
+
.css(
|
|
1244
|
+
$.extend(
|
|
1245
|
+
{},
|
|
1379
1246
|
{
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
arrows: axisArrows,
|
|
1384
|
-
},
|
|
1385
|
-
function () {
|
|
1386
|
-
// @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
|
|
1387
|
-
this.path([
|
|
1388
|
-
[gridRange[0][0], axisCenter[1]],
|
|
1389
|
-
[gridRange[0][1], axisCenter[1]],
|
|
1390
|
-
]);
|
|
1391
|
-
// @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
|
|
1392
|
-
this.path([
|
|
1393
|
-
[axisCenter[0], gridRange[1][0]],
|
|
1394
|
-
[axisCenter[0], gridRange[1][1]],
|
|
1395
|
-
]);
|
|
1247
|
+
position: "absolute",
|
|
1248
|
+
padding: (pad != null ? pad : 7) + "px",
|
|
1249
|
+
color: "black",
|
|
1396
1250
|
},
|
|
1397
|
-
)
|
|
1398
|
-
|
|
1251
|
+
),
|
|
1252
|
+
)
|
|
1253
|
+
.data("labelDirection", direction)
|
|
1254
|
+
.appendTo(thisGraphie.el);
|
|
1255
|
+
|
|
1256
|
+
// @ts-expect-error - TS2339 - Property 'setPosition' does not exist on type 'JQuery<HTMLElement>'.
|
|
1257
|
+
$span.setPosition = function (point) {
|
|
1258
|
+
const scaledPoint = thisGraphie.scalePoint(point);
|
|
1259
|
+
$span.css({
|
|
1260
|
+
left: scaledPoint[0],
|
|
1261
|
+
top: scaledPoint[1],
|
|
1262
|
+
});
|
|
1263
|
+
};
|
|
1399
1264
|
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
);
|
|
1411
|
-
}
|
|
1265
|
+
// @ts-expect-error - TS2339 - Property 'setPosition' does not exist on type 'JQuery<HTMLElement>'.
|
|
1266
|
+
$span.setPosition(point);
|
|
1267
|
+
|
|
1268
|
+
const span = $span[0];
|
|
1269
|
+
|
|
1270
|
+
// @ts-expect-error - TS2339 - Property 'processMath' does not exist on type 'JQuery<HTMLElement>'.
|
|
1271
|
+
$span.processMath = function (math, force) {
|
|
1272
|
+
processMath(span, math, force, function () {
|
|
1273
|
+
const width = span.scrollWidth;
|
|
1274
|
+
const height = span.scrollHeight;
|
|
1275
|
+
setLabelMargins(span, [width, height]);
|
|
1276
|
+
});
|
|
1277
|
+
};
|
|
1278
|
+
|
|
1279
|
+
// @ts-expect-error - TS2339 - Property 'processText' does not exist on type 'JQuery<HTMLElement>'.
|
|
1280
|
+
$span.processText = function (text: any) {
|
|
1281
|
+
$span.html(text);
|
|
1282
|
+
const width = span.scrollWidth;
|
|
1283
|
+
const height = span.scrollHeight;
|
|
1284
|
+
setLabelMargins(span, [width, height]);
|
|
1285
|
+
};
|
|
1286
|
+
|
|
1287
|
+
if (latex) {
|
|
1288
|
+
// @ts-expect-error - TS2339 - Property 'processMath' does not exist on type 'JQuery<HTMLElement>'.
|
|
1289
|
+
$span.processMath(text, /* force */ false);
|
|
1290
|
+
} else {
|
|
1291
|
+
// @ts-expect-error - TS2339 - Property 'processText' does not exist on type 'JQuery<HTMLElement>'.
|
|
1292
|
+
$span.processText(text);
|
|
1412
1293
|
}
|
|
1413
1294
|
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
const halfWidthTicks = options.isMobile;
|
|
1417
|
-
this.style(
|
|
1418
|
-
{
|
|
1419
|
-
stroke: options.isMobile ? KhanColors.GRAY_G : "#000000",
|
|
1420
|
-
opacity: options.isMobile ? 1 : tickOpacity,
|
|
1421
|
-
strokeWidth: 1,
|
|
1422
|
-
},
|
|
1423
|
-
function () {
|
|
1424
|
-
// horizontal axis
|
|
1425
|
-
let step = gridStep[0] * tickStep[0];
|
|
1426
|
-
let len = tickLen[0] / scale[1];
|
|
1427
|
-
let start = gridRange[0][0];
|
|
1428
|
-
let stop = gridRange[0][1];
|
|
1295
|
+
return $span;
|
|
1296
|
+
}
|
|
1429
1297
|
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1298
|
+
function plotParametric(
|
|
1299
|
+
fn: (t: number) => Coord,
|
|
1300
|
+
range,
|
|
1301
|
+
shade,
|
|
1302
|
+
fn2: (t: number) => Coord = (t) => [t, 0],
|
|
1303
|
+
) {
|
|
1304
|
+
// Note: fn2 should only be set if 'shade' is true, as it denotes
|
|
1305
|
+
// the function between which fn should have its area shaded.
|
|
1306
|
+
// In general, plotParametric shouldn't be used to shade the area
|
|
1307
|
+
// between two arbitrary parametrics functions over an interval,
|
|
1308
|
+
// as the method assumes that fn and fn2 are both of the form
|
|
1309
|
+
// fn(t) = (t, fn'(t)) for some initial fn'.
|
|
1310
|
+
|
|
1311
|
+
// We truncate to 500,000, since anything bigger causes
|
|
1312
|
+
// overflow in the firefox svg renderer. This is safe
|
|
1313
|
+
// since 500,000 is outside the viewport anyway. We
|
|
1314
|
+
// write these functions the way we do to handle undefined.
|
|
1315
|
+
const clipper = (xy) => {
|
|
1316
|
+
if (Math.abs(xy[1]) > 500000) {
|
|
1317
|
+
return [xy[0], Math.min(Math.max(xy[1], -500000), 500000)];
|
|
1318
|
+
}
|
|
1319
|
+
return xy;
|
|
1320
|
+
};
|
|
1321
|
+
const clippedFn = (x) => clipper(fn(x));
|
|
1322
|
+
const clippedFn2 = (x: number) => clipper(fn2(x));
|
|
1449
1323
|
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
// @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
|
|
1457
|
-
this.line(
|
|
1458
|
-
[x, -len + axisCenter[1]],
|
|
1459
|
-
[
|
|
1460
|
-
x,
|
|
1461
|
-
halfWidthTicks
|
|
1462
|
-
? 0
|
|
1463
|
-
: len + axisCenter[1],
|
|
1464
|
-
],
|
|
1465
|
-
);
|
|
1466
|
-
}
|
|
1467
|
-
}
|
|
1468
|
-
}
|
|
1324
|
+
if (!thisGraphie.currentStyle.strokeLinejoin) {
|
|
1325
|
+
thisGraphie.currentStyle.strokeLinejoin = "round";
|
|
1326
|
+
}
|
|
1327
|
+
if (!thisGraphie.currentStyle.strokeLinecap) {
|
|
1328
|
+
thisGraphie.currentStyle.strokeLinecap = "round";
|
|
1329
|
+
}
|
|
1469
1330
|
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1331
|
+
const min = range[0];
|
|
1332
|
+
const max = range[1];
|
|
1333
|
+
let step =
|
|
1334
|
+
(max - min) / (thisGraphie.currentStyle["plot-points"] || 800);
|
|
1335
|
+
if (step === 0) {
|
|
1336
|
+
step = 1;
|
|
1337
|
+
}
|
|
1475
1338
|
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
y <= stop;
|
|
1480
|
-
y += step
|
|
1481
|
-
) {
|
|
1482
|
-
if (y < stop || !axisArrows) {
|
|
1483
|
-
// @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
|
|
1484
|
-
this.line(
|
|
1485
|
-
[-len + axisCenter[0], y],
|
|
1486
|
-
[
|
|
1487
|
-
halfWidthTicks
|
|
1488
|
-
? 0
|
|
1489
|
-
: len + axisCenter[0],
|
|
1490
|
-
y,
|
|
1491
|
-
],
|
|
1492
|
-
);
|
|
1493
|
-
}
|
|
1494
|
-
}
|
|
1339
|
+
const paths = thisGraphie.raphael.set();
|
|
1340
|
+
let points = [];
|
|
1341
|
+
let lastDiff = GraphUtils.coordDiff(clippedFn(min), clippedFn2(min));
|
|
1495
1342
|
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
if (y > start || !axisArrows) {
|
|
1502
|
-
// @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
|
|
1503
|
-
this.line(
|
|
1504
|
-
[-len + axisCenter[0], y],
|
|
1505
|
-
[
|
|
1506
|
-
halfWidthTicks
|
|
1507
|
-
? 0
|
|
1508
|
-
: len + axisCenter[0],
|
|
1509
|
-
y,
|
|
1510
|
-
],
|
|
1511
|
-
);
|
|
1512
|
-
}
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
},
|
|
1516
|
-
);
|
|
1517
|
-
}
|
|
1343
|
+
let lastFlip = min;
|
|
1344
|
+
for (let t = min; t <= max; t += step) {
|
|
1345
|
+
const top = clippedFn(t);
|
|
1346
|
+
const bottom = clippedFn2(t);
|
|
1347
|
+
const diff = GraphUtils.coordDiff(top, bottom);
|
|
1518
1348
|
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
axisCenter[0] === 0 && axisCenter[1] !== 0;
|
|
1535
|
-
const yShowZero =
|
|
1536
|
-
axisCenter[0] !== 0 && axisCenter[1] === 0;
|
|
1537
|
-
const axisOffCenter =
|
|
1538
|
-
axisCenter[0] !== 0 || axisCenter[1] !== 0;
|
|
1539
|
-
const showUnityX = unityLabels[0] || axisOffCenter;
|
|
1540
|
-
const showUnityY = unityLabels[1] || axisOffCenter;
|
|
1349
|
+
// Find points where it flips
|
|
1350
|
+
// Create path that sketches area between the two functions
|
|
1351
|
+
if (
|
|
1352
|
+
// if there is an asymptote here, meaning that the graph
|
|
1353
|
+
// switches signs and has a large difference
|
|
1354
|
+
(diff[1] < 0 !== lastDiff[1] < 0 &&
|
|
1355
|
+
Math.abs(diff[1] - lastDiff[1]) >
|
|
1356
|
+
2 * thisGraphie.drawingTransform().pixelsPerUnitY()) ||
|
|
1357
|
+
// or the function is undefined
|
|
1358
|
+
isNaN(diff[1])
|
|
1359
|
+
) {
|
|
1360
|
+
// split the path at this point, and draw it
|
|
1361
|
+
if (shade) {
|
|
1362
|
+
// @ts-expect-error - TS2345 - Argument of type 'any' is not assignable to parameter of type 'never'.
|
|
1363
|
+
points.push(top);
|
|
1541
1364
|
|
|
1542
|
-
//
|
|
1543
|
-
for (
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
x += step
|
|
1547
|
-
) {
|
|
1548
|
-
if (x < stop || !axisArrows) {
|
|
1549
|
-
// @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
|
|
1550
|
-
this.label(
|
|
1551
|
-
[x, axisCenter[1]],
|
|
1552
|
-
xLabelFormat(x),
|
|
1553
|
-
xAxisPosition,
|
|
1554
|
-
);
|
|
1555
|
-
}
|
|
1365
|
+
// backtrack to draw paired function
|
|
1366
|
+
for (let u = t - step; u >= lastFlip; u -= step) {
|
|
1367
|
+
// @ts-expect-error - TS2345 - Argument of type 'any' is not assignable to parameter of type 'never'.
|
|
1368
|
+
points.push(clippedFn2(u));
|
|
1556
1369
|
}
|
|
1370
|
+
lastFlip = t;
|
|
1371
|
+
}
|
|
1372
|
+
paths.push(path(points));
|
|
1373
|
+
// restart the path, excluding this point
|
|
1374
|
+
points = [];
|
|
1375
|
+
if (shade) {
|
|
1376
|
+
// @ts-expect-error - TS2345 - Argument of type 'any' is not assignable to parameter of type 'never'.
|
|
1377
|
+
points.push(top);
|
|
1378
|
+
}
|
|
1379
|
+
} else {
|
|
1380
|
+
// otherwise, just add the point to the path
|
|
1381
|
+
// @ts-expect-error - TS2345 - Argument of type 'any' is not assignable to parameter of type 'never'.
|
|
1382
|
+
points.push(top);
|
|
1383
|
+
}
|
|
1557
1384
|
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
let x = -step * (showUnityX ? 1 : 2) + axisCenter[0];
|
|
1561
|
-
x >= start;
|
|
1562
|
-
x -= step
|
|
1563
|
-
) {
|
|
1564
|
-
if (x > start || !axisArrows) {
|
|
1565
|
-
// @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
|
|
1566
|
-
this.label(
|
|
1567
|
-
[x, axisCenter[1]],
|
|
1568
|
-
xLabelFormat(x),
|
|
1569
|
-
xAxisPosition,
|
|
1570
|
-
);
|
|
1571
|
-
}
|
|
1572
|
-
}
|
|
1385
|
+
lastDiff = diff;
|
|
1386
|
+
}
|
|
1573
1387
|
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1388
|
+
if (shade) {
|
|
1389
|
+
// backtrack to draw paired function
|
|
1390
|
+
for (let u = max - step; u >= lastFlip; u -= step) {
|
|
1391
|
+
// @ts-expect-error - TS2345 - Argument of type 'any' is not assignable to parameter of type 'never'.
|
|
1392
|
+
points.push(clippedFn2(u));
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
paths.push(path(points));
|
|
1577
1396
|
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
let y = (yShowZero ? 0 : step) + axisCenter[1];
|
|
1581
|
-
y <= stop;
|
|
1582
|
-
y += step
|
|
1583
|
-
) {
|
|
1584
|
-
if (y < stop || !axisArrows) {
|
|
1585
|
-
// @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
|
|
1586
|
-
this.label(
|
|
1587
|
-
[axisCenter[0], y],
|
|
1588
|
-
yLabelFormat(y),
|
|
1589
|
-
yAxisPosition,
|
|
1590
|
-
);
|
|
1591
|
-
}
|
|
1592
|
-
}
|
|
1397
|
+
return paths;
|
|
1398
|
+
}
|
|
1593
1399
|
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1400
|
+
function plot(fn, range, swapAxes, shade, fn2) {
|
|
1401
|
+
const min = range[0];
|
|
1402
|
+
const max = range[1];
|
|
1403
|
+
if (!thisGraphie.currentStyle["plot-points"]) {
|
|
1404
|
+
thisGraphie.currentStyle["plot-points"] =
|
|
1405
|
+
2 *
|
|
1406
|
+
(max - min) *
|
|
1407
|
+
thisGraphie.drawingTransform().pixelsPerUnitX();
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
if (swapAxes) {
|
|
1411
|
+
if (fn2) {
|
|
1412
|
+
// TODO(charlie): support swapped axis area shading
|
|
1413
|
+
throw new PerseusError(
|
|
1414
|
+
"Can't shade area between functions with swapped axes.",
|
|
1415
|
+
Errors.Internal,
|
|
1416
|
+
);
|
|
1417
|
+
}
|
|
1418
|
+
return plotParametric(
|
|
1419
|
+
function (y) {
|
|
1420
|
+
return [fn(y), y];
|
|
1609
1421
|
},
|
|
1422
|
+
range,
|
|
1423
|
+
shade,
|
|
1424
|
+
);
|
|
1425
|
+
}
|
|
1426
|
+
if (fn2) {
|
|
1427
|
+
if (shade) {
|
|
1428
|
+
return plotParametric(
|
|
1429
|
+
function (x) {
|
|
1430
|
+
return [x, fn(x)];
|
|
1431
|
+
},
|
|
1432
|
+
range,
|
|
1433
|
+
shade,
|
|
1434
|
+
function (x) {
|
|
1435
|
+
return [x, fn2(x)];
|
|
1436
|
+
},
|
|
1437
|
+
);
|
|
1438
|
+
}
|
|
1439
|
+
throw new PerseusError(
|
|
1440
|
+
"fn2 should only be set when 'shade' is True.",
|
|
1441
|
+
Errors.Internal,
|
|
1610
1442
|
);
|
|
1611
1443
|
}
|
|
1444
|
+
return plotParametric(
|
|
1445
|
+
function (x) {
|
|
1446
|
+
return [x, fn(x)];
|
|
1447
|
+
},
|
|
1448
|
+
range,
|
|
1449
|
+
shade,
|
|
1450
|
+
);
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
const drawingTools = {
|
|
1454
|
+
circle,
|
|
1455
|
+
rect,
|
|
1456
|
+
ellipse,
|
|
1457
|
+
fixedEllipse,
|
|
1458
|
+
arc,
|
|
1459
|
+
path,
|
|
1460
|
+
fixedPath,
|
|
1461
|
+
scaledPath,
|
|
1462
|
+
line,
|
|
1463
|
+
parabola,
|
|
1464
|
+
fixedLine,
|
|
1465
|
+
sinusoid,
|
|
1466
|
+
grid,
|
|
1467
|
+
label,
|
|
1468
|
+
plotParametric,
|
|
1469
|
+
plot,
|
|
1612
1470
|
};
|
|
1613
1471
|
|
|
1614
|
-
|
|
1472
|
+
function graphify(drawingFn: any): any {
|
|
1473
|
+
return function (...args) {
|
|
1474
|
+
const last = args[args.length - 1];
|
|
1475
|
+
const oldStyle = thisGraphie.currentStyle;
|
|
1476
|
+
let result;
|
|
1477
|
+
|
|
1478
|
+
// The last argument is probably trying to change the style
|
|
1479
|
+
if (typeof last === "object" && !_.isArray(last)) {
|
|
1480
|
+
thisGraphie.currentStyle = {
|
|
1481
|
+
...thisGraphie.currentStyle,
|
|
1482
|
+
...thisGraphie.processAttributes(last),
|
|
1483
|
+
};
|
|
1484
|
+
|
|
1485
|
+
const rest = [].slice.call(args, 0, args.length - 1);
|
|
1486
|
+
result = drawingFn(...rest);
|
|
1487
|
+
} else {
|
|
1488
|
+
thisGraphie.currentStyle = $.extend(
|
|
1489
|
+
{},
|
|
1490
|
+
thisGraphie.currentStyle,
|
|
1491
|
+
);
|
|
1492
|
+
|
|
1493
|
+
result = drawingFn(...args);
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
// Bad heuristic for recognizing Raphael elements and sets
|
|
1497
|
+
const type = result.constructor.prototype;
|
|
1498
|
+
if (type === Raphael.el || type === Raphael.st) {
|
|
1499
|
+
result.attr(thisGraphie.currentStyle);
|
|
1500
|
+
|
|
1501
|
+
if (thisGraphie.currentStyle.arrows) {
|
|
1502
|
+
result = addArrowheads(result);
|
|
1503
|
+
}
|
|
1504
|
+
} else if (result instanceof $) {
|
|
1505
|
+
// We assume that if it's not a Raphael element/set, it
|
|
1506
|
+
// does not contain SVG.
|
|
1507
|
+
// @ts-expect-error - TS2339 - Property 'css' does not exist on type '{}'.
|
|
1508
|
+
result.css({
|
|
1509
|
+
...thisGraphie.currentStyle,
|
|
1510
|
+
...SVG_SPECIFIC_STYLE_MASK,
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
thisGraphie.currentStyle = oldStyle;
|
|
1515
|
+
return result;
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
$.each(drawingTools, function (name) {
|
|
1520
|
+
thisGraphie[name] = graphify(drawingTools[name]);
|
|
1521
|
+
});
|
|
1522
|
+
|
|
1523
|
+
return thisGraphie;
|
|
1615
1524
|
};
|
|
1616
1525
|
|
|
1617
1526
|
export default GraphUtils;
|