@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
package/dist/es/index.js CHANGED
@@ -35,6 +35,7 @@ import { Popper } from 'react-popper';
35
35
  import createReactClass from 'create-react-class';
36
36
  import Link from '@khanacademy/wonder-blocks-link';
37
37
  import { addLibraryVersionToPerseusDebug } from '@khanacademy/perseus-core';
38
+ import { keys } from '@khanacademy/wonder-stuff-core';
38
39
 
39
40
  function _extends() {
40
41
  _extends = Object.assign ? Object.assign.bind() : function (target) {
@@ -4221,6 +4222,18 @@ const InteractiveUtil = {
4221
4222
  }
4222
4223
  };
4223
4224
 
4225
+ function _classPrivateFieldBase(receiver, privateKey) {
4226
+ if (!Object.prototype.hasOwnProperty.call(receiver, privateKey)) {
4227
+ throw new TypeError("attempted to use private field on non-instance");
4228
+ }
4229
+ return receiver;
4230
+ }
4231
+
4232
+ var id$1 = 0;
4233
+ function _classPrivateFieldKey(name) {
4234
+ return "__private_" + id$1++ + "_" + name;
4235
+ }
4236
+
4224
4237
  var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
4225
4238
 
4226
4239
  function getDefaultExportFromCjs (x) {
@@ -8511,6 +8524,95 @@ const KhanColors = {
8511
8524
  DYNAMIC: "#6495ED" // BLUE
8512
8525
  };
8513
8526
 
8527
+ // DrawingTransform is responsible for transforming from "math coordinates"
8528
+ // (on the Cartesian plane) to "canvas coordinates" in pixels.
8529
+ class DrawingTransform {
8530
+ constructor(raphael, initialScale, bounds) {
8531
+ this.raphael = void 0;
8532
+ // Scale is measured in pixels per unit on the Cartesian plane.
8533
+ this.xScale = void 0;
8534
+ this.yScale = void 0;
8535
+ this.bounds = void 0;
8536
+ // TODO(benchristel): rename these methods to transform* instead of scale*.
8537
+ // They're doing more than scaling! Some of them also translate.
8538
+ this.scaleVector = point => {
8539
+ if (typeof point === "number") {
8540
+ return this.scaleVector([point, point]);
8541
+ }
8542
+ const x = point[0];
8543
+ const y = point[1];
8544
+ return [x * this.xScale, y * this.yScale];
8545
+ };
8546
+ this.scalePoint = point => {
8547
+ if (typeof point === "number") {
8548
+ return this.scalePoint([point, point]);
8549
+ }
8550
+ const x = point[0];
8551
+ const y = point[1];
8552
+ return [(x - this.bounds.xMin) * this.xScale, (this.bounds.yMax - y) * this.yScale];
8553
+ };
8554
+ this.unscalePoint = point => {
8555
+ if (typeof point === "number") {
8556
+ return this.unscalePoint([point, point]);
8557
+ }
8558
+ const x = point[0];
8559
+ const y = point[1];
8560
+ return [x / this.xScale + this.bounds.xMin, this.bounds.yMax - y / this.yScale];
8561
+ };
8562
+ this.unscaleVector = point => {
8563
+ if (typeof point === "number") {
8564
+ return this.unscaleVector([point, point]);
8565
+ }
8566
+ return [point[0] / this.xScale, point[1] / this.yScale];
8567
+ };
8568
+ this.setScale = scale => {
8569
+ if (typeof scale === "number") {
8570
+ scale = [scale, scale];
8571
+ }
8572
+ this.xScale = scale[0];
8573
+ this.yScale = scale[1];
8574
+
8575
+ // Update the canvas size
8576
+ this.raphael.setSize(...this.canvasDimensions());
8577
+ };
8578
+ this.canvasDimensions = () => {
8579
+ return [this.bounds.width() * this.xScale, this.bounds.height() * this.yScale];
8580
+ };
8581
+ this.pixelsPerUnitX = () => {
8582
+ return this.xScale;
8583
+ };
8584
+ this.pixelsPerUnitY = () => {
8585
+ return this.yScale;
8586
+ };
8587
+ this.raphael = raphael;
8588
+ this.bounds = bounds;
8589
+ this.xScale = initialScale[0];
8590
+ this.yScale = initialScale[1];
8591
+ this.setScale(initialScale);
8592
+ }
8593
+ }
8594
+
8595
+ // Defines the bounds of the visible area of a graph, in Cartesian coordinates.
8596
+ // GraphBounds is immutable.
8597
+ class GraphBounds {
8598
+ constructor(xRange, yRange) {
8599
+ this.xMin = void 0;
8600
+ this.xMax = void 0;
8601
+ this.yMin = void 0;
8602
+ this.yMax = void 0;
8603
+ this.xMin = xRange[0];
8604
+ this.xMax = xRange[1];
8605
+ this.yMin = yRange[0];
8606
+ this.yMax = yRange[1];
8607
+ }
8608
+ width() {
8609
+ return this.xMax - this.xMin;
8610
+ }
8611
+ height() {
8612
+ return this.yMax - this.yMin;
8613
+ }
8614
+ }
8615
+
8514
8616
  function render(element, container, callback) {
8515
8617
  // TODO(LP-11406): Replace this, or callers, with React Portal
8516
8618
  // @ts-expect-error - TS2322 - Type 'void' is not assignable to type 'ElementRef<ElementType>'.
@@ -8538,23 +8640,6 @@ function findChildOrAdd(elem, className) {
8538
8640
  }
8539
8641
  return $child;
8540
8642
  }
8541
- function doCallback(elem, callback) {
8542
- let tries = 0;
8543
- (function check() {
8544
- const height = elem.scrollHeight;
8545
- // Heuristic to guess if the font has kicked in
8546
- // so we have box metrics (magic number ick,
8547
- // but this seems to work mostly-consistently)
8548
- if (height > 18 || tries >= 10) {
8549
- callback();
8550
- } else {
8551
- tries++;
8552
- // TODO(jeff, CP-3128): Use Wonder Blocks Timing API
8553
- // eslint-disable-next-line no-restricted-syntax
8554
- setTimeout(check, 100);
8555
- }
8556
- })();
8557
- }
8558
8643
  var Tex$1 = {
8559
8644
  // Process a node and add math inside of it. This attempts to use KaTeX to
8560
8645
  // format the math, and if that fails it falls back to MathJax.
@@ -8576,27 +8661,14 @@ var Tex$1 = {
8576
8661
 
8577
8662
  // Only process if it hasn't been done before, or it is forced
8578
8663
  if ($elem.attr("data-math-formula") == null || force) {
8579
- const $katexHolder = findChildOrAdd($elem, "katex-holder");
8580
- const $mathjaxHolder = findChildOrAdd($elem, "mathjax-holder");
8581
-
8582
- // Search for MathJax-y script tags inside of the node. These are
8583
- // used by MathJax to denote the formula to be typeset. Before, we
8584
- // would update the formula by updating the contents of the script
8585
- // tag, which shouldn't happen any more, but we manage them just in
8586
- // case.
8587
- const script = $mathjaxHolder.find("script[type='math/tex']")[0];
8588
-
8589
- // If text wasn't provided, we look in two places
8590
- if (text == null) {
8591
- if ($elem.attr("data-math-formula")) {
8592
- // The old typeset formula
8593
- // @ts-expect-error - TS2322 - Type 'string | undefined' is not assignable to type 'string'.
8594
- text = $elem.attr("data-math-formula");
8595
- } else if (script) {
8596
- // The contents of the <script> tag
8597
- // @ts-expect-error - TS2339 - Property 'text' does not exist on type 'HTMLElement'.
8598
- text = script.text || script.textContent;
8599
- }
8664
+ const $texHolder = findChildOrAdd($elem, "tex-holder");
8665
+
8666
+ // If text wasn't provided, we use the cached text.
8667
+ // TODO(benchristel): I'm not sure if text can ever be null. It's
8668
+ // possible we don't need this check.
8669
+ if (text == null && $elem.attr("data-math-formula")) {
8670
+ // @ts-expect-error - TS2322 - Type 'string | undefined' is not assignable to type 'string'.
8671
+ text = $elem.attr("data-math-formula");
8600
8672
  }
8601
8673
  text = text != null ? text + "" : "";
8602
8674
 
@@ -8612,64 +8684,15 @@ var Tex$1 = {
8612
8684
  // that name is already taken.
8613
8685
  render( /*#__PURE__*/React.createElement(TeX, {
8614
8686
  children: text,
8615
- onRender: () => {
8616
- if (callback) {
8617
- doCallback(elem, callback);
8618
- }
8619
- }
8620
- }), $katexHolder[0]);
8621
- }
8622
- },
8623
- // Function to restore a node to a non-math-processed state
8624
- cleanupMath: function (elem) {
8625
- const $elem = $(elem);
8626
-
8627
- // Only mess with it if it's been processed before
8628
- if ($elem.attr("data-math-formula")) {
8629
- // Remove MathJax remnants
8630
- if (typeof MathJax !== "undefined") {
8631
- const jax = MathJax.Hub.getJaxFor($elem.find("script")[0]);
8632
- if (jax) {
8633
- const e = jax.SourceElement();
8634
- if (e.previousSibling && e.previousSibling.className) {
8635
- jax.Remove();
8636
- }
8637
- }
8638
- }
8639
-
8640
- // @ts-expect-error - TS2769 - No overload matches this call.
8641
- $elem.text($elem.attr("data-math-formula"));
8642
- $elem.attr("data-math-formula", null);
8643
- $elem.attr("data-math-type", null);
8687
+ onRender: callback
8688
+ }), $texHolder[0]);
8644
8689
  }
8645
- return elem;
8646
- },
8647
- // Function to retrieve the formula of a typeset math node
8648
- retrieveMathFormula: function (elem) {
8649
- return $(elem).attr("data-math-formula");
8650
8690
  }
8651
8691
  };
8652
8692
 
8653
8693
  const {
8654
8694
  processMath: processMath$1
8655
8695
  } = Tex$1;
8656
-
8657
- /* Convert cartesian coordinates [x, y] to polar coordinates [r,
8658
- * theta], with theta in degrees, or in radians if angleInRadians is
8659
- * specified.
8660
- */
8661
- function cartToPolar(coord, angleInRadians) {
8662
- const r = Math.sqrt(Math.pow(coord[0], 2) + Math.pow(coord[1], 2));
8663
- let theta = Math.atan2(coord[1], coord[0]);
8664
- // convert angle range from [-pi, pi] to [0, 2pi]
8665
- if (theta < 0) {
8666
- theta += 2 * Math.PI;
8667
- }
8668
- if (!angleInRadians) {
8669
- theta = theta * 180 / Math.PI;
8670
- }
8671
- return [r, theta];
8672
- }
8673
8696
  function polar(r, th) {
8674
8697
  if (typeof r === "number") {
8675
8698
  r = [r, r];
@@ -8725,223 +8748,440 @@ const GraphUtils = {
8725
8748
  },
8726
8749
  graphs: {}
8727
8750
  };
8728
- const Graphie$1 = GraphUtils.Graphie = function () {};
8729
- _.extend(Graphie$1.prototype, {
8730
- cartToPolar: cartToPolar,
8731
- polar: polar
8732
- });
8733
- const labelDirections = {
8734
- center: [-0.5, -0.5],
8735
- above: [-0.5, -1.0],
8736
- "above right": [0.0, -1.0],
8737
- right: [0.0, -0.5],
8738
- "below right": [0.0, 0.0],
8739
- below: [-0.5, 0.0],
8740
- "below left": [-1.0, 0.0],
8741
- left: [-1.0, -0.5],
8742
- "above left": [-1.0, -1.0]
8743
- };
8751
+ var _bounds = /*#__PURE__*/_classPrivateFieldKey("bounds");
8752
+ var _drawingTransform = /*#__PURE__*/_classPrivateFieldKey("drawingTransform");
8753
+ class Graphie$1 {
8754
+ constructor(el) {
8755
+ this.el = void 0;
8756
+ Object.defineProperty(this, _bounds, {
8757
+ writable: true,
8758
+ value: void 0
8759
+ });
8760
+ Object.defineProperty(this, _drawingTransform, {
8761
+ writable: true,
8762
+ value: void 0
8763
+ });
8764
+ this.raphael = void 0;
8765
+ this.isMobile = false;
8766
+ // Set up some reasonable defaults
8767
+ this.currentStyle = {
8768
+ "stroke-width": 2,
8769
+ fill: "none"
8770
+ };
8771
+ this.range = void 0;
8772
+ this.scale = void 0;
8773
+ this.dimensions = void 0;
8774
+ // TODO(benchristel): xpixels and ypixels are never used by this class, but
8775
+ // other code reaches in to access them :(
8776
+ // Refactor to accessor methods that look at this.dimensions.
8777
+ this.xpixels = void 0;
8778
+ this.ypixels = void 0;
8779
+ this.svgPath = (points, alreadyScaled) => {
8780
+ return $.map(points, (point, i) => {
8781
+ if (point === true) {
8782
+ return "z";
8783
+ }
8784
+ const scaled = alreadyScaled ? point : this.scalePoint(point);
8785
+ return (i === 0 ? "M" : "L") + KhanMath.bound(scaled[0]) + " " + KhanMath.bound(scaled[1]);
8786
+ }).join("");
8787
+ };
8788
+ this.svgParabolaPath = (a, b, c) => {
8789
+ const computeParabola = function computeParabola(x) {
8790
+ return (a * x + b) * x + c;
8791
+ };
8744
8792
 
8745
- /**
8746
- * Safari applies some SVG-specific styles to things that are not SVGs, so we
8747
- * need to exclude those styles from things that are not SVGs.
8748
- *
8749
- * To see this behavior in action, open https://codepen.io/anon/pen/zENEoa in
8750
- * Safari.
8751
- *
8752
- * Usage `$.extend({}, someStyles, SVG_SPECIFIC_STYLE_MASK)`
8753
- */
8754
- const SVG_SPECIFIC_STYLE_MASK = {
8755
- "stroke-width": null
8756
- };
8757
- GraphUtils.createGraphie = function (el) {
8758
- let xScale = 40;
8759
- let yScale = 40;
8760
- let xRange;
8761
- let yRange;
8762
- $(el).css("position", "relative");
8763
- const raphael$1 = raphael.exports(el);
8764
-
8765
- // For a sometimes-reproducible IE8 bug; doesn't affect SVG browsers at all
8766
- $(el).children("div").css("position", "absolute");
8767
-
8768
- // Set up some reasonable defaults
8769
- let currentStyle = {
8770
- "stroke-width": 2,
8771
- fill: "none"
8772
- };
8773
- const scaleVector = function scaleVector(point) {
8774
- if (typeof point === "number") {
8775
- return scaleVector([point, point]);
8776
- }
8777
- const x = point[0];
8778
- const y = point[1];
8779
- return [x * xScale, y * yScale];
8780
- };
8781
- const scalePoint = function scalePoint(point) {
8782
- if (typeof point === "number") {
8783
- return scalePoint([point, point]);
8784
- }
8785
- const x = point[0];
8786
- const y = point[1];
8787
- return [(x - xRange[0]) * xScale, (yRange[1] - y) * yScale];
8788
- };
8789
- const unscalePoint = function unscalePoint(point) {
8790
- if (typeof point === "number") {
8791
- return unscalePoint([point, point]);
8792
- }
8793
- const x = point[0];
8794
- const y = point[1];
8795
- return [x / xScale + xRange[0], yRange[1] - y / yScale];
8796
- };
8797
- const unscaleVector = function unscaleVector(point) {
8798
- if (typeof point === "number") {
8799
- return unscaleVector([point, point]);
8800
- }
8801
- return [point[0] / xScale, point[1] / yScale];
8802
- };
8803
- const setLabelMargins = function setLabelMargins(span, size) {
8804
- const $span = $(span);
8805
- const direction = $span.data("labelDirection");
8806
- let [width, height] = size;
8807
- // This can happen when a span
8808
- // is invisible but we still want to update the CSS. At worst, we will
8809
- // be off by a few pixels instead of in a different position entirely.
8810
- if (width === 0 && height === 0) {
8811
- [width, height] = [1, 1];
8812
- Log.log("Label size was 0x0 in graphie.js; using 1x1 instead");
8813
- }
8814
- $span.css("visibility", "");
8815
- if (typeof direction === "number") {
8816
- const x = Math.cos(direction);
8817
- const y = Math.sin(direction);
8818
- const scale = Math.min(width / 2 / Math.abs(x), height / 2 / Math.abs(y));
8819
- $span.css({
8820
- marginLeft: -width / 2 + x * scale,
8821
- marginTop: -height / 2 - y * scale
8822
- });
8823
- } else {
8824
- const multipliers = labelDirections[direction || "center"];
8825
- $span.css({
8826
- marginLeft: Math.round(width * multipliers[0]),
8827
- marginTop: Math.round(height * multipliers[1])
8828
- });
8829
- }
8830
- };
8831
- const svgPath = function svgPath(points, alreadyScaled) {
8832
- return $.map(points, function (point, i) {
8833
- if (point === true) {
8834
- return "z";
8793
+ // If points are collinear, plot a line instead
8794
+ if (a === 0) {
8795
+ const _points = [[this.bounds().xMin, computeParabola(this.bounds().xMin)], [this.bounds().xMax, computeParabola(this.bounds().xMax)]];
8796
+ // @ts-expect-error - TS2554 - Expected 2 arguments, but got 1.
8797
+ return this.svgPath(_points);
8835
8798
  }
8836
- const scaled = alreadyScaled ? point : scalePoint(point);
8837
- return (i === 0 ? "M" : "L") + KhanMath.bound(scaled[0]) + " " + KhanMath.bound(scaled[1]);
8838
- }).join("");
8839
- };
8840
- const svgParabolaPath = function svgParabolaPath(a, b, c) {
8841
- const computeParabola = function computeParabola(x) {
8842
- return (a * x + b) * x + c;
8843
- };
8844
8799
 
8845
- // If points are collinear, plot a line instead
8846
- if (a === 0) {
8847
- const _points = _.map(xRange, function (x) {
8848
- return [x, computeParabola(x)];
8849
- });
8850
- // @ts-expect-error - TS2554 - Expected 2 arguments, but got 1.
8851
- return svgPath(_points);
8852
- }
8800
+ // Calculate x coordinates of points on parabola
8801
+ const xVertex = -b / (2 * a);
8802
+ const distToEdge = Math.max(Math.abs(xVertex - this.bounds().xMin), Math.abs(xVertex - this.bounds().xMax));
8853
8803
 
8854
- // Calculate x coordinates of points on parabola
8855
- const xVertex = -b / (2 * a);
8856
- const distToEdge = Math.max(Math.abs(xVertex - xRange[0]), Math.abs(xVertex - xRange[1]));
8804
+ // To guarantee that drawn parabola to spans the viewport, use a point
8805
+ // on the edge of the graph furtherest from the vertex
8806
+ const xPoint = xVertex + distToEdge;
8857
8807
 
8858
- // To guarantee that drawn parabola to spans the viewport, use a point
8859
- // on the edge of the graph furtherest from the vertex
8860
- const xPoint = xVertex + distToEdge;
8808
+ // Compute parabola and other point on the curve
8809
+ const vertex = [xVertex, computeParabola(xVertex)];
8810
+ const point = [xPoint, computeParabola(xPoint)];
8861
8811
 
8862
- // Compute parabola and other point on the curve
8863
- const vertex = [xVertex, computeParabola(xVertex)];
8864
- const point = [xPoint, computeParabola(xPoint)];
8812
+ // Calculate SVG 'control' point, defined by spec
8813
+ const control = [vertex[0], vertex[1] - (point[1] - vertex[1])];
8865
8814
 
8866
- // Calculate SVG 'control' point, defined by spec
8867
- const control = [vertex[0], vertex[1] - (point[1] - vertex[1])];
8815
+ // Calculate mirror points across parabola's axis of symmetry
8816
+ const dx = Math.abs(vertex[0] - point[0]);
8817
+ const left = [vertex[0] - dx, point[1]];
8818
+ const right = [vertex[0] + dx, point[1]];
8868
8819
 
8869
- // Calculate mirror points across parabola's axis of symmetry
8870
- const dx = Math.abs(vertex[0] - point[0]);
8871
- const left = [vertex[0] - dx, point[1]];
8872
- const right = [vertex[0] + dx, point[1]];
8820
+ // Scale and bound
8821
+ // @ts-expect-error - TS2345 - Argument of type '(point: number | Coord) => Coord' is not assignable to parameter of type 'Iteratee<any[][], any, any[]>'.
8822
+ const points = _.map([left, control, right], this.scalePoint);
8823
+ const values = _.map(_.flatten(points), KhanMath.bound);
8824
+ return "M" + values[0] + "," + values[1] + " Q" + values[2] + "," + values[3] + " " + values[4] + "," + values[5];
8825
+ };
8826
+ this.svgSinusoidPath = (a, b, c, d) => {
8827
+ // Plot a sinusoid of the form: f(x) = a * sin(b * x - c) + d
8828
+ const quarterPeriod = Math.abs(Math.PI / (2 * b));
8829
+ const computeSine = function computeSine(x) {
8830
+ return a * Math.sin(b * x - c) + d;
8831
+ };
8832
+ const computeDerivative = function computeDerivative(x) {
8833
+ return a * b * Math.cos(c - b * x);
8834
+ };
8835
+ const coordsForOffset = (initial, i) => {
8836
+ // Return the cubic coordinates (including the two anchor and two
8837
+ // control points) for the ith portion of the sinusoid.
8838
+ const x0 = initial + quarterPeriod * i;
8839
+ const x1 = x0 + quarterPeriod;
8840
+
8841
+ // Interpolate using derivative technique
8842
+ // See: http://stackoverflow.com/questions/13932704/how-to-draw-sine-waves-with-svg-js
8843
+ const xCoords = [x0, x0 * 2 / 3 + x1 * 1 / 3, x0 * 1 / 3 + x1 * 2 / 3, x1];
8844
+ const yCoords = [computeSine(x0), computeSine(x0) + computeDerivative(x0) * (x1 - x0) / 3, computeSine(x1) - computeDerivative(x1) * (x1 - x0) / 3, computeSine(x1)];
8845
+
8846
+ // Zip and scale
8847
+ // @ts-expect-error - TS2345 - Argument of type '(point: number | Coord) => Coord' is not assignable to parameter of type 'Iteratee<any[][], any, any[]>'.
8848
+ return _.map(_.zip(xCoords, yCoords), this.scalePoint);
8849
+ };
8873
8850
 
8874
- // Scale and bound
8875
- // @ts-expect-error - TS2345 - Argument of type '(point: number | Coord) => Coord' is not assignable to parameter of type 'Iteratee<any[][], any, any[]>'.
8876
- const points = _.map([left, control, right], scalePoint);
8877
- const values = _.map(_.flatten(points), KhanMath.bound);
8878
- return "M" + values[0] + "," + values[1] + " Q" + values[2] + "," + values[3] + " " + values[4] + "," + values[5];
8879
- };
8880
- const svgSinusoidPath = function svgSinusoidPath(a, b, c, d) {
8881
- // Plot a sinusoid of the form: f(x) = a * sin(b * x - c) + d
8882
- const quarterPeriod = Math.abs(Math.PI / (2 * b));
8883
- const computeSine = function computeSine(x) {
8884
- return a * Math.sin(b * x - c) + d;
8851
+ // How many quarter-periods do we need to span the graph?
8852
+ const extent = this.bounds().width();
8853
+ const numQuarterPeriods = Math.ceil(extent / quarterPeriod) + 1;
8854
+
8855
+ // Find starting coordinate: first anchor point curve left of bounds.xMin
8856
+ let initial = c / b;
8857
+ const distToEdge = initial - this.bounds().xMin;
8858
+ initial -= quarterPeriod * Math.ceil(distToEdge / quarterPeriod);
8859
+
8860
+ // First portion of path is special-case, requiring move-to ('M')
8861
+ let coords = coordsForOffset(initial, 0);
8862
+ let path = "M" + coords[0][0] + "," + coords[0][1] + " C" + coords[1][0] + "," + coords[1][1] + " " + coords[2][0] + "," + coords[2][1] + " " + coords[3][0] + "," + coords[3][1];
8863
+ for (let i = 1; i < numQuarterPeriods; i++) {
8864
+ coords = coordsForOffset(initial, i);
8865
+ path += " C" + coords[1][0] + "," + coords[1][1] + " " + coords[2][0] + "," + coords[2][1] + " " + coords[3][0] + "," + coords[3][1];
8866
+ }
8867
+ return path;
8868
+ };
8869
+ this.scalePoint = point => {
8870
+ return this.drawingTransform().scalePoint(point);
8885
8871
  };
8886
- const computeDerivative = function computeDerivative(x) {
8887
- return a * b * Math.cos(c - b * x);
8872
+ this.scaleVector = point => {
8873
+ return this.drawingTransform().scaleVector(point);
8888
8874
  };
8889
- const coordsForOffset = function coordsForOffset(initial, i) {
8890
- // Return the cubic coordinates (including the two anchor and two
8891
- // control points) for the ith portion of the sinusoid.
8892
- const x0 = initial + quarterPeriod * i;
8893
- const x1 = x0 + quarterPeriod;
8875
+ this.unscalePoint = point => {
8876
+ return this.drawingTransform().unscalePoint(point);
8877
+ };
8878
+ this.unscaleVector = point => {
8879
+ return this.drawingTransform().unscaleVector(point);
8880
+ };
8881
+ this.el = el;
8882
+ $(el).css("position", "relative");
8883
+ this.raphael = raphael.exports(el);
8894
8884
 
8895
- // Interpolate using derivative technique
8896
- // See: http://stackoverflow.com/questions/13932704/how-to-draw-sine-waves-with-svg-js
8897
- const xCoords = [x0, x0 * 2 / 3 + x1 * 1 / 3, x0 * 1 / 3 + x1 * 2 / 3, x1];
8898
- const yCoords = [computeSine(x0), computeSine(x0) + computeDerivative(x0) * (x1 - x0) / 3, computeSine(x1) - computeDerivative(x1) * (x1 - x0) / 3, computeSine(x1)];
8885
+ // For a sometimes-reproducible IE8 bug; doesn't affect SVG browsers at all
8886
+ $(el).children("div").css("position", "absolute");
8887
+ }
8888
+ init(options) {
8889
+ let scale = options.scale || [40, 40];
8890
+ scale = typeof scale === "number" ? [scale, scale] : scale;
8891
+ if (options.range == null) {
8892
+ throw new PerseusError("range should be specified in graph init", Errors.Internal);
8893
+ }
8894
+ _classPrivateFieldBase(this, _bounds)[_bounds] = new GraphBounds(...options.range);
8895
+ _classPrivateFieldBase(this, _drawingTransform)[_drawingTransform] = new DrawingTransform(this.raphael, scale, this.bounds());
8896
+ const [w, h] = this.drawingTransform().canvasDimensions();
8897
+ $(this.el).css({
8898
+ width: w,
8899
+ height: h
8900
+ });
8901
+ this.range = options.range;
8902
+ this.scale = scale;
8903
+ // TODO(benchristel): I don't think dimensions is used. Can we
8904
+ // remove it?
8905
+ this.dimensions = [w, h];
8906
+ this.xpixels = w;
8907
+ this.ypixels = h;
8908
+ this.isMobile = options.isMobile;
8909
+ return this;
8910
+ }
8899
8911
 
8900
- // Zip and scale
8901
- // @ts-expect-error - TS2345 - Argument of type '(point: number | Coord) => Coord' is not assignable to parameter of type 'Iteratee<any[][], any, any[]>'.
8902
- return _.map(_.zip(xCoords, yCoords), scalePoint);
8912
+ // Initializes graphie settings for a graph and draws the basic graph
8913
+ // features (axes, grid, tick marks, and axis labels)
8914
+ // Options expected are:
8915
+ // - range: [[a, b], [c, d]] or [a, b]
8916
+ // - scale: [a, b] or number
8917
+ // - gridOpacity: number (0 - 1)
8918
+ // - gridStep: [a, b] or number (relative to units)
8919
+ // - tickStep: [a, b] or number (relative to grid steps)
8920
+ // - tickLen: [a, b] or number (in pixels)
8921
+ // - labelStep: [a, b] or number (relative to tick steps)
8922
+ // - yLabelFormat: fn to format label string for y-axis
8923
+ // - xLabelFormat: fn to format label string for x-axis
8924
+ graphInit(options) {
8925
+ options = options || {};
8926
+ $.each(options, function (prop, val) {
8927
+ // allow options to be specified by a single number for shorthand if
8928
+ // the horizontal and vertical components are the same
8929
+ if (
8930
+ // @ts-expect-error - TS2339 - Property 'match' does not exist on type 'string | number | symbol'.
8931
+ !prop.match(/.*Opacity$/) && prop !== "range" && typeof val === "number") {
8932
+ options[prop] = [val, val];
8933
+ }
8934
+
8935
+ // allow symmetric ranges to be specified by the absolute values
8936
+ if (prop === "range" || prop === "gridRange") {
8937
+ if (val.constructor === Array) {
8938
+ // but don't mandate symmetric ranges
8939
+ if (val[0].constructor !== Array) {
8940
+ options[prop] = [[-val[0], val[0]], [-val[1], val[1]]];
8941
+ }
8942
+ } else if (typeof val === "number") {
8943
+ options[prop] = [[-val, val], [-val, val]];
8944
+ }
8945
+ }
8946
+ });
8947
+ const range = options.range || [[-10, 10], [-10, 10]];
8948
+ const gridRange = options.gridRange || options.range;
8949
+ const scale = options.scale || [20, 20];
8950
+ const grid = options.grid != null ? options.grid : true;
8951
+ const gridOpacity = options.gridOpacity || 0.1;
8952
+ const gridStep = options.gridStep || [1, 1];
8953
+ const axes = options.axes != null ? options.axes : true;
8954
+ const axisArrows = options.axisArrows || "";
8955
+ const axisOpacity = options.axisOpacity || 1.0;
8956
+ const axisCenter = options.axisCenter || [Math.min(Math.max(range[0][0], 0), range[0][1]), Math.min(Math.max(range[1][0], 0), range[1][1])];
8957
+ const axisLabels = options.axisLabels != null ? options.axisLabels : false;
8958
+ const ticks = options.ticks != null ? options.ticks : true;
8959
+ const tickStep = options.tickStep || [2, 2];
8960
+ const tickLen = options.tickLen || [5, 5];
8961
+ const tickOpacity = options.tickOpacity || 1.0;
8962
+ const labels = options.labels || options.labelStep || false;
8963
+ const labelStep = options.labelStep || [1, 1];
8964
+ const labelOpacity = options.labelOpacity || 1.0;
8965
+ let unityLabels = options.unityLabels || false;
8966
+ const labelFormat = options.labelFormat || function (a) {
8967
+ return a;
8968
+ };
8969
+ let xLabelFormat = options.xLabelFormat || labelFormat;
8970
+ let yLabelFormat = options.yLabelFormat || labelFormat;
8971
+ const realRange = [[range[0][0] - (range[0][0] > 0 ? 1 : 0), range[0][1] + (range[0][1] < 0 ? 1 : 0)], [range[1][0] - (range[1][0] > 0 ? 1 : 0), range[1][1] + (range[1][1] < 0 ? 1 : 0)]];
8972
+ if (!_.isArray(unityLabels)) {
8973
+ unityLabels = [unityLabels, unityLabels];
8974
+ }
8975
+ const minusIgnorer = function minusIgnorer(lf) {
8976
+ return function (a) {
8977
+ return (lf(a) + "").replace(/-(\d)/g, "\\llap{-}$1");
8978
+ };
8903
8979
  };
8980
+ xLabelFormat = minusIgnorer(xLabelFormat);
8981
+ yLabelFormat = minusIgnorer(yLabelFormat);
8982
+ this.init({
8983
+ range: realRange,
8984
+ scale: scale,
8985
+ isMobile: options.isMobile
8986
+ });
8904
8987
 
8905
- // How many quarter-periods do we need to span the graph?
8906
- const extent = xRange[1] - xRange[0];
8907
- const numQuarterPeriods = Math.ceil(extent / quarterPeriod) + 1;
8988
+ // draw grid
8989
+ if (grid) {
8990
+ this.grid(gridRange[0], gridRange[1], {
8991
+ stroke: options.isMobile ? KhanColors.GRAY_C : "#000000",
8992
+ opacity: options.isMobile ? 1 : gridOpacity,
8993
+ step: gridStep,
8994
+ strokeWidth: options.isMobile ? 1 : 2
8995
+ });
8996
+ }
8908
8997
 
8909
- // Find starting coordinate: first anchor point curve left of xRange[0]
8910
- let initial = c / b;
8911
- const distToEdge = initial - xRange[0];
8912
- initial -= quarterPeriod * Math.ceil(distToEdge / quarterPeriod);
8998
+ // draw axes
8999
+ if (axes) {
9000
+ // this is a slight hack until <-> arrowheads work
9001
+ if (axisArrows === "<->" || axisArrows === true) {
9002
+ const thisGraphie = this;
9003
+ this.style({
9004
+ stroke: options.isMobile ? KhanColors.GRAY_G : "#000000",
9005
+ opacity: options.isMobile ? 1 : axisOpacity,
9006
+ strokeWidth: options.isMobile ? 1 : 2,
9007
+ arrows: "->"
9008
+ }, function () {
9009
+ if (range[1][0] < 0 && range[1][1] > 0) {
9010
+ thisGraphie.path([axisCenter, [gridRange[0][0], axisCenter[1]]]);
9011
+ thisGraphie.path([axisCenter, [gridRange[0][1], axisCenter[1]]]);
9012
+ }
9013
+ if (range[0][0] < 0 && range[0][1] > 0) {
9014
+ thisGraphie.path([axisCenter, [axisCenter[0], gridRange[1][0]]]);
9015
+ thisGraphie.path([axisCenter, [axisCenter[0], gridRange[1][1]]]);
9016
+ }
9017
+ });
8913
9018
 
8914
- // First portion of path is special-case, requiring move-to ('M')
8915
- let coords = coordsForOffset(initial, 0);
8916
- let path = "M" + coords[0][0] + "," + coords[0][1] + " C" + coords[1][0] + "," + coords[1][1] + " " + coords[2][0] + "," + coords[2][1] + " " + coords[3][0] + "," + coords[3][1];
8917
- for (let i = 1; i < numQuarterPeriods; i++) {
8918
- coords = coordsForOffset(initial, i);
8919
- path += " C" + coords[1][0] + "," + coords[1][1] + " " + coords[2][0] + "," + coords[2][1] + " " + coords[3][0] + "," + coords[3][1];
9019
+ // also, we don't support "<-" arrows yet, but why you
9020
+ // would want that on your graph is beyond me.
9021
+ } else if (axisArrows === "->" || axisArrows === "") {
9022
+ const thisGraphie = this;
9023
+ this.style({
9024
+ stroke: "#000000",
9025
+ opacity: axisOpacity,
9026
+ strokeWidth: 2,
9027
+ arrows: axisArrows
9028
+ }, function () {
9029
+ thisGraphie.path([[gridRange[0][0], axisCenter[1]], [gridRange[0][1], axisCenter[1]]]);
9030
+ thisGraphie.path([[axisCenter[0], gridRange[1][0]], [axisCenter[0], gridRange[1][1]]]);
9031
+ });
9032
+ }
9033
+ if (axisLabels && axisLabels.length === 2) {
9034
+ this.label([gridRange[0][1], axisCenter[1]], axisLabels[0], "right");
9035
+ this.label([axisCenter[0], gridRange[1][1]], axisLabels[1], "above");
9036
+ }
8920
9037
  }
8921
- return path;
8922
- };
8923
9038
 
8924
- // `svgPath` is independent of graphie range, so we export it independently
8925
- GraphUtils.svgPath = svgPath;
8926
- const processAttributes = function processAttributes(attrs) {
8927
- const transformers = {
8928
- scale: function (scale) {
8929
- if (typeof scale === "number") {
8930
- scale = [scale, scale];
9039
+ // draw tick marks
9040
+ if (ticks) {
9041
+ const halfWidthTicks = options.isMobile;
9042
+ const thisGraphie = this;
9043
+ this.style({
9044
+ stroke: options.isMobile ? KhanColors.GRAY_G : "#000000",
9045
+ opacity: options.isMobile ? 1 : tickOpacity,
9046
+ strokeWidth: 1
9047
+ }, function () {
9048
+ // horizontal axis
9049
+ let step = gridStep[0] * tickStep[0];
9050
+ let len = tickLen[0] / scale[1];
9051
+ let start = gridRange[0][0];
9052
+ let stop = gridRange[0][1];
9053
+ if (range[1][0] < 0 && range[1][1] > 0) {
9054
+ for (let x = step + axisCenter[0]; x <= stop; x += step) {
9055
+ if (x < stop || !axisArrows) {
9056
+ thisGraphie.line([x, -len + axisCenter[1]], [x, halfWidthTicks ? 0 : len + axisCenter[1]]);
9057
+ }
9058
+ }
9059
+ for (let x = -step + axisCenter[0]; x >= start; x -= step) {
9060
+ if (x > start || !axisArrows) {
9061
+ thisGraphie.line([x, -len + axisCenter[1]], [x, halfWidthTicks ? 0 : len + axisCenter[1]]);
9062
+ }
9063
+ }
8931
9064
  }
8932
- xScale = scale[0];
8933
- yScale = scale[1];
8934
9065
 
8935
- // Update the canvas size
8936
- raphael$1.setSize((xRange[1] - xRange[0]) * xScale, (yRange[1] - yRange[0]) * yScale);
9066
+ // vertical axis
9067
+ step = gridStep[1] * tickStep[1];
9068
+ len = tickLen[1] / scale[0];
9069
+ start = gridRange[1][0];
9070
+ stop = gridRange[1][1];
9071
+ if (range[0][0] < 0 && range[0][1] > 0) {
9072
+ for (let y = step + axisCenter[1]; y <= stop; y += step) {
9073
+ if (y < stop || !axisArrows) {
9074
+ // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
9075
+ this.line([-len + axisCenter[0], y], [halfWidthTicks ? 0 : len + axisCenter[0], y]);
9076
+ }
9077
+ }
9078
+ for (let y = -step + axisCenter[1]; y >= start; y -= step) {
9079
+ if (y > start || !axisArrows) {
9080
+ // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
9081
+ this.line([-len + axisCenter[0], y], [halfWidthTicks ? 0 : len + axisCenter[0], y]);
9082
+ }
9083
+ }
9084
+ }
9085
+ });
9086
+ }
9087
+
9088
+ // draw axis labels
9089
+ if (labels) {
9090
+ const thisGraphie = this;
9091
+ this.style({
9092
+ stroke: options.isMobile ? KhanColors.GRAY_G : "#000000",
9093
+ opacity: options.isMobile ? 1 : labelOpacity
9094
+ }, function () {
9095
+ // horizontal axis
9096
+ let step = gridStep[0] * tickStep[0] * labelStep[0];
9097
+ let start = gridRange[0][0];
9098
+ let stop = gridRange[0][1];
9099
+ const xAxisPosition = axisCenter[0] < 0 ? "above" : "below";
9100
+ const yAxisPosition = axisCenter[0] < 0 ? "right" : "left";
9101
+ const xShowZero = axisCenter[0] === 0 && axisCenter[1] !== 0;
9102
+ const yShowZero = axisCenter[0] !== 0 && axisCenter[1] === 0;
9103
+ const axisOffCenter = axisCenter[0] !== 0 || axisCenter[1] !== 0;
9104
+ const showUnityX = unityLabels[0] || axisOffCenter;
9105
+ const showUnityY = unityLabels[1] || axisOffCenter;
9106
+
9107
+ // positive x-axis
9108
+ for (let x = (xShowZero ? 0 : step) + axisCenter[0]; x <= stop; x += step) {
9109
+ if (x < stop || !axisArrows) {
9110
+ thisGraphie.label([x, axisCenter[1]], xLabelFormat(x), xAxisPosition);
9111
+ }
9112
+ }
9113
+
9114
+ // negative x-axis
9115
+ for (let x = -step * (showUnityX ? 1 : 2) + axisCenter[0]; x >= start; x -= step) {
9116
+ if (x > start || !axisArrows) {
9117
+ thisGraphie.label([x, axisCenter[1]], xLabelFormat(x), xAxisPosition);
9118
+ }
9119
+ }
9120
+ step = gridStep[1] * tickStep[1] * labelStep[1];
9121
+ start = gridRange[1][0];
9122
+ stop = gridRange[1][1];
9123
+
9124
+ // positive y-axis
9125
+ for (let y = (yShowZero ? 0 : step) + axisCenter[1]; y <= stop; y += step) {
9126
+ if (y < stop || !axisArrows) {
9127
+ thisGraphie.label([axisCenter[0], y], yLabelFormat(y), yAxisPosition);
9128
+ }
9129
+ }
9130
+
9131
+ // negative y-axis
9132
+ for (let y = -step * (showUnityY ? 1 : 2) + axisCenter[1]; y >= start; y -= step) {
9133
+ if (y > start || !axisArrows) {
9134
+ thisGraphie.label([axisCenter[0], y], yLabelFormat(y), yAxisPosition);
9135
+ }
9136
+ }
9137
+ });
9138
+ }
9139
+ }
9140
+ drawingTransform() {
9141
+ if (_classPrivateFieldBase(this, _drawingTransform)[_drawingTransform] == null) {
9142
+ throw new Error("Can't get drawingTransform of an uninitialized Graphie");
9143
+ }
9144
+ return _classPrivateFieldBase(this, _drawingTransform)[_drawingTransform];
9145
+ }
9146
+ bounds() {
9147
+ if (_classPrivateFieldBase(this, _bounds)[_bounds] == null) {
9148
+ throw new Error("Can't get bounds of an uninitialized Graphie");
9149
+ }
9150
+ return _classPrivateFieldBase(this, _bounds)[_bounds];
9151
+ }
9152
+ style(attrs, fn) {
9153
+ const processed = this.processAttributes(attrs);
9154
+ if (typeof fn === "function") {
9155
+ const oldStyle = this.currentStyle;
9156
+ this.currentStyle = $.extend({}, this.currentStyle, processed);
9157
+ const result = fn.call(this);
9158
+ this.currentStyle = oldStyle;
9159
+ return result;
9160
+ }
9161
+ $.extend(this.currentStyle, processed);
9162
+ }
9163
+ label(point, text, direction, latex) {}
9164
+ grid(xr, yr, styleAttributes) {}
9165
+
9166
+ // path is a stub that gets overwritten with a function from drawingTools
9167
+ // in createGraphie
9168
+ path(points) {}
9169
+
9170
+ // line is a stub that gets overwritten with a function from drawingTools
9171
+ // in createGraphie
9172
+ line(start, end) {}
9173
+ processAttributes(attrs) {
9174
+ const transformers = {
9175
+ scale: scale => {
9176
+ this.drawingTransform().setScale(scale);
8937
9177
  },
8938
- clipRect: function (pair) {
9178
+ clipRect: pair => {
8939
9179
  const point = pair[0];
8940
9180
  const size = pair[1];
8941
9181
  point[1] += size[1]; // because our coordinates are flipped
8942
9182
 
8943
9183
  return {
8944
- "clip-rect": scalePoint(point).concat(scaleVector(size)).join(" ")
9184
+ "clip-rect": this.scalePoint(point).concat(this.scaleVector(size)).join(" ")
8945
9185
  };
8946
9186
  },
8947
9187
  strokeWidth: function (val) {
@@ -8949,18 +9189,18 @@ GraphUtils.createGraphie = function (el) {
8949
9189
  "stroke-width": parseFloat(val)
8950
9190
  };
8951
9191
  },
8952
- rx: function (val) {
9192
+ rx: val => {
8953
9193
  return {
8954
- rx: scaleVector([val, 0])[0]
9194
+ rx: this.scaleVector([val, 0])[0]
8955
9195
  };
8956
9196
  },
8957
- ry: function (val) {
9197
+ ry: val => {
8958
9198
  return {
8959
- ry: scaleVector([0, val])[1]
9199
+ ry: this.scaleVector([0, val])[1]
8960
9200
  };
8961
9201
  },
8962
- r: function (val) {
8963
- const scaled = scaleVector([val, val]);
9202
+ r: val => {
9203
+ const scaled = this.scaleVector([val, val]);
8964
9204
  return {
8965
9205
  rx: scaled[0],
8966
9206
  ry: scaled[1]
@@ -8980,7 +9220,66 @@ GraphUtils.createGraphie = function (el) {
8980
9220
  }
8981
9221
  });
8982
9222
  return processed;
9223
+ }
9224
+ }
9225
+ GraphUtils.Graphie = Graphie$1;
9226
+ const labelDirections = {
9227
+ center: [-0.5, -0.5],
9228
+ above: [-0.5, -1.0],
9229
+ "above right": [0.0, -1.0],
9230
+ right: [0.0, -0.5],
9231
+ "below right": [0.0, 0.0],
9232
+ below: [-0.5, 0.0],
9233
+ "below left": [-1.0, 0.0],
9234
+ left: [-1.0, -0.5],
9235
+ "above left": [-1.0, -1.0]
9236
+ };
9237
+
9238
+ /**
9239
+ * Safari applies some SVG-specific styles to things that are not SVGs, so we
9240
+ * need to exclude those styles from things that are not SVGs.
9241
+ *
9242
+ * To see this behavior in action, open https://codepen.io/anon/pen/zENEoa in
9243
+ * Safari.
9244
+ *
9245
+ * Usage `$.extend({}, someStyles, SVG_SPECIFIC_STYLE_MASK)`
9246
+ */
9247
+ const SVG_SPECIFIC_STYLE_MASK = {
9248
+ "stroke-width": null
9249
+ };
9250
+ GraphUtils.createGraphie = function (el) {
9251
+ const thisGraphie = new Graphie$1(el);
9252
+ const setLabelMargins = function setLabelMargins(span, size) {
9253
+ const $span = $(span);
9254
+ const direction = $span.data("labelDirection");
9255
+ let [width, height] = size;
9256
+ // This can happen when a span
9257
+ // is invisible but we still want to update the CSS. At worst, we will
9258
+ // be off by a few pixels instead of in a different position entirely.
9259
+ if (width === 0 && height === 0) {
9260
+ [width, height] = [1, 1];
9261
+ Log.log("Label size was 0x0 in graphie.js; using 1x1 instead");
9262
+ }
9263
+ $span.css("visibility", "");
9264
+ if (typeof direction === "number") {
9265
+ const x = Math.cos(direction);
9266
+ const y = Math.sin(direction);
9267
+ const scale = Math.min(width / 2 / Math.abs(x), height / 2 / Math.abs(y));
9268
+ $span.css({
9269
+ marginLeft: -width / 2 + x * scale,
9270
+ marginTop: -height / 2 - y * scale
9271
+ });
9272
+ } else {
9273
+ const multipliers = labelDirections[direction || "center"];
9274
+ $span.css({
9275
+ marginLeft: Math.round(width * multipliers[0]),
9276
+ marginTop: Math.round(height * multipliers[1])
9277
+ });
9278
+ }
8983
9279
  };
9280
+
9281
+ // `svgPath` is independent of graphie range, so we export it independently
9282
+ GraphUtils.svgPath = thisGraphie.svgPath;
8984
9283
  const addArrowheads = function arrows(path) {
8985
9284
  const type = path.constructor.prototype;
8986
9285
  if (type === raphael.exports.el) {
@@ -8988,21 +9287,21 @@ GraphUtils.createGraphie = function (el) {
8988
9287
  const w = path.attr("stroke-width");
8989
9288
  const s = 0.6 + 0.4 * w;
8990
9289
  const l = path.getTotalLength();
8991
- const set = raphael$1.set();
8992
- const head = raphael$1.path(graphie.isMobile ? "M-4,4 C-4,4 -0.25,0 -0.25,0 C-0.25,0 -4,-4 -4,-4" : "M-3 4 C-2.75 2.5 0 0.25 0.75 0C0 -0.25 -2.75 -2.5 -3 -4");
9290
+ const set = thisGraphie.raphael.set();
9291
+ const head = thisGraphie.raphael.path(thisGraphie.isMobile ? "M-4,4 C-4,4 -0.25,0 -0.25,0 C-0.25,0 -4,-4 -4,-4" : "M-3 4 C-2.75 2.5 0 0.25 0.75 0C0 -0.25 -2.75 -2.5 -3 -4");
8993
9292
  const end = path.getPointAtLength(l - 0.4);
8994
9293
  const almostTheEnd = path.getPointAtLength(l - 0.75 * s);
8995
9294
  const angle = Math.atan2(end.y - almostTheEnd.y, end.x - almostTheEnd.x) * 180 / Math.PI;
8996
9295
  const attrs = path.attr();
8997
9296
  delete attrs.path;
8998
9297
  let subpath = path.getSubpath(0, l - 0.75 * s);
8999
- subpath = raphael$1.path(subpath).attr(attrs);
9298
+ subpath = thisGraphie.raphael.path(subpath).attr(attrs);
9000
9299
  subpath.arrowheadsDrawn = true;
9001
9300
  path.remove();
9002
9301
 
9003
9302
  // For some unknown reason 0 doesn't work for the rotation
9004
9303
  // origin so we use a tiny number.
9005
- head.rotate(angle, graphie.isMobile ? 1e-5 : 0.75, 0).scale(s, s, 0.75, 0).translate(almostTheEnd.x, almostTheEnd.y).attr(attrs).attr({
9304
+ head.rotate(angle, thisGraphie.isMobile ? 1e-5 : 0.75, 0).scale(s, s, 0.75, 0).translate(almostTheEnd.x, almostTheEnd.y).attr(attrs).attr({
9006
9305
  "stroke-linejoin": "round",
9007
9306
  "stroke-linecap": "round"
9008
9307
  });
@@ -9018,767 +9317,424 @@ GraphUtils.createGraphie = function (el) {
9018
9317
  }
9019
9318
  return path;
9020
9319
  };
9021
- const drawingTools = {
9022
- circle: function (center, radius) {
9023
- return raphael$1.ellipse(...scalePoint(center).concat(scaleVector([radius, radius])));
9024
- },
9025
- // (x, y) is coordinate of bottom left corner
9026
- rect: function (x, y, width, height) {
9027
- // Raphael needs (x, y) to be coordinate of upper left corner
9028
- const corner = scalePoint([x, y + height]);
9029
- const dims = scaleVector([width, height]);
9030
- const elem = raphael$1.rect(...corner.concat(dims));
9031
- if (graphie.isMobile) {
9032
- elem.node.style.shapeRendering = "crispEdges";
9033
- }
9034
- return elem;
9035
- },
9036
- ellipse: function (center, radii) {
9037
- return raphael$1.ellipse(...scalePoint(center).concat(scaleVector(radii)));
9038
- },
9039
- fixedEllipse: function (center,
9040
- // Different type than Coord, this is radiusX, radiusY
9041
- radii, maxScale, padding) {
9042
- // Scale point and radius
9043
- const scaledPoint = scalePoint(center);
9044
- const scaledRadii = scaleVector(radii);
9045
- const width = 2 * scaledRadii[0] * maxScale + padding;
9046
- const height = 2 * scaledRadii[1] * maxScale + padding;
9047
-
9048
- // Calculate absolute left, top
9049
- const left = scaledPoint[0] - width / 2;
9050
- const top = scaledPoint[1] - height / 2;
9051
-
9052
- // Wrap in <div>
9053
- const wrapper = document.createElement("div");
9054
- $(wrapper).css({
9055
- position: "absolute",
9056
- width: width + "px",
9057
- height: height + "px",
9058
- left: left + "px",
9059
- top: top + "px"
9060
- });
9061
- // wrapper.setAttribute("data-graphie-type", "ellipse");
9320
+ function circle(center, radius) {
9321
+ return thisGraphie.raphael.ellipse(...thisGraphie.scalePoint(center).concat(thisGraphie.scaleVector([radius, radius])));
9322
+ }
9062
9323
 
9063
- // Create Raphael canvas
9064
- const localRaphael = raphael.exports(wrapper, width, height);
9065
- const visibleShape = localRaphael.ellipse(width / 2, height / 2, scaledRadii[0], scaledRadii[1]);
9066
- return {
9067
- wrapper: wrapper,
9068
- visibleShape: visibleShape
9069
- };
9070
- },
9071
- arc: function (center, radius, startAngle, endAngle, sector) {
9072
- startAngle = (startAngle % 360 + 360) % 360;
9073
- endAngle = (endAngle % 360 + 360) % 360;
9074
- const cent = scalePoint(center);
9075
- const radii = scaleVector(radius);
9076
- const startVector = polar(radius, startAngle);
9077
- const endVector = polar(radius, endAngle);
9078
- const startPoint = scalePoint([center[0] + startVector[0], center[1] + startVector[1]]);
9079
- const endPoint = scalePoint([center[0] + endVector[0], center[1] + endVector[1]]);
9080
- const largeAngle = ((endAngle - startAngle) % 360 + 360) % 360 > 180;
9081
- return raphael$1.path("M" + startPoint.join(" ") + "A" + radii.join(" ") + " 0 " + (
9082
- // ellipse rotation
9083
- largeAngle ? 1 : 0) + " 0 " +
9084
- // sweep flag
9085
- endPoint.join(" ") + (sector ? "L" + cent.join(" ") + "z" : ""));
9086
- },
9087
- path: function (points) {
9088
- // @ts-expect-error - TS2554 - Expected 2 arguments, but got 1.
9089
- const p = raphael$1.path(svgPath(points));
9090
- p.graphiePath = points;
9091
- return p;
9092
- },
9093
- fixedPath: function (points, center, createPath) {
9094
- points = _.map(points, scalePoint);
9095
- center = center ? scalePoint(center) : null;
9096
- createPath = createPath || svgPath;
9097
- const pathLeft = _.min(_.pluck(points, 0));
9098
- const pathRight = _.max(_.pluck(points, 0));
9099
- const pathTop = _.min(_.pluck(points, 1));
9100
- const pathBottom = _.max(_.pluck(points, 1));
9101
-
9102
- // Apply padding to line
9103
- const padding = [4, 4];
9104
-
9105
- // Calculate and apply additional offset
9106
- const extraOffset = [pathLeft, pathTop];
9107
-
9108
- // Apply padding and offset to points
9109
- points = _.map(points, function (point) {
9110
- return vector$1.add(vector$1.subtract(point, extraOffset), vector$1.scale(padding, 0.5));
9111
- });
9112
-
9113
- // Calculate <div> dimensions
9114
- const width = pathRight - pathLeft + padding[0];
9115
- const height = pathBottom - pathTop + padding[1];
9116
- const left = extraOffset[0] - padding[0] / 2;
9117
- const top = extraOffset[1] - padding[1] / 2;
9118
-
9119
- // Create <div>
9120
- const wrapper = document.createElement("div");
9121
- $(wrapper).css({
9122
- position: "absolute",
9123
- width: width + "px",
9124
- height: height + "px",
9125
- left: left + "px",
9126
- top: top + "px",
9127
- // If user specified a center, set it
9128
- // NOTE(kevinb): jQuery doesn't like that `transformOrigin `could be `null`
9129
- // so we cast to `any` here.
9130
- transformOrigin: center ? width / 2 + center[0] + "px " + (height / 2 + center[1]) + "px" : null
9131
- });
9324
+ // (x, y) is coordinate of bottom left corner
9325
+ function rect(x, y, width, height) {
9326
+ // Raphael needs (x, y) to be coordinate of upper left corner
9327
+ const corner = thisGraphie.scalePoint([x, y + height]);
9328
+ const dims = thisGraphie.scaleVector([width, height]);
9329
+ const elem = thisGraphie.raphael.rect(...corner.concat(dims));
9330
+ if (thisGraphie.isMobile) {
9331
+ elem.node.style.shapeRendering = "crispEdges";
9332
+ }
9333
+ return elem;
9334
+ }
9335
+ function ellipse(center, radii) {
9336
+ return thisGraphie.raphael.ellipse(...thisGraphie.scalePoint(center).concat(thisGraphie.scaleVector(radii)));
9337
+ }
9338
+ function fixedEllipse(center,
9339
+ // Different type than Coord, this is radiusX, radiusY
9340
+ radii, maxScale, padding) {
9341
+ // Scale point and radius
9342
+ const scaledPoint = thisGraphie.scalePoint(center);
9343
+ const scaledRadii = thisGraphie.scaleVector(radii);
9344
+ const width = 2 * scaledRadii[0] * maxScale + padding;
9345
+ const height = 2 * scaledRadii[1] * maxScale + padding;
9346
+
9347
+ // Calculate absolute left, top
9348
+ const left = scaledPoint[0] - width / 2;
9349
+ const top = scaledPoint[1] - height / 2;
9350
+
9351
+ // Wrap in <div>
9352
+ const wrapper = document.createElement("div");
9353
+ $(wrapper).css({
9354
+ position: "absolute",
9355
+ width: width + "px",
9356
+ height: height + "px",
9357
+ left: left + "px",
9358
+ top: top + "px"
9359
+ });
9360
+ // wrapper.setAttribute("data-graphie-type", "ellipse");
9132
9361
 
9133
- // Create Raphael canvas
9134
- const localRaphael = raphael.exports(wrapper, width, height);
9362
+ // Create Raphael canvas
9363
+ const localRaphael = raphael.exports(wrapper, width, height);
9364
+ const visibleShape = localRaphael.ellipse(width / 2, height / 2, scaledRadii[0], scaledRadii[1]);
9365
+ return {
9366
+ wrapper: wrapper,
9367
+ visibleShape: visibleShape
9368
+ };
9369
+ }
9370
+ function arc(center, radius, startAngle, endAngle, sector) {
9371
+ startAngle = (startAngle % 360 + 360) % 360;
9372
+ endAngle = (endAngle % 360 + 360) % 360;
9373
+ const cent = thisGraphie.scalePoint(center);
9374
+ const radii = thisGraphie.scaleVector(radius);
9375
+ const startVector = polar(radius, startAngle);
9376
+ const endVector = polar(radius, endAngle);
9377
+ const startPoint = thisGraphie.scalePoint([center[0] + startVector[0], center[1] + startVector[1]]);
9378
+ const endPoint = thisGraphie.scalePoint([center[0] + endVector[0], center[1] + endVector[1]]);
9379
+ const largeAngle = ((endAngle - startAngle) % 360 + 360) % 360 > 180;
9380
+ return thisGraphie.raphael.path("M" + startPoint.join(" ") + "A" + radii.join(" ") + " 0 " + (
9381
+ // ellipse rotation
9382
+ largeAngle ? 1 : 0) + " 0 " +
9383
+ // sweep flag
9384
+ endPoint.join(" ") + (sector ? "L" + cent.join(" ") + "z" : ""));
9385
+ }
9386
+ function path(points) {
9387
+ // @ts-expect-error - TS2554 - Expected 2 arguments, but got 1.
9388
+ const p = thisGraphie.raphael.path(thisGraphie.svgPath(points));
9389
+ p.graphiePath = points;
9390
+ return p;
9391
+ }
9392
+ function fixedPath(points, center, createPath) {
9393
+ points = _.map(points, thisGraphie.scalePoint);
9394
+ center = center ? thisGraphie.scalePoint(center) : null;
9395
+ createPath = createPath || thisGraphie.svgPath;
9396
+ const pathLeft = _.min(_.pluck(points, 0));
9397
+ const pathRight = _.max(_.pluck(points, 0));
9398
+ const pathTop = _.min(_.pluck(points, 1));
9399
+ const pathBottom = _.max(_.pluck(points, 1));
9400
+
9401
+ // Apply padding to line
9402
+ const padding = [4, 4];
9403
+
9404
+ // Calculate and apply additional offset
9405
+ const extraOffset = [pathLeft, pathTop];
9406
+
9407
+ // Apply padding and offset to points
9408
+ points = _.map(points, function (point) {
9409
+ return vector$1.add(vector$1.subtract(point, extraOffset), vector$1.scale(padding, 0.5));
9410
+ });
9135
9411
 
9136
- // Calculate path
9137
- const visibleShape = localRaphael.path(createPath(points));
9138
- return {
9139
- wrapper: wrapper,
9140
- visibleShape: visibleShape
9141
- };
9142
- },
9143
- scaledPath: function (points) {
9144
- const p = raphael$1.path(svgPath(points, /* alreadyScaled */true));
9145
- p.graphiePath = points;
9146
- return p;
9147
- },
9148
- line: function (start, end) {
9149
- const l = this.path([start, end]);
9150
- if (graphie.isMobile) {
9151
- l.node.style.shapeRendering = "crispEdges";
9152
- }
9153
- return l;
9154
- },
9155
- parabola: function (a, b, c) {
9156
- // Plot a parabola of the form: f(x) = (a * x + b) * x + c
9157
- return raphael$1.path(svgParabolaPath(a, b, c));
9158
- },
9159
- fixedLine: function (start, end, thickness) {
9160
- // Apply padding to line
9161
- const padding = [thickness, thickness];
9162
-
9163
- // Scale points to get values in pixels
9164
- start = scalePoint(start);
9165
- end = scalePoint(end);
9166
-
9167
- // Calculate and apply additional offset
9168
- const extraOffset = [Math.min(start[0], end[0]), Math.min(start[1], end[1])];
9169
-
9170
- // Apply padding and offset to start, end points
9171
- start = vector$1.add(vector$1.subtract(start, extraOffset), vector$1.scale(padding, 0.5));
9172
- end = vector$1.add(vector$1.subtract(end, extraOffset), vector$1.scale(padding, 0.5));
9173
-
9174
- // Calculate <div> dimensions
9175
- const left = extraOffset[0] - padding[0] / 2;
9176
- const top = extraOffset[1] - padding[1] / 2;
9177
- const width = Math.abs(start[0] - end[0]) + padding[0];
9178
- const height = Math.abs(start[1] - end[1]) + padding[1];
9179
-
9180
- // Create <div>
9181
- const wrapper = document.createElement("div");
9182
- $(wrapper).css({
9183
- position: "absolute",
9184
- width: width + "px",
9185
- height: height + "px",
9186
- left: left + "px",
9187
- top: top + "px",
9188
- // Outsiders should feel like the line's 'origin' (i.e., for
9189
- // rotation) is the starting point
9190
- transformOrigin: start[0] + "px " + start[1] + "px"
9191
- });
9192
-
9193
- // Create Raphael canvas
9194
- const localRaphael = raphael.exports(wrapper, width, height);
9195
-
9196
- // Calculate path
9197
- const path = "M" + start[0] + " " + start[1] + " " + "L" + end[0] + " " + end[1];
9198
- const visibleShape = localRaphael.path(path);
9199
- visibleShape.graphiePath = [start, end];
9200
- return {
9201
- wrapper: wrapper,
9202
- visibleShape: visibleShape
9203
- };
9204
- },
9205
- sinusoid: function (a, b, c, d) {
9206
- // Plot a sinusoid of the form: f(x) = a * sin(b * x - c) + d
9207
- return raphael$1.path(svgSinusoidPath(a, b, c, d));
9208
- },
9209
- grid: function (xr, yr) {
9210
- const step = currentStyle.step || [1, 1];
9211
- const set = raphael$1.set();
9212
- let x = step[0] * Math.ceil(xr[0] / step[0]);
9213
- for (; x <= xr[1]; x += step[0]) {
9214
- set.push(this.line([x, yr[0]], [x, yr[1]]));
9215
- }
9216
- let y = step[1] * Math.ceil(yr[0] / step[1]);
9217
- for (; y <= yr[1]; y += step[1]) {
9218
- set.push(this.line([xr[0], y], [xr[1], y]));
9219
- }
9220
- return set;
9221
- },
9222
- label: function (point, text, direction, latex) {
9223
- latex = typeof latex === "undefined" || latex;
9224
- const $span = $("<span>").addClass("graphie-label");
9225
- const pad = currentStyle["label-distance"];
9226
- $span.css($.extend({}, {
9227
- position: "absolute",
9228
- padding: (pad != null ? pad : 7) + "px",
9229
- color: "black"
9230
- })).data("labelDirection", direction).appendTo(el);
9231
-
9232
- // @ts-expect-error - TS2339 - Property 'setPosition' does not exist on type 'JQuery<HTMLElement>'.
9233
- $span.setPosition = function (point) {
9234
- const scaledPoint = scalePoint(point);
9235
- $span.css({
9236
- left: scaledPoint[0],
9237
- top: scaledPoint[1]
9238
- });
9239
- };
9412
+ // Calculate <div> dimensions
9413
+ const width = pathRight - pathLeft + padding[0];
9414
+ const height = pathBottom - pathTop + padding[1];
9415
+ const left = extraOffset[0] - padding[0] / 2;
9416
+ const top = extraOffset[1] - padding[1] / 2;
9240
9417
 
9241
- // @ts-expect-error - TS2339 - Property 'setPosition' does not exist on type 'JQuery<HTMLElement>'.
9242
- $span.setPosition(point);
9243
- const span = $span[0];
9418
+ // Create <div>
9419
+ const wrapper = document.createElement("div");
9420
+ $(wrapper).css({
9421
+ position: "absolute",
9422
+ width: width + "px",
9423
+ height: height + "px",
9424
+ left: left + "px",
9425
+ top: top + "px",
9426
+ // If user specified a center, set it
9427
+ // NOTE(kevinb): jQuery doesn't like that `transformOrigin `could be `null`
9428
+ // so we cast to `any` here.
9429
+ transformOrigin: center ? width / 2 + center[0] + "px " + (height / 2 + center[1]) + "px" : null
9430
+ });
9244
9431
 
9245
- // @ts-expect-error - TS2339 - Property 'processMath' does not exist on type 'JQuery<HTMLElement>'.
9246
- $span.processMath = function (math, force) {
9247
- processMath$1(span, math, force, function () {
9248
- const width = span.scrollWidth;
9249
- const height = span.scrollHeight;
9250
- setLabelMargins(span, [width, height]);
9251
- });
9252
- };
9432
+ // Create Raphael canvas
9433
+ const localRaphael = raphael.exports(wrapper, width, height);
9253
9434
 
9254
- // @ts-expect-error - TS2339 - Property 'processText' does not exist on type 'JQuery<HTMLElement>'.
9255
- $span.processText = function (text) {
9256
- $span.html(text);
9435
+ // Calculate path
9436
+ const visibleShape = localRaphael.path(createPath(points));
9437
+ return {
9438
+ wrapper: wrapper,
9439
+ visibleShape: visibleShape
9440
+ };
9441
+ }
9442
+ function scaledPath(points) {
9443
+ const p = thisGraphie.raphael.path(thisGraphie.svgPath(points, /* alreadyScaled */true));
9444
+ p.graphiePath = points;
9445
+ return p;
9446
+ }
9447
+ function line(start, end) {
9448
+ const l = path([start, end]);
9449
+ if (thisGraphie.isMobile) {
9450
+ l.node.style.shapeRendering = "crispEdges";
9451
+ }
9452
+ return l;
9453
+ }
9454
+ function parabola(a, b, c) {
9455
+ // Plot a parabola of the form: f(x) = (a * x + b) * x + c
9456
+ return thisGraphie.raphael.path(thisGraphie.svgParabolaPath(a, b, c));
9457
+ }
9458
+ function fixedLine(start, end, thickness) {
9459
+ // Apply padding to line
9460
+ const padding = [thickness, thickness];
9461
+
9462
+ // Scale points to get values in pixels
9463
+ start = thisGraphie.scalePoint(start);
9464
+ end = thisGraphie.scalePoint(end);
9465
+
9466
+ // Calculate and apply additional offset
9467
+ const extraOffset = [Math.min(start[0], end[0]), Math.min(start[1], end[1])];
9468
+
9469
+ // Apply padding and offset to start, end points
9470
+ start = vector$1.add(vector$1.subtract(start, extraOffset), vector$1.scale(padding, 0.5));
9471
+ end = vector$1.add(vector$1.subtract(end, extraOffset), vector$1.scale(padding, 0.5));
9472
+
9473
+ // Calculate <div> dimensions
9474
+ const left = extraOffset[0] - padding[0] / 2;
9475
+ const top = extraOffset[1] - padding[1] / 2;
9476
+ const width = Math.abs(start[0] - end[0]) + padding[0];
9477
+ const height = Math.abs(start[1] - end[1]) + padding[1];
9478
+
9479
+ // Create <div>
9480
+ const wrapper = document.createElement("div");
9481
+ $(wrapper).css({
9482
+ position: "absolute",
9483
+ width: width + "px",
9484
+ height: height + "px",
9485
+ left: left + "px",
9486
+ top: top + "px",
9487
+ // Outsiders should feel like the line's 'origin' (i.e., for
9488
+ // rotation) is the starting point
9489
+ transformOrigin: start[0] + "px " + start[1] + "px"
9490
+ });
9491
+
9492
+ // Create Raphael canvas
9493
+ const localRaphael = raphael.exports(wrapper, width, height);
9494
+
9495
+ // Calculate path
9496
+ const path = "M" + start[0] + " " + start[1] + " " + "L" + end[0] + " " + end[1];
9497
+ const visibleShape = localRaphael.path(path);
9498
+ visibleShape.graphiePath = [start, end];
9499
+ return {
9500
+ wrapper: wrapper,
9501
+ visibleShape: visibleShape
9502
+ };
9503
+ }
9504
+ function sinusoid(a, b, c, d) {
9505
+ // Plot a sinusoid of the form: f(x) = a * sin(b * x - c) + d
9506
+ return thisGraphie.raphael.path(thisGraphie.svgSinusoidPath(a, b, c, d));
9507
+ }
9508
+ function grid(xr, yr) {
9509
+ const step = thisGraphie.currentStyle.step || [1, 1];
9510
+ const set = thisGraphie.raphael.set();
9511
+ let x = step[0] * Math.ceil(xr[0] / step[0]);
9512
+ for (; x <= xr[1]; x += step[0]) {
9513
+ set.push(line([x, yr[0]], [x, yr[1]]));
9514
+ }
9515
+ let y = step[1] * Math.ceil(yr[0] / step[1]);
9516
+ for (; y <= yr[1]; y += step[1]) {
9517
+ set.push(line([xr[0], y], [xr[1], y]));
9518
+ }
9519
+ return set;
9520
+ }
9521
+ function label(point, text, direction, latex) {
9522
+ latex = typeof latex === "undefined" || latex;
9523
+ const $span = $("<span>").addClass("graphie-label");
9524
+ const pad = thisGraphie.currentStyle["label-distance"];
9525
+ $span.css($.extend({}, {
9526
+ position: "absolute",
9527
+ padding: (pad != null ? pad : 7) + "px",
9528
+ color: "black"
9529
+ })).data("labelDirection", direction).appendTo(thisGraphie.el);
9530
+
9531
+ // @ts-expect-error - TS2339 - Property 'setPosition' does not exist on type 'JQuery<HTMLElement>'.
9532
+ $span.setPosition = function (point) {
9533
+ const scaledPoint = thisGraphie.scalePoint(point);
9534
+ $span.css({
9535
+ left: scaledPoint[0],
9536
+ top: scaledPoint[1]
9537
+ });
9538
+ };
9539
+
9540
+ // @ts-expect-error - TS2339 - Property 'setPosition' does not exist on type 'JQuery<HTMLElement>'.
9541
+ $span.setPosition(point);
9542
+ const span = $span[0];
9543
+
9544
+ // @ts-expect-error - TS2339 - Property 'processMath' does not exist on type 'JQuery<HTMLElement>'.
9545
+ $span.processMath = function (math, force) {
9546
+ processMath$1(span, math, force, function () {
9257
9547
  const width = span.scrollWidth;
9258
9548
  const height = span.scrollHeight;
9259
9549
  setLabelMargins(span, [width, height]);
9260
- };
9261
- if (latex) {
9262
- // @ts-expect-error - TS2339 - Property 'processMath' does not exist on type 'JQuery<HTMLElement>'.
9263
- $span.processMath(text, /* force */false);
9264
- } else {
9265
- // @ts-expect-error - TS2339 - Property 'processText' does not exist on type 'JQuery<HTMLElement>'.
9266
- $span.processText(text);
9267
- }
9268
- return $span;
9269
- },
9270
- plotParametric: function (fn, range, shade, fn2) {
9271
- // Note: fn2 should only be set if 'shade' is true, as it denotes
9272
- // the function between which fn should have its area shaded.
9273
- // In general, plotParametric shouldn't be used to shade the area
9274
- // between two arbitrary parametrics functions over an interval,
9275
- // as the method assumes that fn and fn2 are both of the form
9276
- // fn(t) = (t, fn'(t)) for some initial fn'.
9277
- fn2 = fn2 || function (t) {
9278
- return [t, 0];
9279
- };
9550
+ });
9551
+ };
9280
9552
 
9281
- // We truncate to 500,000, since anything bigger causes
9282
- // overflow in the firefox svg renderer. This is safe
9283
- // since 500,000 is outside the viewport anyway. We
9284
- // write these functions the way we do to handle undefined.
9285
- const clipper = xy => {
9286
- if (Math.abs(xy[1]) > 500000) {
9287
- return [xy[0], Math.min(Math.max(xy[1], -500000), 500000)];
9288
- }
9289
- return xy;
9290
- };
9291
- const clippedFn = x => clipper(fn(x));
9292
- const clippedFn2 = x => clipper(fn2(x));
9293
- if (!currentStyle.strokeLinejoin) {
9294
- currentStyle.strokeLinejoin = "round";
9295
- }
9296
- if (!currentStyle.strokeLinecap) {
9297
- currentStyle.strokeLinecap = "round";
9298
- }
9299
- const min = range[0];
9300
- const max = range[1];
9301
- let step = (max - min) / (currentStyle["plot-points"] || 800);
9302
- if (step === 0) {
9303
- step = 1;
9304
- }
9305
- const paths = raphael$1.set();
9306
- let points = [];
9307
- let lastDiff = GraphUtils.coordDiff(clippedFn(min), clippedFn2(min));
9308
- let lastFlip = min;
9309
- for (let t = min; t <= max; t += step) {
9310
- const top = clippedFn(t);
9311
- const bottom = clippedFn2(t);
9312
- const diff = GraphUtils.coordDiff(top, bottom);
9313
-
9314
- // Find points where it flips
9315
- // Create path that sketches area between the two functions
9316
- if (
9317
- // if there is an asymptote here, meaning that the graph
9318
- // switches signs and has a large difference
9319
- diff[1] < 0 !== lastDiff[1] < 0 && Math.abs(diff[1] - lastDiff[1]) > 2 * yScale ||
9320
- // or the function is undefined
9321
- isNaN(diff[1])) {
9322
- // split the path at this point, and draw it
9323
- if (shade) {
9324
- // @ts-expect-error - TS2345 - Argument of type 'any' is not assignable to parameter of type 'never'.
9325
- points.push(top);
9553
+ // @ts-expect-error - TS2339 - Property 'processText' does not exist on type 'JQuery<HTMLElement>'.
9554
+ $span.processText = function (text) {
9555
+ $span.html(text);
9556
+ const width = span.scrollWidth;
9557
+ const height = span.scrollHeight;
9558
+ setLabelMargins(span, [width, height]);
9559
+ };
9560
+ if (latex) {
9561
+ // @ts-expect-error - TS2339 - Property 'processMath' does not exist on type 'JQuery<HTMLElement>'.
9562
+ $span.processMath(text, /* force */false);
9563
+ } else {
9564
+ // @ts-expect-error - TS2339 - Property 'processText' does not exist on type 'JQuery<HTMLElement>'.
9565
+ $span.processText(text);
9566
+ }
9567
+ return $span;
9568
+ }
9569
+ function plotParametric(fn, range, shade, fn2 = t => [t, 0]) {
9570
+ // Note: fn2 should only be set if 'shade' is true, as it denotes
9571
+ // the function between which fn should have its area shaded.
9572
+ // In general, plotParametric shouldn't be used to shade the area
9573
+ // between two arbitrary parametrics functions over an interval,
9574
+ // as the method assumes that fn and fn2 are both of the form
9575
+ // fn(t) = (t, fn'(t)) for some initial fn'.
9576
+
9577
+ // We truncate to 500,000, since anything bigger causes
9578
+ // overflow in the firefox svg renderer. This is safe
9579
+ // since 500,000 is outside the viewport anyway. We
9580
+ // write these functions the way we do to handle undefined.
9581
+ const clipper = xy => {
9582
+ if (Math.abs(xy[1]) > 500000) {
9583
+ return [xy[0], Math.min(Math.max(xy[1], -500000), 500000)];
9584
+ }
9585
+ return xy;
9586
+ };
9587
+ const clippedFn = x => clipper(fn(x));
9588
+ const clippedFn2 = x => clipper(fn2(x));
9589
+ if (!thisGraphie.currentStyle.strokeLinejoin) {
9590
+ thisGraphie.currentStyle.strokeLinejoin = "round";
9591
+ }
9592
+ if (!thisGraphie.currentStyle.strokeLinecap) {
9593
+ thisGraphie.currentStyle.strokeLinecap = "round";
9594
+ }
9595
+ const min = range[0];
9596
+ const max = range[1];
9597
+ let step = (max - min) / (thisGraphie.currentStyle["plot-points"] || 800);
9598
+ if (step === 0) {
9599
+ step = 1;
9600
+ }
9601
+ const paths = thisGraphie.raphael.set();
9602
+ let points = [];
9603
+ let lastDiff = GraphUtils.coordDiff(clippedFn(min), clippedFn2(min));
9604
+ let lastFlip = min;
9605
+ for (let t = min; t <= max; t += step) {
9606
+ const top = clippedFn(t);
9607
+ const bottom = clippedFn2(t);
9608
+ const diff = GraphUtils.coordDiff(top, bottom);
9609
+
9610
+ // Find points where it flips
9611
+ // Create path that sketches area between the two functions
9612
+ if (
9613
+ // if there is an asymptote here, meaning that the graph
9614
+ // switches signs and has a large difference
9615
+ diff[1] < 0 !== lastDiff[1] < 0 && Math.abs(diff[1] - lastDiff[1]) > 2 * thisGraphie.drawingTransform().pixelsPerUnitY() ||
9616
+ // or the function is undefined
9617
+ isNaN(diff[1])) {
9618
+ // split the path at this point, and draw it
9619
+ if (shade) {
9620
+ // @ts-expect-error - TS2345 - Argument of type 'any' is not assignable to parameter of type 'never'.
9621
+ points.push(top);
9326
9622
 
9327
- // backtrack to draw paired function
9328
- for (let u = t - step; u >= lastFlip; u -= step) {
9329
- // @ts-expect-error - TS2345 - Argument of type 'any' is not assignable to parameter of type 'never'.
9330
- points.push(clippedFn2(u));
9331
- }
9332
- lastFlip = t;
9333
- }
9334
- paths.push(this.path(points));
9335
- // restart the path, excluding this point
9336
- points = [];
9337
- if (shade) {
9623
+ // backtrack to draw paired function
9624
+ for (let u = t - step; u >= lastFlip; u -= step) {
9338
9625
  // @ts-expect-error - TS2345 - Argument of type 'any' is not assignable to parameter of type 'never'.
9339
- points.push(top);
9626
+ points.push(clippedFn2(u));
9340
9627
  }
9341
- } else {
9342
- // otherwise, just add the point to the path
9343
- // @ts-expect-error - TS2345 - Argument of type 'any' is not assignable to parameter of type 'never'.
9344
- points.push(top);
9628
+ lastFlip = t;
9345
9629
  }
9346
- lastDiff = diff;
9347
- }
9348
- if (shade) {
9349
- // backtrack to draw paired function
9350
- for (let u = max - step; u >= lastFlip; u -= step) {
9630
+ paths.push(path(points));
9631
+ // restart the path, excluding this point
9632
+ points = [];
9633
+ if (shade) {
9351
9634
  // @ts-expect-error - TS2345 - Argument of type 'any' is not assignable to parameter of type 'never'.
9352
- points.push(clippedFn2(u));
9635
+ points.push(top);
9353
9636
  }
9637
+ } else {
9638
+ // otherwise, just add the point to the path
9639
+ // @ts-expect-error - TS2345 - Argument of type 'any' is not assignable to parameter of type 'never'.
9640
+ points.push(top);
9354
9641
  }
9355
- paths.push(this.path(points));
9356
- return paths;
9357
- },
9358
- plotPolar: function (fn, range) {
9359
- const min = range[0];
9360
- const max = range[1];
9361
-
9362
- // There is probably a better heuristic for this
9363
- if (!currentStyle["plot-points"]) {
9364
- currentStyle["plot-points"] = 2 * (max - min) * xScale;
9365
- }
9366
- return this.plotParametric(function (th) {
9367
- return polar(fn(th), th * 180 / Math.PI);
9368
- }, range);
9369
- },
9370
- plot: function (fn, range, swapAxes, shade, fn2) {
9371
- const min = range[0];
9372
- const max = range[1];
9373
- if (!currentStyle["plot-points"]) {
9374
- currentStyle["plot-points"] = 2 * (max - min) * xScale;
9375
- }
9376
- if (swapAxes) {
9377
- if (fn2) {
9378
- // TODO(charlie): support swapped axis area shading
9379
- throw new PerseusError("Can't shade area between functions with swapped axes.", Errors.Internal);
9380
- }
9381
- return this.plotParametric(function (y) {
9382
- return [fn(y), y];
9383
- }, range, shade);
9642
+ lastDiff = diff;
9643
+ }
9644
+ if (shade) {
9645
+ // backtrack to draw paired function
9646
+ for (let u = max - step; u >= lastFlip; u -= step) {
9647
+ // @ts-expect-error - TS2345 - Argument of type 'any' is not assignable to parameter of type 'never'.
9648
+ points.push(clippedFn2(u));
9384
9649
  }
9650
+ }
9651
+ paths.push(path(points));
9652
+ return paths;
9653
+ }
9654
+ function plot(fn, range, swapAxes, shade, fn2) {
9655
+ const min = range[0];
9656
+ const max = range[1];
9657
+ if (!thisGraphie.currentStyle["plot-points"]) {
9658
+ thisGraphie.currentStyle["plot-points"] = 2 * (max - min) * thisGraphie.drawingTransform().pixelsPerUnitX();
9659
+ }
9660
+ if (swapAxes) {
9385
9661
  if (fn2) {
9386
- if (shade) {
9387
- return this.plotParametric(function (x) {
9388
- return [x, fn(x)];
9389
- }, range, shade, function (x) {
9390
- return [x, fn2(x)];
9391
- });
9392
- }
9393
- throw new PerseusError("fn2 should only be set when 'shade' is True.", Errors.Internal);
9662
+ // TODO(charlie): support swapped axis area shading
9663
+ throw new PerseusError("Can't shade area between functions with swapped axes.", Errors.Internal);
9394
9664
  }
9395
- return this.plotParametric(function (x) {
9396
- return [x, fn(x)];
9665
+ return plotParametric(function (y) {
9666
+ return [fn(y), y];
9397
9667
  }, range, shade);
9398
- },
9399
- /**
9400
- * Given a piecewise function, return a Raphael set of paths that
9401
- * can be used to draw the function, e.g. using style().
9402
- * Calls plotParametric.
9403
- *
9404
- * @param {[]} fnArray array of functions which when called
9405
- * with a parameter i return the value of
9406
- * the function at i
9407
- * @param {[]} rangeArray array of ranges over which the
9408
- * corresponding functions are defined
9409
- * @return {Set<any>} set of paths
9410
- */
9411
- plotPiecewise: function (fnArray, rangeArray) {
9412
- const paths = raphael$1.set();
9413
- const self = this;
9414
- _.times(fnArray.length, function (i) {
9415
- const fn = fnArray[i];
9416
- const range = rangeArray[i];
9417
- const fnPaths = self.plotParametric(function (x) {
9668
+ }
9669
+ if (fn2) {
9670
+ if (shade) {
9671
+ return plotParametric(function (x) {
9418
9672
  return [x, fn(x)];
9419
- }, range);
9420
- _.each(fnPaths, function (fnPath) {
9421
- paths.push(fnPath);
9673
+ }, range, shade, function (x) {
9674
+ return [x, fn2(x)];
9422
9675
  });
9423
- });
9424
- return paths;
9425
- },
9426
- /**
9427
- * Given an array of coordinates of the form [x, y], create and
9428
- * return a Raphael set of Raphael circle objects at those
9429
- * coordinates
9430
- *
9431
- * @param {Array<[number, number]>} endpointArray
9432
- * @return {Set<any>} set of circles
9433
- */
9434
- plotEndpointCircles: function (endpointArray) {
9435
- const circles = raphael$1.set();
9436
- const self = this;
9437
- _.each(endpointArray, function (coord, i) {
9438
- circles.push(self.circle(coord, 0.15));
9439
- });
9440
- return circles;
9441
- },
9442
- plotAsymptotes: function (fn, range) {
9443
- const min = range[0];
9444
- const max = range[1];
9445
- const step = (max - min) / (currentStyle["plot-points"] || 800);
9446
- const asymptotes = raphael$1.set();
9447
- let lastVal = fn(min);
9448
- for (let t = min; t <= max; t += step) {
9449
- const funcVal = fn(t);
9450
- if (funcVal < 0 !== lastVal < 0 && Math.abs(funcVal - lastVal) > 2 * yScale) {
9451
- asymptotes.push(this.line([t, yScale], [t, -yScale]));
9452
- }
9453
- lastVal = funcVal;
9454
9676
  }
9455
- return asymptotes;
9677
+ throw new PerseusError("fn2 should only be set when 'shade' is True.", Errors.Internal);
9456
9678
  }
9679
+ return plotParametric(function (x) {
9680
+ return [x, fn(x)];
9681
+ }, range, shade);
9682
+ }
9683
+ const drawingTools = {
9684
+ circle,
9685
+ rect,
9686
+ ellipse,
9687
+ fixedEllipse,
9688
+ arc,
9689
+ path,
9690
+ fixedPath,
9691
+ scaledPath,
9692
+ line,
9693
+ parabola,
9694
+ fixedLine,
9695
+ sinusoid,
9696
+ grid,
9697
+ label,
9698
+ plotParametric,
9699
+ plot
9457
9700
  };
9458
- const graphie = new Graphie$1();
9459
- _.extend(graphie, {
9460
- raphael: raphael$1,
9461
- init: function (options) {
9462
- let scale = options.scale || [40, 40];
9463
- scale = typeof scale === "number" ? [scale, scale] : scale;
9464
- xScale = scale[0];
9465
- yScale = scale[1];
9466
- if (options.range == null) {
9467
- throw new PerseusError("range should be specified in graph init", Errors.Internal);
9468
- }
9469
- xRange = options.range[0];
9470
- yRange = options.range[1];
9471
- const w = (xRange[1] - xRange[0]) * xScale;
9472
- const h = (yRange[1] - yRange[0]) * yScale;
9473
- raphael$1.setSize(w, h);
9474
- $(el).css({
9475
- width: w,
9476
- height: h
9477
- });
9478
- this.range = options.range;
9479
- this.scale = scale;
9480
- this.dimensions = [w, h];
9481
- this.xpixels = w;
9482
- this.ypixels = h;
9483
- this.isMobile = options.isMobile;
9484
- return this;
9485
- },
9486
- style: function (attrs, fn) {
9487
- const processed = processAttributes(attrs);
9488
- if (typeof fn === "function") {
9489
- const oldStyle = currentStyle;
9490
- currentStyle = $.extend({}, currentStyle, processed);
9491
- const result = fn.call(graphie);
9492
- currentStyle = oldStyle;
9493
- return result;
9494
- }
9495
- $.extend(currentStyle, processed);
9496
- },
9497
- scalePoint: scalePoint,
9498
- scaleVector: scaleVector,
9499
- unscalePoint: unscalePoint,
9500
- unscaleVector: unscaleVector,
9501
- // Custom SVG path functions that are dependent on graphie range
9502
- // `svgPath`, while independent of range, is exported for consistency
9503
- svgPath: svgPath,
9504
- svgParabolaPath: svgParabolaPath,
9505
- svgSinusoidPath: svgSinusoidPath
9506
- });
9507
- $.each(drawingTools, function (name) {
9508
- graphie[name] = function (...args) {
9701
+ function graphify(drawingFn) {
9702
+ return function (...args) {
9509
9703
  const last = args[args.length - 1];
9510
- const oldStyle = currentStyle;
9704
+ const oldStyle = thisGraphie.currentStyle;
9511
9705
  let result;
9512
9706
 
9513
9707
  // The last argument is probably trying to change the style
9514
9708
  if (typeof last === "object" && !_.isArray(last)) {
9515
- currentStyle = _extends({}, currentStyle, processAttributes(last));
9709
+ thisGraphie.currentStyle = _extends({}, thisGraphie.currentStyle, thisGraphie.processAttributes(last));
9516
9710
  const rest = [].slice.call(args, 0, args.length - 1);
9517
- result = drawingTools[name](...rest);
9711
+ result = drawingFn(...rest);
9518
9712
  } else {
9519
- currentStyle = $.extend({}, currentStyle);
9520
- result = drawingTools[name](...args);
9713
+ thisGraphie.currentStyle = $.extend({}, thisGraphie.currentStyle);
9714
+ result = drawingFn(...args);
9521
9715
  }
9522
9716
 
9523
9717
  // Bad heuristic for recognizing Raphael elements and sets
9524
9718
  const type = result.constructor.prototype;
9525
9719
  if (type === raphael.exports.el || type === raphael.exports.st) {
9526
- result.attr(currentStyle);
9527
- if (currentStyle.arrows) {
9720
+ result.attr(thisGraphie.currentStyle);
9721
+ if (thisGraphie.currentStyle.arrows) {
9528
9722
  result = addArrowheads(result);
9529
9723
  }
9530
9724
  } else if (result instanceof $) {
9531
9725
  // We assume that if it's not a Raphael element/set, it
9532
9726
  // does not contain SVG.
9533
9727
  // @ts-expect-error - TS2339 - Property 'css' does not exist on type '{}'.
9534
- result.css(_extends({}, currentStyle, SVG_SPECIFIC_STYLE_MASK));
9728
+ result.css(_extends({}, thisGraphie.currentStyle, SVG_SPECIFIC_STYLE_MASK));
9535
9729
  }
9536
- currentStyle = oldStyle;
9730
+ thisGraphie.currentStyle = oldStyle;
9537
9731
  return result;
9538
9732
  };
9733
+ }
9734
+ $.each(drawingTools, function (name) {
9735
+ thisGraphie[name] = graphify(drawingTools[name]);
9539
9736
  });
9540
-
9541
- // Initializes graphie settings for a graph and draws the basic graph
9542
- // features (axes, grid, tick marks, and axis labels)
9543
- // Options expected are:
9544
- // - range: [[a, b], [c, d]] or [a, b]
9545
- // - scale: [a, b] or number
9546
- // - gridOpacity: number (0 - 1)
9547
- // - gridStep: [a, b] or number (relative to units)
9548
- // - tickStep: [a, b] or number (relative to grid steps)
9549
- // - tickLen: [a, b] or number (in pixels)
9550
- // - labelStep: [a, b] or number (relative to tick steps)
9551
- // - yLabelFormat: fn to format label string for y-axis
9552
- // - xLabelFormat: fn to format label string for x-axis
9553
- // - smartLabelPositioning: true or false to ignore minus sign
9554
- graphie.graphInit = function (options) {
9555
- options = options || {};
9556
- $.each(options, function (prop, val) {
9557
- // allow options to be specified by a single number for shorthand if
9558
- // the horizontal and vertical components are the same
9559
- if (
9560
- // @ts-expect-error - TS2339 - Property 'match' does not exist on type 'string | number | symbol'.
9561
- !prop.match(/.*Opacity$/) && prop !== "range" && typeof val === "number") {
9562
- options[prop] = [val, val];
9563
- }
9564
-
9565
- // allow symmetric ranges to be specified by the absolute values
9566
- if (prop === "range" || prop === "gridRange") {
9567
- if (val.constructor === Array) {
9568
- // but don't mandate symmetric ranges
9569
- if (val[0].constructor !== Array) {
9570
- options[prop] = [[-val[0], val[0]], [-val[1], val[1]]];
9571
- }
9572
- } else if (typeof val === "number") {
9573
- options[prop] = [[-val, val], [-val, val]];
9574
- }
9575
- }
9576
- });
9577
- const range = options.range || [[-10, 10], [-10, 10]];
9578
- const gridRange = options.gridRange || options.range;
9579
- const scale = options.scale || [20, 20];
9580
- const grid = options.grid != null ? options.grid : true;
9581
- const gridOpacity = options.gridOpacity || 0.1;
9582
- const gridStep = options.gridStep || [1, 1];
9583
- const axes = options.axes != null ? options.axes : true;
9584
- const axisArrows = options.axisArrows || "";
9585
- const axisOpacity = options.axisOpacity || 1.0;
9586
- const axisCenter = options.axisCenter || [Math.min(Math.max(range[0][0], 0), range[0][1]), Math.min(Math.max(range[1][0], 0), range[1][1])];
9587
- const axisLabels = options.axisLabels != null ? options.axisLabels : false;
9588
- const ticks = options.ticks != null ? options.ticks : true;
9589
- const tickStep = options.tickStep || [2, 2];
9590
- const tickLen = options.tickLen || [5, 5];
9591
- const tickOpacity = options.tickOpacity || 1.0;
9592
- const labels = options.labels || options.labelStep || false;
9593
- const labelStep = options.labelStep || [1, 1];
9594
- const labelOpacity = options.labelOpacity || 1.0;
9595
- let unityLabels = options.unityLabels || false;
9596
- const labelFormat = options.labelFormat || function (a) {
9597
- return a;
9598
- };
9599
- let xLabelFormat = options.xLabelFormat || labelFormat;
9600
- let yLabelFormat = options.yLabelFormat || labelFormat;
9601
- const smartLabelPositioning = options.smartLabelPositioning != null ? options.smartLabelPositioning : true;
9602
- const realRange = [[range[0][0] - (range[0][0] > 0 ? 1 : 0), range[0][1] + (range[0][1] < 0 ? 1 : 0)], [range[1][0] - (range[1][0] > 0 ? 1 : 0), range[1][1] + (range[1][1] < 0 ? 1 : 0)]];
9603
- if (!_.isArray(unityLabels)) {
9604
- unityLabels = [unityLabels, unityLabels];
9605
- }
9606
- if (smartLabelPositioning) {
9607
- const minusIgnorer = function minusIgnorer(lf) {
9608
- return function (a) {
9609
- return (lf(a) + "").replace(/-(\d)/g, "\\llap{-}$1");
9610
- };
9611
- };
9612
- xLabelFormat = minusIgnorer(xLabelFormat);
9613
- yLabelFormat = minusIgnorer(yLabelFormat);
9614
- }
9615
- this.init({
9616
- range: realRange,
9617
- scale: scale,
9618
- isMobile: options.isMobile
9619
- });
9620
-
9621
- // draw grid
9622
- if (grid) {
9623
- this.grid(gridRange[0], gridRange[1], {
9624
- stroke: options.isMobile ? KhanColors.GRAY_C : "#000000",
9625
- opacity: options.isMobile ? 1 : gridOpacity,
9626
- step: gridStep,
9627
- strokeWidth: options.isMobile ? 1 : 2
9628
- });
9629
- }
9630
-
9631
- // draw axes
9632
- if (axes) {
9633
- // this is a slight hack until <-> arrowheads work
9634
- if (axisArrows === "<->" || axisArrows === true) {
9635
- this.style({
9636
- stroke: options.isMobile ? KhanColors.GRAY_G : "#000000",
9637
- opacity: options.isMobile ? 1 : axisOpacity,
9638
- strokeWidth: options.isMobile ? 1 : 2,
9639
- arrows: "->"
9640
- }, function () {
9641
- if (range[1][0] < 0 && range[1][1] > 0) {
9642
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
9643
- this.path([axisCenter, [gridRange[0][0], axisCenter[1]]]);
9644
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
9645
- this.path([axisCenter, [gridRange[0][1], axisCenter[1]]]);
9646
- }
9647
- if (range[0][0] < 0 && range[0][1] > 0) {
9648
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
9649
- this.path([axisCenter, [axisCenter[0], gridRange[1][0]]]);
9650
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
9651
- this.path([axisCenter, [axisCenter[0], gridRange[1][1]]]);
9652
- }
9653
- });
9654
-
9655
- // also, we don't support "<-" arrows yet, but why you
9656
- // would want that on your graph is beyond me.
9657
- } else if (axisArrows === "->" || axisArrows === "") {
9658
- this.style({
9659
- stroke: "#000000",
9660
- opacity: axisOpacity,
9661
- strokeWidth: 2,
9662
- arrows: axisArrows
9663
- }, function () {
9664
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
9665
- this.path([[gridRange[0][0], axisCenter[1]], [gridRange[0][1], axisCenter[1]]]);
9666
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
9667
- this.path([[axisCenter[0], gridRange[1][0]], [axisCenter[0], gridRange[1][1]]]);
9668
- });
9669
- }
9670
- if (axisLabels && axisLabels.length === 2) {
9671
- this.label([gridRange[0][1], axisCenter[1]], axisLabels[0], "right");
9672
- this.label([axisCenter[0], gridRange[1][1]], axisLabels[1], "above");
9673
- }
9674
- }
9675
-
9676
- // draw tick marks
9677
- if (ticks) {
9678
- const halfWidthTicks = options.isMobile;
9679
- this.style({
9680
- stroke: options.isMobile ? KhanColors.GRAY_G : "#000000",
9681
- opacity: options.isMobile ? 1 : tickOpacity,
9682
- strokeWidth: 1
9683
- }, function () {
9684
- // horizontal axis
9685
- let step = gridStep[0] * tickStep[0];
9686
- let len = tickLen[0] / scale[1];
9687
- let start = gridRange[0][0];
9688
- let stop = gridRange[0][1];
9689
- if (range[1][0] < 0 && range[1][1] > 0) {
9690
- for (let x = step + axisCenter[0]; x <= stop; x += step) {
9691
- if (x < stop || !axisArrows) {
9692
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
9693
- this.line([x, -len + axisCenter[1]], [x, halfWidthTicks ? 0 : len + axisCenter[1]]);
9694
- }
9695
- }
9696
- for (let x = -step + axisCenter[0]; x >= start; x -= step) {
9697
- if (x > start || !axisArrows) {
9698
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
9699
- this.line([x, -len + axisCenter[1]], [x, halfWidthTicks ? 0 : len + axisCenter[1]]);
9700
- }
9701
- }
9702
- }
9703
-
9704
- // vertical axis
9705
- step = gridStep[1] * tickStep[1];
9706
- len = tickLen[1] / scale[0];
9707
- start = gridRange[1][0];
9708
- stop = gridRange[1][1];
9709
- if (range[0][0] < 0 && range[0][1] > 0) {
9710
- for (let y = step + axisCenter[1]; y <= stop; y += step) {
9711
- if (y < stop || !axisArrows) {
9712
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
9713
- this.line([-len + axisCenter[0], y], [halfWidthTicks ? 0 : len + axisCenter[0], y]);
9714
- }
9715
- }
9716
- for (let y = -step + axisCenter[1]; y >= start; y -= step) {
9717
- if (y > start || !axisArrows) {
9718
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
9719
- this.line([-len + axisCenter[0], y], [halfWidthTicks ? 0 : len + axisCenter[0], y]);
9720
- }
9721
- }
9722
- }
9723
- });
9724
- }
9725
-
9726
- // draw axis labels
9727
- if (labels) {
9728
- this.style({
9729
- stroke: options.isMobile ? KhanColors.GRAY_G : "#000000",
9730
- opacity: options.isMobile ? 1 : labelOpacity
9731
- }, function () {
9732
- // horizontal axis
9733
- let step = gridStep[0] * tickStep[0] * labelStep[0];
9734
- let start = gridRange[0][0];
9735
- let stop = gridRange[0][1];
9736
- const xAxisPosition = axisCenter[0] < 0 ? "above" : "below";
9737
- const yAxisPosition = axisCenter[0] < 0 ? "right" : "left";
9738
- const xShowZero = axisCenter[0] === 0 && axisCenter[1] !== 0;
9739
- const yShowZero = axisCenter[0] !== 0 && axisCenter[1] === 0;
9740
- const axisOffCenter = axisCenter[0] !== 0 || axisCenter[1] !== 0;
9741
- const showUnityX = unityLabels[0] || axisOffCenter;
9742
- const showUnityY = unityLabels[1] || axisOffCenter;
9743
-
9744
- // positive x-axis
9745
- for (let x = (xShowZero ? 0 : step) + axisCenter[0]; x <= stop; x += step) {
9746
- if (x < stop || !axisArrows) {
9747
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
9748
- this.label([x, axisCenter[1]], xLabelFormat(x), xAxisPosition);
9749
- }
9750
- }
9751
-
9752
- // negative x-axis
9753
- for (let x = -step * (showUnityX ? 1 : 2) + axisCenter[0]; x >= start; x -= step) {
9754
- if (x > start || !axisArrows) {
9755
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
9756
- this.label([x, axisCenter[1]], xLabelFormat(x), xAxisPosition);
9757
- }
9758
- }
9759
- step = gridStep[1] * tickStep[1] * labelStep[1];
9760
- start = gridRange[1][0];
9761
- stop = gridRange[1][1];
9762
-
9763
- // positive y-axis
9764
- for (let y = (yShowZero ? 0 : step) + axisCenter[1]; y <= stop; y += step) {
9765
- if (y < stop || !axisArrows) {
9766
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
9767
- this.label([axisCenter[0], y], yLabelFormat(y), yAxisPosition);
9768
- }
9769
- }
9770
-
9771
- // negative y-axis
9772
- for (let y = -step * (showUnityY ? 1 : 2) + axisCenter[1]; y >= start; y -= step) {
9773
- if (y > start || !axisArrows) {
9774
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
9775
- this.label([axisCenter[0], y], yLabelFormat(y), yAxisPosition);
9776
- }
9777
- }
9778
- });
9779
- }
9780
- };
9781
- return graphie;
9737
+ return thisGraphie;
9782
9738
  };
9783
9739
 
9784
9740
  /* eslint-disable */
@@ -13368,7 +13324,7 @@ _.extend(MovableAngle.prototype, {
13368
13324
  let angle = GraphUtils.findAngle(newPoint, vertex);
13369
13325
  angle = Math.round((angle - snapOffset) / snap) * snap + snapOffset;
13370
13326
  const distance = GraphUtils.getDistance(newPoint, vertex);
13371
- return addPoints(vertex, graphie.polar(distance, angle));
13327
+ return addPoints(vertex, polar(distance, angle));
13372
13328
  }
13373
13329
  return true;
13374
13330
  };
@@ -18436,6 +18392,27 @@ const registerEditors = editorsToRegister => {
18436
18392
  editors[editor.widgetName] = editor;
18437
18393
  });
18438
18394
  };
18395
+
18396
+ /**
18397
+ *
18398
+ * @param name - the widget that you are trying to replace
18399
+ * @param replacementName - the name of the widget that takes its place
18400
+ *
18401
+ * e.g. replaceEditor("transformer", "always-correct") will make it so the
18402
+ * transformer widget is replaced by the always correct widget
18403
+ */
18404
+ const replaceEditor = (name, replacementName) => {
18405
+ const substituteEditor = editors[replacementName];
18406
+ if (!substituteEditor && Log) {
18407
+ const errorMsg = `Failed to replace editor ${name} with ${replacementName}`;
18408
+ Log.error(errorMsg, Errors.Internal);
18409
+ return;
18410
+ }
18411
+ editors[name] = substituteEditor;
18412
+ if (Log) {
18413
+ Log.log(`INFO: Replacing editor ${name} with ${replacementName}`);
18414
+ }
18415
+ };
18439
18416
  const getWidget = name => {
18440
18417
  // TODO(alex): Consider referring to these as renderers to avoid
18441
18418
  // overloading "widget"
@@ -18464,7 +18441,7 @@ const getVersion = name => {
18464
18441
  minor: 0
18465
18442
  };
18466
18443
  }
18467
- return null;
18444
+ return;
18468
18445
  };
18469
18446
  const getVersionVector = () => {
18470
18447
  const version = {};
@@ -18745,6 +18722,7 @@ var widgets$1 = /*#__PURE__*/Object.freeze({
18745
18722
  registerWidgets: registerWidgets,
18746
18723
  replaceWidget: replaceWidget,
18747
18724
  registerEditors: registerEditors,
18725
+ replaceEditor: replaceEditor,
18748
18726
  getWidget: getWidget,
18749
18727
  getEditor: getEditor,
18750
18728
  getTransform: getTransform,
@@ -24370,14 +24348,14 @@ var Definition$1 = {
24370
24348
 
24371
24349
  // The props are type `any` on purpose so that this can receive props
24372
24350
  // from any deprecated widget
24373
- class AutoCorrect extends React__default.Component {
24351
+ class DeprecatedStandin extends React__default.Component {
24374
24352
  constructor(...args) {
24375
24353
  super(...args);
24376
24354
  this.getUserInput = () => {
24377
24355
  return {};
24378
24356
  };
24379
24357
  this.simpleValidate = rubric => {
24380
- return AutoCorrect.validate(this.getUserInput(), rubric);
24358
+ return DeprecatedStandin.validate(this.getUserInput(), rubric);
24381
24359
  };
24382
24360
  }
24383
24361
  static validate(userInput, rubric) {
@@ -24397,16 +24375,17 @@ class AutoCorrect extends React__default.Component {
24397
24375
  paddingBottom: 8
24398
24376
  }
24399
24377
  }, /*#__PURE__*/React__default.createElement(Banner, {
24400
- text: i18n._("This part of the question is no-longer available. You will not be graded on this part."),
24401
- kind: "warning",
24378
+ text: i18n._("Sorry, this part of the question is no longer available. 😅 Don't worry, you won't be graded on this part. Keep going!"),
24379
+ kind: "info",
24402
24380
  layout: "full-width"
24403
24381
  }));
24404
24382
  }
24405
24383
  }
24406
- var DeprecatedStandin = {
24384
+ var DeprecatedStandin$1 = {
24407
24385
  name: "deprecated-standin",
24408
24386
  displayName: "Deprecated Standin",
24409
- widget: AutoCorrect
24387
+ widget: DeprecatedStandin,
24388
+ hidden: true
24410
24389
  };
24411
24390
 
24412
24391
  class Dropdown extends React.Component {
@@ -28895,7 +28874,7 @@ class InteractiveGraph extends React.Component {
28895
28874
  if (
28896
28875
  // @ts-expect-error - TS2339 - Property 'snapTo' does not exist on type 'PerseusGraphType'.
28897
28876
  this.props.graph.snapTo === "angles" && this.points.length > 2) {
28898
- var _this$graphie5, _this$graphie6;
28877
+ var _this$graphie5;
28899
28878
  // Snap to whole degree interior angles
28900
28879
 
28901
28880
  const angles = _.map(angleMeasures(coords), function (rad) {
@@ -28925,12 +28904,12 @@ class InteractiveGraph extends React.Component {
28925
28904
  // Solve for side by using the law of sines
28926
28905
  const side = Math.sin(innerAngles[1] * Math.PI / 180) / Math.sin(innerAngles[2] * Math.PI / 180) * knownSide;
28927
28906
  const outerAngle = GraphUtils.findAngle(coords[rel(1)], coords[rel(-1)]);
28928
- const offset = (_this$graphie5 = this.graphie) == null ? void 0 : _this$graphie5.polar(side, outerAngle + (onLeft ? 1 : -1) * innerAngles[0]);
28929
- return (_this$graphie6 = this.graphie) == null ? void 0 : _this$graphie6.addPoints(coords[rel(-1)], offset);
28907
+ const offset = polar(side, outerAngle + (onLeft ? 1 : -1) * innerAngles[0]);
28908
+ return (_this$graphie5 = this.graphie) == null ? void 0 : _this$graphie5.addPoints(coords[rel(-1)], offset);
28930
28909
  }
28931
28910
  // @ts-expect-error - TS2339 - Property 'snapTo' does not exist on type 'PerseusGraphType'.
28932
28911
  if (this.props.graph.snapTo === "sides" && this.points.length > 1) {
28933
- var _this$graphie7, _this$graphie8;
28912
+ var _this$graphie6;
28934
28913
  // Snap to whole unit side measures
28935
28914
 
28936
28915
  const sides = _.map([[coords[rel(-1)], coords[i]], [coords[i], coords[rel(1)]], [coords[rel(-1)], coords[rel(1)]]], function (coords) {
@@ -28950,8 +28929,8 @@ class InteractiveGraph extends React.Component {
28950
28929
  const innerAngle = lawOfCosines(sides[0], sides[2], sides[1]);
28951
28930
  const outerAngle = GraphUtils.findAngle(coords[rel(1)], coords[rel(-1)]);
28952
28931
  const onLeft = sign(ccw(coords[rel(-1)], coords[rel(1)], coords[i])) === 1;
28953
- const offset = (_this$graphie7 = this.graphie) == null ? void 0 : _this$graphie7.polar(sides[0], outerAngle + (onLeft ? 1 : -1) * innerAngle);
28954
- return (_this$graphie8 = this.graphie) == null ? void 0 : _this$graphie8.addPoints(coords[rel(-1)], offset);
28932
+ const offset = polar(sides[0], outerAngle + (onLeft ? 1 : -1) * innerAngle);
28933
+ return (_this$graphie6 = this.graphie) == null ? void 0 : _this$graphie6.addPoints(coords[rel(-1)], offset);
28955
28934
  }
28956
28935
  // Snap to grid (already done)
28957
28936
  return true;
@@ -31029,7 +31008,7 @@ const AnswerPill = props => {
31029
31008
  id: idFactory.get("perseus-label-image-widget-answer-pill"),
31030
31009
  onClick: correct ? undefined : onClick,
31031
31010
  ref: ref,
31032
- style: [style, popperStyle, styles$h.relative, correct && styles$h.correct, incorrect && styles$h.incorrect, (focused || hovered) && BringToFront]
31011
+ style: [style, popperStyle, styles$h.pill, correct && styles$h.correct, incorrect && styles$h.incorrect, (focused || hovered) && BringToFront]
31033
31012
  }, /*#__PURE__*/React.createElement(Renderer, {
31034
31013
  content: answerString,
31035
31014
  inline: true
@@ -31042,6 +31021,11 @@ const styles$h = StyleSheet.create({
31042
31021
  },
31043
31022
  incorrect: {
31044
31023
  backgroundColor: Color.offBlack64
31024
+ },
31025
+ pill: {
31026
+ // Reset the Pill's default height in order to account
31027
+ // for multi-line pills.
31028
+ height: "auto"
31045
31029
  }
31046
31030
  });
31047
31031
 
@@ -43958,7 +43942,7 @@ var Video$1 = {
43958
43942
  };
43959
43943
 
43960
43944
  /* eslint-disable import/no-named-as-default */
43961
- var extraWidgets = [Categorizer$1, CSProgram$1, Dropdown$1, Explanation$1, Definition$1, Grapher$1, GradedGroup$1, GradedGroupSet$1, Group$1, Iframe$1, Image$1, Interactive, InteractiveGraph$1, LabelImage$1, LightsPuzzle$1, Matrix$1, Matcher$1, Measurer$1, Molecule$1, NumberLine$1, Orderer$1, Passage$1, PassageRef$1, PassageRefTarget$1, Plotter$1, ReactionDiagram, Sequence$1, Simulator$1, Sorter$1, Table$1, Transformer$1, Unit, Video$1, DeprecatedStandin];
43945
+ var extraWidgets = [Categorizer$1, CSProgram$1, Dropdown$1, Explanation$1, Definition$1, Grapher$1, GradedGroup$1, GradedGroupSet$1, Group$1, Iframe$1, Image$1, Interactive, InteractiveGraph$1, LabelImage$1, LightsPuzzle$1, Matrix$1, Matcher$1, Measurer$1, Molecule$1, NumberLine$1, Orderer$1, Passage$1, PassageRef$1, PassageRefTarget$1, Plotter$1, ReactionDiagram, Sequence$1, Simulator$1, Sorter$1, Table$1, Transformer$1, Unit, Video$1, DeprecatedStandin$1];
43962
43946
 
43963
43947
  /**
43964
43948
  * This should be called by all clients, specifying whether extra widgets are
@@ -43976,7 +43960,7 @@ const init = function init(options) {
43976
43960
  // This section replaces widgets
43977
43961
  // When it's time to replace the transformer widget
43978
43962
  // uncomment this line
43979
- // Widgets.replaceWidget("transformer", "always-correct");
43963
+ // Widgets.replaceWidget("transformer", "deprecated-standin");
43980
43964
 
43981
43965
  // Pass skipMathJax: true if MathJax is already loaded and configured.
43982
43966
  const skipMathJax = options.skipMathJax;
@@ -44002,7 +43986,7 @@ const init = function init(options) {
44002
43986
 
44003
43987
  // This file is processed by a Rollup plugin (replace) to inject the production
44004
43988
  const libName = "@khanacademy/perseus";
44005
- const libVersion = "17.8.0";
43989
+ const libVersion = "18.0.1";
44006
43990
  addLibraryVersionToPerseusDebug(libName, libVersion);
44007
43991
 
44008
43992
  const apiVersion = {
@@ -46588,6 +46572,423 @@ const registerAllWidgetsForTesting = () => {
46588
46572
  // Widgets.replaceWidget("transformer", "deprecated-standin");
46589
46573
  };
46590
46574
 
46575
+ /**
46576
+ * This function extracts the answers from the widgets.
46577
+ *
46578
+ * For each widget type, we first check to make sure the path to the answer exists,
46579
+ * then we extract the answer and add it to the list of answers.
46580
+ *
46581
+ * A list is returned because some widgets, like multi-select radio widgets and groups
46582
+ * can have multiple answers.
46583
+ *
46584
+ * @param {PerseusRenderer["widgets"]} widgets
46585
+ * @returns a list of strings that are the answers to the widgets
46586
+ */
46587
+ function getAnswersFromWidgets(widgets) {
46588
+ const answers = [];
46589
+ keys(widgets).forEach(widgetID => {
46590
+ var _options$choices, _categorizer$options, _categorizer$options2, _categorizer$options3, _dropdown$options, _numericInput$options, _inputNumber$options, _expression$options, _gradedGroup$options, _plotter$options, _plotter$options2, _grapher$options, _orderer$options, _sorter$options, _labelImage$options, _numberLine$options, _matrix$options, _matcher$options, _matcher$options2;
46591
+ const widget = widgets[widgetID];
46592
+ if (!widget.options) {
46593
+ return;
46594
+ }
46595
+ switch (widget.type) {
46596
+ case "radio":
46597
+ const radio = widget;
46598
+ const options = radio.options;
46599
+ // Answer is the content of the correct choice
46600
+ if (options != null && (_options$choices = options.choices) != null && _options$choices.length) {
46601
+ for (const choice of options.choices) {
46602
+ if (choice != null && choice.correct) {
46603
+ answers.push(choice.content);
46604
+ }
46605
+ }
46606
+ }
46607
+ break;
46608
+ case "categorizer":
46609
+ // Answer is a list of items mapped to their category
46610
+ // e.g. ['item 1: category 1', 'item 2: category 2']
46611
+ const categorizer = widget;
46612
+ if ((_categorizer$options = categorizer.options) != null && _categorizer$options.categories && (_categorizer$options2 = categorizer.options) != null && _categorizer$options2.items && (_categorizer$options3 = categorizer.options) != null && _categorizer$options3.values) {
46613
+ var _categorizer$options4, _categorizer$options5, _categorizer$options6;
46614
+ const categories = (_categorizer$options4 = categorizer.options) == null ? void 0 : _categorizer$options4.categories;
46615
+ const items = (_categorizer$options5 = categorizer.options) == null ? void 0 : _categorizer$options5.items;
46616
+ const values = (_categorizer$options6 = categorizer.options) == null ? void 0 : _categorizer$options6.values;
46617
+ answers.push(...values.map((value, index) => `${items[index]}: ${categories[value]}`));
46618
+ }
46619
+ break;
46620
+ case "dropdown":
46621
+ // Answer is the content of correct dropdown option
46622
+ const dropdown = widget;
46623
+ if ((_dropdown$options = dropdown.options) != null && _dropdown$options.choices) {
46624
+ for (const choice of dropdown.options.choices) {
46625
+ if (choice.correct) {
46626
+ answers.push(choice.content);
46627
+ }
46628
+ }
46629
+ }
46630
+ break;
46631
+ case "numeric-input":
46632
+ // Answer is the numeric value cast to a string
46633
+ const numericInput = widget;
46634
+ if ((_numericInput$options = numericInput.options) != null && _numericInput$options.answers) {
46635
+ for (const ans of numericInput.options.answers) {
46636
+ if (ans.status === "correct" && ans.value != null) {
46637
+ answers.push(ans.value.toString());
46638
+ }
46639
+ }
46640
+ }
46641
+ break;
46642
+ case "input-number":
46643
+ // Answer is the correct value
46644
+ const inputNumber = widget;
46645
+ if ((_inputNumber$options = inputNumber.options) != null && _inputNumber$options.value) {
46646
+ answers.push(inputNumber.options.value.toString());
46647
+ }
46648
+ break;
46649
+ case "expression":
46650
+ // Answer is a list of potential correct expressions,
46651
+ // since there can be multiple correct forms of the expression
46652
+ const expression = widget;
46653
+ if ((_expression$options = expression.options) != null && _expression$options.answerForms) {
46654
+ answers.push(...expression.options.answerForms.map(answer => answer.value));
46655
+ }
46656
+ break;
46657
+ case "group":
46658
+ case "graded-group":
46659
+ // Groups can contain multiple widgets, so we recursively
46660
+ // call this function to get the nested answers from the
46661
+ // group's widgets
46662
+ const gradedGroup = widget;
46663
+ if ((_gradedGroup$options = gradedGroup.options) != null && _gradedGroup$options.widgets) {
46664
+ answers.push(...getAnswersFromWidgets(gradedGroup.options.widgets));
46665
+ }
46666
+ break;
46667
+ case "plotter":
46668
+ // Answer is a map of the correct value for each category
46669
+ // e.g. '{Soccer: 35, Basketball: 20, Golf: 45, Volleyball: 15,
46670
+ // Tennis: 30}'
46671
+ const plotter = widget;
46672
+ if ((_plotter$options = plotter.options) != null && _plotter$options.categories && (_plotter$options2 = plotter.options) != null && _plotter$options2.correct && plotter.options.categories.length === plotter.options.correct.length) {
46673
+ const {
46674
+ categories,
46675
+ correct
46676
+ } = plotter.options;
46677
+ answers.push(`{${categories.map((category, index) => `${category}: ${correct[index]}`).join(", ")}}`);
46678
+ }
46679
+ break;
46680
+ case "interactive-graph":
46681
+ case "grapher":
46682
+ // Answer is a list of points that should be on the graph
46683
+ // E.g. 'There should be point(s) on [(-1, 1), (1, 1)]'
46684
+ const grapher = widget;
46685
+ // @ts-expect-error - TS2339 - Property 'coords' does not exist on type '{ type: "absolute_value"; coords: [Coord, Coord]; } | { type: "exponential"; asymptote: [Coord, Coord]; coords: [Coord, Coord]; } | { type: "linear"; coords: [...]; } | ... 4 more ... | PerseusGraphType'.
46686
+ if ((_grapher$options = grapher.options) != null && (_grapher$options = _grapher$options.correct) != null && _grapher$options.coords) {
46687
+ var _grapher$options$corr;
46688
+ answers.push(
46689
+ // @ts-expect-error - TS2339 - Property 'coords' does not exist on type '{ type: "absolute_value"; coords: [Coord, Coord]; } | { type: "exponential"; asymptote: [Coord, Coord]; coords: [Coord, Coord]; } | { type: "linear"; coords: [...]; } | ... 4 more ... | PerseusGraphType'.
46690
+ `There should be point(s) on [${(_grapher$options$corr = grapher.options.correct) == null ? void 0 : _grapher$options$corr.coords.join("], [")}]`);
46691
+ }
46692
+ break;
46693
+ case "orderer":
46694
+ // Answer is the content of the each option
46695
+ // in the correct order on newlines
46696
+ // E.g. 'item 1\nitem 2\nitem 3'
46697
+ const orderer = widget;
46698
+ if ((_orderer$options = orderer.options) != null && _orderer$options.correctOptions) {
46699
+ answers.push(orderer.options.correctOptions.map(option => option.content).join("\n"));
46700
+ }
46701
+ break;
46702
+ case "sorter":
46703
+ // Answer is the content of each option in the correct order
46704
+ // separated by commas
46705
+ // E.g. 'item 1, item 2, item 3'
46706
+ const sorter = widget;
46707
+ if ((_sorter$options = sorter.options) != null && _sorter$options.correct) {
46708
+ answers.push(sorter.options.correct.join(", "));
46709
+ }
46710
+ break;
46711
+ case "label-image":
46712
+ // Answer is a list of labels, their positions, and their answer
46713
+ // E.g. '{label: "label 1", position: {x: 0.5, y: 0.5}, answer: "answer 1"}'
46714
+ const labelImage = widget;
46715
+ if ((_labelImage$options = labelImage.options) != null && _labelImage$options.markers) {
46716
+ answers.push(...labelImage.options.markers.map(m => `{label: "${m.label}", position: {${m.x},${m.y}}, answer: "${m.answers.join(", ")}"}`));
46717
+ }
46718
+ break;
46719
+ case "number-line":
46720
+ // Answer is the correct x position on the number line
46721
+ // E.g. '0.5'
46722
+ const numberLine = widget;
46723
+ if (((_numberLine$options = numberLine.options) == null ? void 0 : _numberLine$options.correctX) != null) {
46724
+ answers.push(numberLine.options.correctX.toString());
46725
+ }
46726
+ break;
46727
+ case "matrix":
46728
+ // Answer is a list of rows representing the matrix
46729
+ // E.g. '[[1, 2], [3, 4]]'
46730
+ const matrix = widget;
46731
+ if ((_matrix$options = matrix.options) != null && _matrix$options.answers) {
46732
+ answers.push(`[${matrix.options.answers.join("], [")}]`);
46733
+ }
46734
+ break;
46735
+ case "matcher":
46736
+ // Answer is a table of the left and right items
46737
+ // E.g. '| Left | Right |\n| --- | --- |\n| left item 1 | right item 1 |\n| left item 2 | right item 2 |'
46738
+ const matcher = widget;
46739
+ if ((_matcher$options = matcher.options) != null && _matcher$options.left && (_matcher$options2 = matcher.options) != null && _matcher$options2.right) {
46740
+ const {
46741
+ left,
46742
+ right
46743
+ } = matcher.options;
46744
+ const [leftHeader, rightHeader] = matcher.options.labels;
46745
+ const tableHeader = `| ${leftHeader} | ${rightHeader} |\n| --- | --- |`;
46746
+ const tableRows = left.map((leftItem, index) => {
46747
+ return `| ${leftItem} | ${right[index]} |`;
46748
+ });
46749
+ const table = [tableHeader, ...tableRows].join("\n");
46750
+ answers.push(table);
46751
+ }
46752
+ break;
46753
+ }
46754
+ });
46755
+ return answers;
46756
+ }
46757
+
46758
+ /**
46759
+ * Inject a string equivalent of the widgets into the content.
46760
+ *
46761
+ * Content may contain Perseus widgets, that looks like this: '[[☃ Radio 1]]'.
46762
+ * This function replaces those widgets with its equivalent string.
46763
+ *
46764
+ * @param {string} content
46765
+ * @param {PerseusRenderer["widgets"]} widgets
46766
+ * @returns
46767
+ */
46768
+ function injectWidgets(content, widgets) {
46769
+ // The types for taskProgress.itemData are not well defined,
46770
+ // so there is a chance that widgets or content could be undefined.
46771
+ // In that scenario, simply exit early and return the content as is.
46772
+ if (!content) {
46773
+ return "";
46774
+ }
46775
+ if (!widgets) {
46776
+ return content;
46777
+ }
46778
+ let context = content;
46779
+ keys(widgets).forEach(widgetID => {
46780
+ var _radio$options, _image$options, _labelImage$options2, _explanation$options, _passage$options, _passage$options2, _group$options, _gradedGroup$options2, _categorizer$options7, _dropdown$options2, _definition$options, _orderer$options2, _sorter$options2, _interactiveGraph$opt, _numberLine$options2, _numberLine$options3, _numberLine$options4, _matrix$options2, _matcher$options3, _matcher$options4;
46781
+ const widget = widgets[widgetID];
46782
+ if (!widget.options) {
46783
+ return;
46784
+ }
46785
+ switch (widget.type) {
46786
+ case "radio":
46787
+ // Replace radio with the radio options
46788
+ // '[[☃ Radio 1]]' -> 'option 1\noption 2\noption 3'
46789
+ const radio = widget;
46790
+ if ((_radio$options = radio.options) != null && (_radio$options = _radio$options.choices) != null && _radio$options.length) {
46791
+ var _radio$options2;
46792
+ let radioContext = radio.options.choices.map(choice => choice.content).join("\n");
46793
+ if ((_radio$options2 = radio.options) != null && _radio$options2.randomize) {
46794
+ radioContext += "\nThose options are displayed in a different order to the user. If the user says the letter, number, or ordinal number, always ask them clarify which option they are referring to.\n";
46795
+ }
46796
+ context = context.replace(`[[☃ ${widgetID}]]`, radioContext);
46797
+ }
46798
+ break;
46799
+ case "image":
46800
+ // Replace image with the image's alt text
46801
+ // '[[☃ Image 1]]' -> '<img id="Image 1" alt="The text for this image">'
46802
+ const image = widget;
46803
+ if ((_image$options = image.options) != null && _image$options.alt) {
46804
+ context = context.replace(`[[☃ ${widgetID}]]`, `<img id="${widgetID}" alt="${image.options.alt}">`);
46805
+ }
46806
+ break;
46807
+ case "label-image":
46808
+ // Replace label-image with the label-image's alt text
46809
+ // '[[☃ Label Image 1]]' -> '[An image with dots that user needs to label. Label choices: [choice 1, choice 2]. Image alt text: The text for this image]'
46810
+ const labelImage = widget;
46811
+ if ((_labelImage$options2 = labelImage.options) != null && _labelImage$options2.imageAlt) {
46812
+ var _labelImage$options$i, _labelImage$options3;
46813
+ context = context.replace(`[[☃ ${widgetID}]]`, `[An image with dots that user needs to label. Label choices: [${labelImage.options.choices.join(", ")}]. Image alt text: ${(_labelImage$options$i = (_labelImage$options3 = labelImage.options) == null ? void 0 : _labelImage$options3.imageAlt) != null ? _labelImage$options$i : ""}]`);
46814
+ }
46815
+ break;
46816
+ case "explanation":
46817
+ // Replace explaintion with the explaination text
46818
+ // Note: explainations can have widgets inside of them too
46819
+ // '[[☃ Explanation 1]]' -> 'This is the hidden explanation text'
46820
+ const explanation = widget;
46821
+ if ((_explanation$options = explanation.options) != null && _explanation$options.explanation) {
46822
+ context = context.replace(`[[☃ ${widgetID}]]`, injectWidgets(explanation.options.explanation, explanation.options.widgets));
46823
+ }
46824
+ break;
46825
+ case "passage":
46826
+ // Replace passage with the passage text
46827
+ // '[[☃ Passage 1]]' -> '# Passage Title\n\nPassage text'
46828
+ const passage = widget;
46829
+ if ((_passage$options = passage.options) != null && _passage$options.passageTitle || (_passage$options2 = passage.options) != null && _passage$options2.passageText) {
46830
+ const {
46831
+ passageTitle,
46832
+ passageText
46833
+ } = passage.options;
46834
+ context = context.replace(`[[☃ ${widgetID}]]`, `# ${passageTitle}\n\n${passageText}`);
46835
+ }
46836
+ break;
46837
+ case "group":
46838
+ case "graded-group":
46839
+ // Replace group with the group's nested content
46840
+ // '[[☃ Group 1]]' -> 'Nested question\n[[☃ Radio 1]]'
46841
+ // -> 'Nested question\noption 1\noption 2\noption 3'
46842
+ const group = widget;
46843
+ if ((_group$options = group.options) != null && _group$options.widgets && group.options.content) {
46844
+ context = context.replace(`[[☃ ${widgetID}]]`, injectWidgets(group.options.content, group.options.widgets));
46845
+ }
46846
+ break;
46847
+ case "graded-group-set":
46848
+ // Replace graded-group-set with the the nested groups
46849
+ // '[[☃ Graded Group Set 1]]'
46850
+ // -> 'Nested question 1\noption 1\noption 2\noption 3\nNested question 2\noption 1\noption 2\noption 3'
46851
+ const gradedGroup = widget;
46852
+ if ((_gradedGroup$options2 = gradedGroup.options) != null && _gradedGroup$options2.gradedGroups) {
46853
+ const gradedGroups = gradedGroup.options.gradedGroups;
46854
+ const gradedGroupsContent = gradedGroups.reduce((acc, group) => {
46855
+ if (group.widgets && group.content) {
46856
+ acc += injectWidgets(group.content, group.widgets) + "\n";
46857
+ }
46858
+ return acc;
46859
+ }, "");
46860
+ context = context.replace(`[[☃ ${widgetID}]]`, gradedGroupsContent);
46861
+ }
46862
+ break;
46863
+ case "categorizer":
46864
+ // Replace categorizer with the options and categories
46865
+ // '[[☃ Categorizer 1]]' -> '''
46866
+ // For each item, select the correct category. Categories: category 1, category 2, category 3.
46867
+ // Items:
46868
+ // item 1
46869
+ // item 2
46870
+ // '''
46871
+ const categorizer = widget;
46872
+ if ((_categorizer$options7 = categorizer.options) != null && _categorizer$options7.categories && categorizer.options.items) {
46873
+ const categories = categorizer.options.categories;
46874
+ const items = categorizer.options.items;
46875
+ context = context.replace(`[[☃ ${widgetID}]]`, `For each item, select the correct category. Categories: ${categories.join(", ")}.\nItems:\n${items.join("\n")}\n`);
46876
+ }
46877
+ break;
46878
+ case "dropdown":
46879
+ // Replace dropdown with the dropdown options
46880
+ // '[[☃ Dropdown 1]]' -> '[option 1 | option 2 | option 3]'
46881
+ const dropdown = widget;
46882
+ if ((_dropdown$options2 = dropdown.options) != null && _dropdown$options2.choices) {
46883
+ const choices = dropdown.options.choices.map(choice => choice.content);
46884
+ context = context.replace(`[[☃ ${widgetID}]]`, `[${choices.join(" | ")}]`);
46885
+ }
46886
+ break;
46887
+ case "definition":
46888
+ // Replace definition with the word that is being defined
46889
+ // NOTE(chase): this does not include the definition itself,
46890
+ // because I believe the AI can infer that without wasting extra tokens
46891
+ // '[[☃ Definition 1]]' -> 'word'
46892
+ const definition = widget;
46893
+ if ((_definition$options = definition.options) != null && _definition$options.togglePrompt) {
46894
+ context = context.replace(`[[☃ ${widgetID}]]`, definition.options.togglePrompt);
46895
+ }
46896
+ break;
46897
+ case "orderer":
46898
+ // Replace orderer with the orderer options
46899
+ // '[[☃ Orderer 1]]' -> 'option 1\noption 2\noption 3'
46900
+ const orderer = widget;
46901
+ if ((_orderer$options2 = orderer.options) != null && _orderer$options2.options) {
46902
+ context = context.replace(`[[☃ ${widgetID}]]`, orderer.options.options.map(option => option.content).join("\n"));
46903
+ }
46904
+ break;
46905
+ case "sorter":
46906
+ // Replace sorter with the options
46907
+ // NOTE(chase): These are already in the correct order
46908
+ // '[[☃ Sorter 1]]' -> '[option 1 | option 2 | option 3]'
46909
+ const sorter = widget;
46910
+ if ((_sorter$options2 = sorter.options) != null && _sorter$options2.correct) {
46911
+ const choices = sorter.options.correct;
46912
+ context = context.replace(`[[☃ ${widgetID}]]`, `[${choices.join(" | ")}]`);
46913
+ }
46914
+ break;
46915
+ case "interactive-graph":
46916
+ // Replace interactive-graph with the graph's range
46917
+ // '[[☃ Interactive Graph 1]]' -> '[Graph with an x range of -10 to 10 and y range of -10 to 10]'
46918
+ const interactiveGraph = widget;
46919
+ if (((_interactiveGraph$opt = interactiveGraph.options) == null ? void 0 : _interactiveGraph$opt.range.length) === 2) {
46920
+ const [x, y] = interactiveGraph.options.range;
46921
+ context = context.replace(`[[☃ ${widgetID}]]`, `[Graph with an x range of ${x[0]} to ${x[1]} and y range of ${y[0]} to ${y[1]}]`);
46922
+ }
46923
+ break;
46924
+ case "number-line":
46925
+ // Replace number-line with the number line's range, step, and initial position
46926
+ // '[[☃ Number Line 1]]' -> '[Number line with a range of -10 to 10, a step of 1, and an initial position of 0]'
46927
+ const numberLine = widget;
46928
+ if (((_numberLine$options2 = numberLine.options) == null ? void 0 : _numberLine$options2.range.length) === 2 && (_numberLine$options3 = numberLine.options) != null && _numberLine$options3.tickStep && (_numberLine$options4 = numberLine.options) != null && _numberLine$options4.initialX) {
46929
+ const [min, max] = numberLine.options.range;
46930
+ const step = numberLine.options.tickStep;
46931
+ const initialPosition = numberLine.options.initialX;
46932
+ context = context.replace(`[[☃ ${widgetID}]]`, `[Number line with a range of ${min} to ${max}, a step of ${step}, and an initial position of ${initialPosition}]`);
46933
+ }
46934
+ break;
46935
+ case "matrix":
46936
+ // Replace matrix with the matrix's dimensions
46937
+ // '[[☃ Matrix 1]]' -> '[Matrix with 2 rows and 3 columns. The user can click on each cell to enter a value]'
46938
+ const matrix = widget;
46939
+ if (((_matrix$options2 = matrix.options) == null ? void 0 : _matrix$options2.matrixBoardSize.length) === 2) {
46940
+ const [rows, columns] = matrix.options.matrixBoardSize;
46941
+ context = context.replace(`[[☃ ${widgetID}]]`, `[Matrix with ${rows} rows and ${columns} columns. The user can click on each cell to enter a value]`);
46942
+ }
46943
+ break;
46944
+ case "matcher":
46945
+ // Replace matcher with a table representing the left and right columns
46946
+ // '[[☃ Matcher 1]]' -> '| Left Column | Right Column |\n| --- | --- |\n| option 1 | option 2 |\n| option 3 | option 4 |'
46947
+ const matcher = widget;
46948
+ if ((_matcher$options3 = matcher.options) != null && _matcher$options3.left && (_matcher$options4 = matcher.options) != null && _matcher$options4.right) {
46949
+ const {
46950
+ left,
46951
+ right
46952
+ } = matcher.options;
46953
+ const [leftHeader, rightHeader] = matcher.options.labels;
46954
+ const tableHeader = `| ${leftHeader} | ${rightHeader} |\n| --- | --- |`;
46955
+ const tableRows = left.map((leftItem, index) => {
46956
+ return `| ${leftItem} | ${right[index]} |`;
46957
+ });
46958
+ const table = [tableHeader, ...tableRows].join("\n");
46959
+ const matcherWidgetExplanation = "The user needs to move items in the right column to match the correct option on the left. The order of items on the right side will be different from what the user sees.";
46960
+ context = context.replace(`[[☃ ${widgetID}]]`, `${matcherWidgetExplanation}\n${table}`);
46961
+ }
46962
+ break;
46963
+ case "numeric-input":
46964
+ case "input-number":
46965
+ case "expression":
46966
+ // These widgets are just input fields, so we replace them with a ?
46967
+ // 'Solve 2+2 [[☃ Numeric Input 1]]' -> 'Solve 2+2 ?'
46968
+ // (we've found that using "?" gives the best results on fill-
46969
+ // in-the-blank questions).
46970
+ context = context.replace(`[[☃ ${widgetID}]]`, "?");
46971
+ break;
46972
+ default:
46973
+ // Tell the bot that it doesn't understand this widget
46974
+ context = context.replace(`[[☃ ${widgetID}]]`, `[[Unsupported ${widget.type} widget: Explain to the user that you are unable to understand the content in this widget and ask them to describe it.]]`);
46975
+ }
46976
+ });
46977
+ return context;
46978
+ }
46979
+
46980
+ /**
46981
+ * Helper to parse Perseus JSON
46982
+ * Why not just use JSON.parse? We want:
46983
+ * - To make sure types are correct
46984
+ * - To give us a central place to validate/transform output if needed
46985
+ * @param {string} input - the stringified Perseus JSON
46986
+ * @returns {PerseusItem} the parsed PerseusItem object
46987
+ */
46988
+ function parsePerseusJSON(input) {
46989
+ return JSON.parse(input);
46990
+ }
46991
+
46591
46992
  const EditorJsonify = {
46592
46993
  serialize: function () {
46593
46994
  // Omit props that get passed to all widgets
@@ -46596,5 +46997,5 @@ const EditorJsonify = {
46596
46997
  }
46597
46998
  };
46598
46999
 
46599
- export { ApiOptions, ArrowPicker, ArticleRenderer, BaseRadio, Categorizer$1 as Categorizer, changeable as Changeable, ClassNames, ColorPicker, DashPicker, dependencies as Dependencies, EditorJsonify, ElementContainer, Errors, Expression, util as GrapherUtil, Grapher$1 as GrapherWidget, HintRenderer, HintsRenderer, InputNumber$1 as InputNumber, InteractiveGraph$1 as InteractiveGraphWidget, ItemExtras, JiptParagraphs, KhanColors, KhanMath, context as LoadingContext, Log, Matrix$1 as MatrixWidget, MultiItems, NumericInput$1 as NumericInput, PerseusError, PerseusExpressionAnswerFormConsidered, PerseusMarkdown, Plotter$1 as PlotterWidget, Radio, Renderer, ref as ServerItemRenderer, sizingUtils as SizingUtils, Table$1 as TableWidget, Transformer$1 as TransformerWidget, Util, denylist as WIDGET_PROP_DENYLIST, WidgetJsonifyDeprecated, widgets$1 as Widgets, apiVersion, buildEmptyItemTreeForShape, buildMapper, components, constants, displaySigFigs, globalConstants, globalStyles, iconPaths as icons, init, interactiveSizes$1 as interactiveSizes, isItemRenderableByVersion, itemDataVersion, itemToTree, ItemVersion as itemVersion, libVersion, mediaQueries, plotterPlotTypes, preprocessTex, registerAllWidgetsForTesting, shapes, traverse, violatingWidgets, allWidgets as widgets };
47000
+ export { ApiOptions, ArrowPicker, ArticleRenderer, BaseRadio, Categorizer$1 as Categorizer, changeable as Changeable, ClassNames, ColorPicker, DashPicker, dependencies as Dependencies, EditorJsonify, ElementContainer, Errors, Expression, util as GrapherUtil, Grapher$1 as GrapherWidget, HintRenderer, HintsRenderer, InputNumber$1 as InputNumber, InteractiveGraph$1 as InteractiveGraphWidget, ItemExtras, JiptParagraphs, KhanColors, KhanMath, context as LoadingContext, Log, Matrix$1 as MatrixWidget, MultiItems, NumericInput$1 as NumericInput, PerseusError, PerseusExpressionAnswerFormConsidered, PerseusMarkdown, Plotter$1 as PlotterWidget, Radio, Renderer, ref as ServerItemRenderer, sizingUtils as SizingUtils, Table$1 as TableWidget, Transformer$1 as TransformerWidget, Util, denylist as WIDGET_PROP_DENYLIST, WidgetJsonifyDeprecated, widgets$1 as Widgets, apiVersion, buildEmptyItemTreeForShape, buildMapper, components, constants, displaySigFigs, getAnswersFromWidgets, globalConstants, globalStyles, iconPaths as icons, init, injectWidgets, interactiveSizes$1 as interactiveSizes, isItemRenderableByVersion, itemDataVersion, itemToTree, ItemVersion as itemVersion, libVersion, mediaQueries, parsePerseusJSON, plotterPlotTypes, preprocessTex, registerAllWidgetsForTesting, shapes, traverse, violatingWidgets, allWidgets as widgets };
46600
47001
  //# sourceMappingURL=index.js.map