@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.
Files changed (56) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/es/index.css +0 -4
  3. package/dist/es/index.js +1422 -1021
  4. package/dist/es/index.js.map +1 -1
  5. package/dist/index.css +0 -4
  6. package/dist/index.d.ts +4 -2
  7. package/dist/index.js +1321 -954
  8. package/dist/index.js.map +1 -1
  9. package/dist/perseus-types.d.ts +4 -3
  10. package/dist/renderer.d.ts +6 -6
  11. package/dist/server-item-renderer.d.ts +2 -4
  12. package/dist/strings.js +1 -1
  13. package/dist/types.d.ts +19 -2
  14. package/dist/util/drawing-transform.d.ts +21 -0
  15. package/dist/util/extract-perseus-data.d.ts +26 -0
  16. package/dist/util/graph-bounds.d.ts +9 -0
  17. package/dist/util/graphie.d.ts +2 -0
  18. package/dist/util/interval.d.ts +1 -0
  19. package/dist/util/parse-perseus-json.d.ts +10 -0
  20. package/dist/util/tex.d.ts +1 -3
  21. package/dist/widgets/deprecated-standin/index.d.ts +4 -4
  22. package/dist/widgets/passage.d.ts +3 -3
  23. package/dist/widgets.d.ts +16 -6
  24. package/package.json +9 -9
  25. package/src/__tests__/extract-perseus-data.test.ts +1027 -0
  26. package/src/index.ts +6 -1
  27. package/src/init.ts +1 -1
  28. package/src/perseus-types.ts +4 -3
  29. package/src/renderer.tsx +12 -9
  30. package/src/server-item-renderer.tsx +2 -4
  31. package/src/styles/widgets/image.less +0 -3
  32. package/src/types.ts +19 -16
  33. package/src/util/drawing-transform.test.ts +67 -0
  34. package/src/util/drawing-transform.ts +102 -0
  35. package/src/util/extract-perseus-data.ts +539 -0
  36. package/src/util/graph-bounds.ts +23 -0
  37. package/src/util/graphie.ts +1167 -1258
  38. package/src/util/interactive.ts +2 -2
  39. package/src/util/interval.ts +4 -0
  40. package/src/util/parse-perseus-json.test.ts +10 -0
  41. package/src/util/parse-perseus-json.ts +13 -0
  42. package/src/util/tex.ts +11 -84
  43. package/src/widgets/__stories__/image.stories.tsx +12 -26
  44. package/src/widgets/__tests__/__snapshots__/grapher.test.ts.snap +72 -288
  45. package/src/widgets/__tests__/__snapshots__/interaction.test.ts.snap +5 -20
  46. package/src/widgets/__tests__/__snapshots__/interactive-graph.test.ts.snap +32 -128
  47. package/src/widgets/__tests__/__snapshots__/number-line.test.ts.snap +22 -88
  48. package/src/widgets/__tests__/__snapshots__/plotter.test.tsx.snap +12 -48
  49. package/src/widgets/deprecated-standin/__stories__/deprecated-standin.stories.tsx +8 -12
  50. package/src/widgets/deprecated-standin/__tests__/__snapshots__/deprecated-standin.test.ts.snap +6 -7
  51. package/src/widgets/deprecated-standin/index.tsx +8 -7
  52. package/src/widgets/interactive-graph.tsx +3 -2
  53. package/src/widgets/label-image/answer-pill.tsx +6 -1
  54. package/src/widgets/passage.tsx +5 -7
  55. package/src/widgets.ts +34 -10
  56. package/tsconfig-build.tsbuildinfo +1 -1
@@ -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
- /* Convert cartesian coordinates [x, y] to polar coordinates [r,
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
- const Graphie = (GraphUtils.Graphie = function () {});
106
-
107
- _.extend(Graphie.prototype, {
108
- cartToPolar: cartToPolar,
109
- polar: polar,
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
- let currentStyle: any = {
98
+ currentStyle: any = {
151
99
  "stroke-width": 2,
152
100
  fill: "none",
153
101
  };
154
102
 
155
- const scaleVector = function (point: number | Coord) {
156
- if (typeof point === "number") {
157
- return scaleVector([point, point]);
158
- }
159
-
160
- const x = point[0];
161
- const y = point[1];
162
- return [x * xScale, y * yScale];
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
- const scalePoint = function scalePoint(point: number | Coord): Coord {
166
- if (typeof point === "number") {
167
- return scalePoint([point, point]);
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
- const x = point[0];
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
- const unscalePoint = function (point: Array<never>) {
176
- if (typeof point === "number") {
177
- return unscalePoint([point, point]);
178
- }
138
+ this.#drawingTransform = new DrawingTransform(
139
+ this.raphael,
140
+ scale,
141
+ this.bounds(),
142
+ );
179
143
 
180
- const x = point[0];
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
- const unscaleVector = function (point: Array<never>) {
186
- if (typeof point === "number") {
187
- return unscaleVector([point, point]);
188
- }
146
+ $(this.el).css({
147
+ width: w,
148
+ height: h,
149
+ });
189
150
 
190
- return [point[0] / xScale, point[1] / yScale];
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
- const setLabelMargins = function (span: any, size: Array<any>) {
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
- if (typeof direction === "number") {
207
- const x = Math.cos(direction);
208
- const y = Math.sin(direction);
161
+ return this;
162
+ }
209
163
 
210
- const scale = Math.min(
211
- width / 2 / Math.abs(x),
212
- height / 2 / Math.abs(y),
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
- $span.css({
216
- marginLeft: -width / 2 + x * scale,
217
- marginTop: -height / 2 - y * scale,
218
- });
219
- } else {
220
- const multipliers = labelDirections[direction || "center"];
221
- $span.css({
222
- marginLeft: Math.round(width * multipliers[0]),
223
- marginTop: Math.round(height * multipliers[1]),
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
- const svgPath = function (points: any, alreadyScaled) {
229
- return $.map(points, function (point, i) {
230
- if (point === true) {
231
- return "z";
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
- const scaled = alreadyScaled ? point : scalePoint(point);
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
- const svgParabolaPath = function (a: any, b: any, c: any) {
244
- const computeParabola = function (x) {
245
- return (a * x + b) * x + c;
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
- // If points are collinear, plot a line instead
249
- if (a === 0) {
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
- // Calculate x coordinates of points on parabola
258
- const xVertex = -b / (2 * a);
259
- const distToEdge = Math.max(
260
- Math.abs(xVertex - xRange[0]),
261
- Math.abs(xVertex - xRange[1]),
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
- // To guarantee that drawn parabola to spans the viewport, use a point
265
- // on the edge of the graph furtherest from the vertex
266
- const xPoint = xVertex + distToEdge;
264
+ xLabelFormat = minusIgnorer(xLabelFormat);
265
+ yLabelFormat = minusIgnorer(yLabelFormat);
267
266
 
268
- // Compute parabola and other point on the curve
269
- const vertex = [xVertex, computeParabola(xVertex)];
270
- const point = [xPoint, computeParabola(xPoint)];
267
+ this.init({
268
+ range: realRange,
269
+ scale: scale,
270
+ isMobile: options.isMobile,
271
+ });
271
272
 
272
- // Calculate SVG 'control' point, defined by spec
273
- const control = [vertex[0], vertex[1] - (point[1] - vertex[1])];
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
- // Calculate mirror points across parabola's axis of symmetry
276
- const dx = Math.abs(vertex[0] - point[0]);
277
- const left = [vertex[0] - dx, point[1]];
278
- const right = [vertex[0] + dx, point[1]];
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
- // Scale and bound
281
- // @ts-expect-error - TS2345 - Argument of type '(point: number | Coord) => Coord' is not assignable to parameter of type 'Iteratee<any[][], any, any[]>'.
282
- const points = _.map([left, control, right], scalePoint);
283
- const values = _.map(_.flatten(points), KhanMath.bound);
284
- return (
285
- "M" +
286
- values[0] +
287
- "," +
288
- values[1] +
289
- " Q" +
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
- const svgSinusoidPath = function (a, b, c: any, d: any) {
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 = function (initial: number, i: number) {
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 = xRange[1] - xRange[0];
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 xRange[0]
711
+ // Find starting coordinate: first anchor point curve left of bounds.xMin
343
712
  let initial = c / b;
344
- const distToEdge = initial - xRange[0];
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
- // `svgPath` is independent of graphie range, so we export it independently
387
- GraphUtils.svgPath = svgPath;
755
+ scalePoint = (point: number | Coord): Coord => {
756
+ return this.drawingTransform().scalePoint(point);
757
+ };
388
758
 
389
- const processAttributes = function (attrs) {
390
- const transformers = {
391
- scale: function (scale) {
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
- xScale = scale[0];
397
- yScale = scale[1];
763
+ unscalePoint = (point: Coord) => {
764
+ return this.drawingTransform().unscalePoint(point);
765
+ };
398
766
 
399
- // Update the canvas size
400
- raphael.setSize(
401
- (xRange[1] - xRange[0]) * xScale,
402
- (yRange[1] - yRange[0]) * yScale,
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: function (pair) {
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: function (val) {
423
- return {rx: scaleVector([val, 0])[0]};
793
+ rx: (val) => {
794
+ return {rx: this.scaleVector([val, 0])[0]};
424
795
  },
425
796
 
426
- ry: function (val) {
427
- return {ry: scaleVector([0, val])[1]};
797
+ ry: (val) => {
798
+ return {ry: this.scaleVector([0, val])[1]};
428
799
  },
429
800
 
430
- r: function (val) {
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
- graphie.isMobile
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, graphie.isMobile ? 1e-5 : 0.75, 0)
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
- return paths;
1054
- },
1055
-
1056
- /**
1057
- * Given an array of coordinates of the form [x, y], create and
1058
- * return a Raphael set of Raphael circle objects at those
1059
- * coordinates
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
- return circles;
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
- plotAsymptotes: function (fn, range) {
1076
- const min = range[0];
1077
- const max = range[1];
1078
- const step = (max - min) / (currentStyle["plot-points"] || 800);
968
+ if (thisGraphie.isMobile) {
969
+ elem.node.style.shapeRendering = "crispEdges";
970
+ }
1079
971
 
1080
- const asymptotes = raphael.set();
1081
- let lastVal = fn(min);
972
+ return elem;
973
+ }
1082
974
 
1083
- for (let t = min; t <= max; t += step) {
1084
- const funcVal = fn(t);
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
- if (
1087
- funcVal < 0 !== lastVal < 0 &&
1088
- Math.abs(funcVal - lastVal) > 2 * yScale
1089
- ) {
1090
- asymptotes.push(this.line([t, yScale], [t, -yScale]));
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
- lastVal = funcVal;
1094
- }
1021
+ return {
1022
+ wrapper: wrapper,
1023
+ visibleShape: visibleShape,
1024
+ };
1025
+ }
1095
1026
 
1096
- return asymptotes;
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
- const graphie = new Graphie();
1101
- _.extend(graphie, {
1102
- raphael: raphael,
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
- init: function (options) {
1105
- let scale = options.scale || [40, 40];
1106
- scale = typeof scale === "number" ? [scale, scale] : scale;
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
- xScale = scale[0];
1109
- yScale = scale[1];
1045
+ const largeAngle = (((endAngle - startAngle) % 360) + 360) % 360 > 180;
1110
1046
 
1111
- if (options.range == null) {
1112
- throw new PerseusError(
1113
- "range should be specified in graph init",
1114
- Errors.Internal,
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
- xRange = options.range[0];
1119
- yRange = options.range[1];
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
- const w = (xRange[1] - xRange[0]) * xScale;
1122
- const h = (yRange[1] - yRange[0]) * yScale;
1123
- raphael.setSize(w, h);
1065
+ return p;
1066
+ }
1124
1067
 
1125
- $(el).css({
1126
- width: w,
1127
- height: h,
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
- this.range = options.range;
1131
- this.scale = scale;
1132
- this.dimensions = [w, h];
1133
- this.xpixels = w;
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
- this.isMobile = options.isMobile;
1078
+ // Apply padding to line
1079
+ const padding = [4, 4];
1137
1080
 
1138
- return this;
1139
- },
1081
+ // Calculate and apply additional offset
1082
+ const extraOffset = [pathLeft, pathTop];
1140
1083
 
1141
- style: function (attrs, fn) {
1142
- const processed = processAttributes(attrs);
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
- if (typeof fn === "function") {
1145
- const oldStyle = currentStyle;
1146
- currentStyle = $.extend({}, currentStyle, processed);
1147
- const result = fn.call(graphie);
1148
- currentStyle = oldStyle;
1149
- return result;
1150
- }
1151
- $.extend(currentStyle, processed);
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
- scalePoint: scalePoint,
1155
- scaleVector: scaleVector,
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
- unscalePoint: unscalePoint,
1158
- unscaleVector: unscaleVector,
1138
+ function line(start, end) {
1139
+ const l = path([start, end]);
1159
1140
 
1160
- // Custom SVG path functions that are dependent on graphie range
1161
- // `svgPath`, while independent of range, is exported for consistency
1162
- svgPath: svgPath,
1163
- svgParabolaPath: svgParabolaPath,
1164
- svgSinusoidPath: svgSinusoidPath,
1165
- });
1141
+ if (thisGraphie.isMobile) {
1142
+ l.node.style.shapeRendering = "crispEdges";
1143
+ }
1166
1144
 
1167
- $.each(drawingTools, function (name) {
1168
- graphie[name] = function (...args) {
1169
- const last = args[args.length - 1];
1170
- const oldStyle = currentStyle;
1171
- let result;
1145
+ return l;
1146
+ }
1172
1147
 
1173
- // The last argument is probably trying to change the style
1174
- if (typeof last === "object" && !_.isArray(last)) {
1175
- currentStyle = {
1176
- ...currentStyle,
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
- const rest = [].slice.call(args, 0, args.length - 1);
1181
- result = drawingTools[name](...rest);
1182
- } else {
1183
- currentStyle = $.extend({}, currentStyle);
1153
+ function fixedLine(start, end, thickness) {
1154
+ // Apply padding to line
1155
+ const padding = [thickness, thickness];
1184
1156
 
1185
- result = drawingTools[name](...args);
1186
- }
1157
+ // Scale points to get values in pixels
1158
+ start = thisGraphie.scalePoint(start);
1159
+ end = thisGraphie.scalePoint(end);
1187
1160
 
1188
- // Bad heuristic for recognizing Raphael elements and sets
1189
- const type = result.constructor.prototype;
1190
- if (type === Raphael.el || type === Raphael.st) {
1191
- result.attr(currentStyle);
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
- if (currentStyle.arrows) {
1194
- result = addArrowheads(result);
1195
- }
1196
- } else if (result instanceof $) {
1197
- // We assume that if it's not a Raphael element/set, it
1198
- // does not contain SVG.
1199
- // @ts-expect-error - TS2339 - Property 'css' does not exist on type '{}'.
1200
- result.css({...currentStyle, ...SVG_SPECIFIC_STYLE_MASK});
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
- currentStyle = oldStyle;
1204
- return result;
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
- // Initializes graphie settings for a graph and draws the basic graph
1209
- // features (axes, grid, tick marks, and axis labels)
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
- $.each(options, function (prop, val: any) {
1225
- // allow options to be specified by a single number for shorthand if
1226
- // the horizontal and vertical components are the same
1227
- if (
1228
- // @ts-expect-error - TS2339 - Property 'match' does not exist on type 'string | number | symbol'.
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
- // allow symmetric ranges to be specified by the absolute values
1237
- if (prop === "range" || prop === "gridRange") {
1238
- if (val.constructor === Array) {
1239
- // but don't mandate symmetric ranges
1240
- if (val[0].constructor !== Array) {
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
- const range = options.range || [
1256
- [-10, 10],
1257
- [-10, 10],
1258
- ];
1259
- const gridRange = options.gridRange || options.range;
1260
- const scale = options.scale || [20, 20];
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
- if (!_.isArray(unityLabels)) {
1304
- unityLabels = [unityLabels, unityLabels];
1305
- }
1218
+ function grid(xr, yr) {
1219
+ const step: any = thisGraphie.currentStyle.step || [1, 1];
1220
+ const set = thisGraphie.raphael.set();
1306
1221
 
1307
- if (smartLabelPositioning) {
1308
- const minusIgnorer = function (lf: any) {
1309
- return function (a) {
1310
- return (lf(a) + "").replace(/-(\d)/g, "\\llap{-}$1");
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
- xLabelFormat = minusIgnorer(xLabelFormat);
1315
- yLabelFormat = minusIgnorer(yLabelFormat);
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
- this.init({
1319
- range: realRange,
1320
- scale: scale,
1321
- isMobile: options.isMobile,
1322
- });
1232
+ return set;
1233
+ }
1323
1234
 
1324
- // draw grid
1325
- if (grid) {
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
- // draw axes
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
- // also, we don't support "<-" arrows yet, but why you
1376
- // would want that on your graph is beyond me.
1377
- } else if (axisArrows === "->" || axisArrows === "") {
1378
- this.style(
1240
+ const pad = thisGraphie.currentStyle["label-distance"];
1241
+
1242
+ $span
1243
+ .css(
1244
+ $.extend(
1245
+ {},
1379
1246
  {
1380
- stroke: "#000000",
1381
- opacity: axisOpacity,
1382
- strokeWidth: 2,
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
- if (axisLabels && axisLabels.length === 2) {
1401
- this.label(
1402
- [gridRange[0][1], axisCenter[1]],
1403
- axisLabels[0],
1404
- "right",
1405
- );
1406
- this.label(
1407
- [axisCenter[0], gridRange[1][1]],
1408
- axisLabels[1],
1409
- "above",
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
- // draw tick marks
1415
- if (ticks) {
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
- if (range[1][0] < 0 && range[1][1] > 0) {
1431
- for (
1432
- let x = step + axisCenter[0];
1433
- x <= stop;
1434
- x += step
1435
- ) {
1436
- if (x < stop || !axisArrows) {
1437
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
1438
- this.line(
1439
- [x, -len + axisCenter[1]],
1440
- [
1441
- x,
1442
- halfWidthTicks
1443
- ? 0
1444
- : len + axisCenter[1],
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
- for (
1451
- let x = -step + axisCenter[0];
1452
- x >= start;
1453
- x -= step
1454
- ) {
1455
- if (x > start || !axisArrows) {
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
- // vertical axis
1471
- step = gridStep[1] * tickStep[1];
1472
- len = tickLen[1] / scale[0];
1473
- start = gridRange[1][0];
1474
- stop = gridRange[1][1];
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
- if (range[0][0] < 0 && range[0][1] > 0) {
1477
- for (
1478
- let y = step + axisCenter[1];
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
- for (
1497
- let y = -step + axisCenter[1];
1498
- y >= start;
1499
- y -= step
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
- // draw axis labels
1520
- if (labels) {
1521
- this.style(
1522
- {
1523
- stroke: options.isMobile ? KhanColors.GRAY_G : "#000000",
1524
- opacity: options.isMobile ? 1 : labelOpacity,
1525
- },
1526
- function () {
1527
- // horizontal axis
1528
- let step = gridStep[0] * tickStep[0] * labelStep[0];
1529
- let start = gridRange[0][0];
1530
- let stop = gridRange[0][1];
1531
- const xAxisPosition = axisCenter[0] < 0 ? "above" : "below";
1532
- const yAxisPosition = axisCenter[0] < 0 ? "right" : "left";
1533
- const xShowZero =
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
- // positive x-axis
1543
- for (
1544
- let x = (xShowZero ? 0 : step) + axisCenter[0];
1545
- x <= stop;
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
- // negative x-axis
1559
- for (
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
- step = gridStep[1] * tickStep[1] * labelStep[1];
1575
- start = gridRange[1][0];
1576
- stop = gridRange[1][1];
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
- // positive y-axis
1579
- for (
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
- // negative y-axis
1595
- for (
1596
- let y = -step * (showUnityY ? 1 : 2) + axisCenter[1];
1597
- y >= start;
1598
- y -= step
1599
- ) {
1600
- if (y > start || !axisArrows) {
1601
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
1602
- this.label(
1603
- [axisCenter[0], y],
1604
- yLabelFormat(y),
1605
- yAxisPosition,
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
- return graphie;
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;