@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/index.js CHANGED
@@ -36,6 +36,7 @@ var reactPopper = require('react-popper');
36
36
  var createReactClass = require('create-react-class');
37
37
  var Link = require('@khanacademy/wonder-blocks-link');
38
38
  var perseusCore = require('@khanacademy/perseus-core');
39
+ var wonderStuffCore = require('@khanacademy/wonder-stuff-core');
39
40
 
40
41
  function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
41
42
 
@@ -8536,6 +8537,89 @@ const KhanColors = {
8536
8537
  DYNAMIC: "#6495ED" // BLUE
8537
8538
  };
8538
8539
 
8540
+ // DrawingTransform is responsible for transforming from "math coordinates"
8541
+ // (on the Cartesian plane) to "canvas coordinates" in pixels.
8542
+ class DrawingTransform {
8543
+ // Scale is measured in pixels per unit on the Cartesian plane.
8544
+
8545
+ constructor(raphael, initialScale, bounds) {
8546
+ this.raphael = raphael;
8547
+ this.bounds = bounds;
8548
+ this.xScale = initialScale[0];
8549
+ this.yScale = initialScale[1];
8550
+ this.setScale(initialScale);
8551
+ }
8552
+
8553
+ // TODO(benchristel): rename these methods to transform* instead of scale*.
8554
+ // They're doing more than scaling! Some of them also translate.
8555
+ scaleVector = point => {
8556
+ if (typeof point === "number") {
8557
+ return this.scaleVector([point, point]);
8558
+ }
8559
+ const x = point[0];
8560
+ const y = point[1];
8561
+ return [x * this.xScale, y * this.yScale];
8562
+ };
8563
+ scalePoint = point => {
8564
+ if (typeof point === "number") {
8565
+ return this.scalePoint([point, point]);
8566
+ }
8567
+ const x = point[0];
8568
+ const y = point[1];
8569
+ return [(x - this.bounds.xMin) * this.xScale, (this.bounds.yMax - y) * this.yScale];
8570
+ };
8571
+ unscalePoint = point => {
8572
+ if (typeof point === "number") {
8573
+ return this.unscalePoint([point, point]);
8574
+ }
8575
+ const x = point[0];
8576
+ const y = point[1];
8577
+ return [x / this.xScale + this.bounds.xMin, this.bounds.yMax - y / this.yScale];
8578
+ };
8579
+ unscaleVector = point => {
8580
+ if (typeof point === "number") {
8581
+ return this.unscaleVector([point, point]);
8582
+ }
8583
+ return [point[0] / this.xScale, point[1] / this.yScale];
8584
+ };
8585
+ setScale = scale => {
8586
+ if (typeof scale === "number") {
8587
+ scale = [scale, scale];
8588
+ }
8589
+ this.xScale = scale[0];
8590
+ this.yScale = scale[1];
8591
+
8592
+ // Update the canvas size
8593
+ this.raphael.setSize(...this.canvasDimensions());
8594
+ };
8595
+ canvasDimensions = () => {
8596
+ return [this.bounds.width() * this.xScale, this.bounds.height() * this.yScale];
8597
+ };
8598
+ pixelsPerUnitX = () => {
8599
+ return this.xScale;
8600
+ };
8601
+ pixelsPerUnitY = () => {
8602
+ return this.yScale;
8603
+ };
8604
+ }
8605
+
8606
+ // Defines the bounds of the visible area of a graph, in Cartesian coordinates.
8607
+ // GraphBounds is immutable.
8608
+ class GraphBounds {
8609
+ constructor(xRange, yRange) {
8610
+ this.xMin = xRange[0];
8611
+ this.xMax = xRange[1];
8612
+ this.yMin = yRange[0];
8613
+ this.yMax = yRange[1];
8614
+ }
8615
+ width() {
8616
+ return this.xMax - this.xMin;
8617
+ }
8618
+ height() {
8619
+ return this.yMax - this.yMin;
8620
+ }
8621
+ }
8622
+
8539
8623
  function render(element, container, callback) {
8540
8624
  // TODO(LP-11406): Replace this, or callers, with React Portal
8541
8625
  // @ts-expect-error - TS2322 - Type 'void' is not assignable to type 'ElementRef<ElementType>'.
@@ -8563,23 +8647,6 @@ function findChildOrAdd(elem, className) {
8563
8647
  }
8564
8648
  return $child;
8565
8649
  }
8566
- function doCallback(elem, callback) {
8567
- let tries = 0;
8568
- (function check() {
8569
- const height = elem.scrollHeight;
8570
- // Heuristic to guess if the font has kicked in
8571
- // so we have box metrics (magic number ick,
8572
- // but this seems to work mostly-consistently)
8573
- if (height > 18 || tries >= 10) {
8574
- callback();
8575
- } else {
8576
- tries++;
8577
- // TODO(jeff, CP-3128): Use Wonder Blocks Timing API
8578
- // eslint-disable-next-line no-restricted-syntax
8579
- setTimeout(check, 100);
8580
- }
8581
- })();
8582
- }
8583
8650
  var Tex$1 = {
8584
8651
  // Process a node and add math inside of it. This attempts to use KaTeX to
8585
8652
  // format the math, and if that fails it falls back to MathJax.
@@ -8601,27 +8668,14 @@ var Tex$1 = {
8601
8668
 
8602
8669
  // Only process if it hasn't been done before, or it is forced
8603
8670
  if ($elem.attr("data-math-formula") == null || force) {
8604
- const $katexHolder = findChildOrAdd($elem, "katex-holder");
8605
- const $mathjaxHolder = findChildOrAdd($elem, "mathjax-holder");
8606
-
8607
- // Search for MathJax-y script tags inside of the node. These are
8608
- // used by MathJax to denote the formula to be typeset. Before, we
8609
- // would update the formula by updating the contents of the script
8610
- // tag, which shouldn't happen any more, but we manage them just in
8611
- // case.
8612
- const script = $mathjaxHolder.find("script[type='math/tex']")[0];
8613
-
8614
- // If text wasn't provided, we look in two places
8615
- if (text == null) {
8616
- if ($elem.attr("data-math-formula")) {
8617
- // The old typeset formula
8618
- // @ts-expect-error - TS2322 - Type 'string | undefined' is not assignable to type 'string'.
8619
- text = $elem.attr("data-math-formula");
8620
- } else if (script) {
8621
- // The contents of the <script> tag
8622
- // @ts-expect-error - TS2339 - Property 'text' does not exist on type 'HTMLElement'.
8623
- text = script.text || script.textContent;
8624
- }
8671
+ const $texHolder = findChildOrAdd($elem, "tex-holder");
8672
+
8673
+ // If text wasn't provided, we use the cached text.
8674
+ // TODO(benchristel): I'm not sure if text can ever be null. It's
8675
+ // possible we don't need this check.
8676
+ if (text == null && $elem.attr("data-math-formula")) {
8677
+ // @ts-expect-error - TS2322 - Type 'string | undefined' is not assignable to type 'string'.
8678
+ text = $elem.attr("data-math-formula");
8625
8679
  }
8626
8680
  text = text != null ? text + "" : "";
8627
8681
 
@@ -8637,41 +8691,9 @@ var Tex$1 = {
8637
8691
  // that name is already taken.
8638
8692
  render( /*#__PURE__*/React__namespace.createElement(TeX, {
8639
8693
  children: text,
8640
- onRender: () => {
8641
- if (callback) {
8642
- doCallback(elem, callback);
8643
- }
8644
- }
8645
- }), $katexHolder[0]);
8646
- }
8647
- },
8648
- // Function to restore a node to a non-math-processed state
8649
- cleanupMath: function (elem) {
8650
- const $elem = $__default["default"](elem);
8651
-
8652
- // Only mess with it if it's been processed before
8653
- if ($elem.attr("data-math-formula")) {
8654
- // Remove MathJax remnants
8655
- if (typeof MathJax !== "undefined") {
8656
- const jax = MathJax.Hub.getJaxFor($elem.find("script")[0]);
8657
- if (jax) {
8658
- const e = jax.SourceElement();
8659
- if (e.previousSibling && e.previousSibling.className) {
8660
- jax.Remove();
8661
- }
8662
- }
8663
- }
8664
-
8665
- // @ts-expect-error - TS2769 - No overload matches this call.
8666
- $elem.text($elem.attr("data-math-formula"));
8667
- $elem.attr("data-math-formula", null);
8668
- $elem.attr("data-math-type", null);
8694
+ onRender: callback
8695
+ }), $texHolder[0]);
8669
8696
  }
8670
- return elem;
8671
- },
8672
- // Function to retrieve the formula of a typeset math node
8673
- retrieveMathFormula: function (elem) {
8674
- return $__default["default"](elem).attr("data-math-formula");
8675
8697
  }
8676
8698
  };
8677
8699
 
@@ -8679,23 +8701,6 @@ var Tex$1 = {
8679
8701
  const {
8680
8702
  processMath: processMath$1
8681
8703
  } = Tex$1;
8682
-
8683
- /* Convert cartesian coordinates [x, y] to polar coordinates [r,
8684
- * theta], with theta in degrees, or in radians if angleInRadians is
8685
- * specified.
8686
- */
8687
- function cartToPolar(coord, angleInRadians) {
8688
- const r = Math.sqrt(Math.pow(coord[0], 2) + Math.pow(coord[1], 2));
8689
- let theta = Math.atan2(coord[1], coord[0]);
8690
- // convert angle range from [-pi, pi] to [0, 2pi]
8691
- if (theta < 0) {
8692
- theta += 2 * Math.PI;
8693
- }
8694
- if (!angleInRadians) {
8695
- theta = theta * 180 / Math.PI;
8696
- }
8697
- return [r, theta];
8698
- }
8699
8704
  function polar(r, th) {
8700
8705
  if (typeof r === "number") {
8701
8706
  r = [r, r];
@@ -8751,135 +8756,336 @@ const GraphUtils = {
8751
8756
  },
8752
8757
  graphs: {}
8753
8758
  };
8754
- const Graphie$1 = GraphUtils.Graphie = function () {};
8755
- ___default["default"].extend(Graphie$1.prototype, {
8756
- cartToPolar: cartToPolar,
8757
- polar: polar
8758
- });
8759
- const labelDirections = {
8760
- center: [-0.5, -0.5],
8761
- above: [-0.5, -1.0],
8762
- "above right": [0.0, -1.0],
8763
- right: [0.0, -0.5],
8764
- "below right": [0.0, 0.0],
8765
- below: [-0.5, 0.0],
8766
- "below left": [-1.0, 0.0],
8767
- left: [-1.0, -0.5],
8768
- "above left": [-1.0, -1.0]
8769
- };
8770
-
8771
- /**
8772
- * Safari applies some SVG-specific styles to things that are not SVGs, so we
8773
- * need to exclude those styles from things that are not SVGs.
8774
- *
8775
- * To see this behavior in action, open https://codepen.io/anon/pen/zENEoa in
8776
- * Safari.
8777
- *
8778
- * Usage `$.extend({}, someStyles, SVG_SPECIFIC_STYLE_MASK)`
8779
- */
8780
- const SVG_SPECIFIC_STYLE_MASK = {
8781
- "stroke-width": null
8782
- };
8783
- GraphUtils.createGraphie = function (el) {
8784
- let xScale = 40;
8785
- let yScale = 40;
8786
- let xRange;
8787
- let yRange;
8788
- $__default["default"](el).css("position", "relative");
8789
- const raphael$1 = raphael.exports(el);
8790
-
8791
- // For a sometimes-reproducible IE8 bug; doesn't affect SVG browsers at all
8792
- $__default["default"](el).children("div").css("position", "absolute");
8793
-
8759
+ class Graphie$1 {
8760
+ #bounds;
8761
+ #drawingTransform;
8762
+ isMobile = false;
8794
8763
  // Set up some reasonable defaults
8795
- let currentStyle = {
8764
+ currentStyle = {
8796
8765
  "stroke-width": 2,
8797
8766
  fill: "none"
8798
8767
  };
8799
- const scaleVector = function (point) {
8800
- if (typeof point === "number") {
8801
- return scaleVector([point, point]);
8802
- }
8803
- const x = point[0];
8804
- const y = point[1];
8805
- return [x * xScale, y * yScale];
8806
- };
8807
- const scalePoint = function scalePoint(point) {
8808
- if (typeof point === "number") {
8809
- return scalePoint([point, point]);
8810
- }
8811
- const x = point[0];
8812
- const y = point[1];
8813
- return [(x - xRange[0]) * xScale, (yRange[1] - y) * yScale];
8814
- };
8815
- const unscalePoint = function (point) {
8816
- if (typeof point === "number") {
8817
- return unscalePoint([point, point]);
8768
+
8769
+ // TODO(benchristel): xpixels and ypixels are never used by this class, but
8770
+ // other code reaches in to access them :(
8771
+ // Refactor to accessor methods that look at this.dimensions.
8772
+ constructor(el) {
8773
+ this.el = el;
8774
+ $__default["default"](el).css("position", "relative");
8775
+ this.raphael = raphael.exports(el);
8776
+
8777
+ // For a sometimes-reproducible IE8 bug; doesn't affect SVG browsers at all
8778
+ $__default["default"](el).children("div").css("position", "absolute");
8779
+ }
8780
+ init(options) {
8781
+ let scale = options.scale || [40, 40];
8782
+ scale = typeof scale === "number" ? [scale, scale] : scale;
8783
+ if (options.range == null) {
8784
+ throw new PerseusError("range should be specified in graph init", Errors.Internal);
8785
+ }
8786
+ this.#bounds = new GraphBounds(...options.range);
8787
+ this.#drawingTransform = new DrawingTransform(this.raphael, scale, this.bounds());
8788
+ const [w, h] = this.drawingTransform().canvasDimensions();
8789
+ $__default["default"](this.el).css({
8790
+ width: w,
8791
+ height: h
8792
+ });
8793
+ this.range = options.range;
8794
+ this.scale = scale;
8795
+ // TODO(benchristel): I don't think dimensions is used. Can we
8796
+ // remove it?
8797
+ this.dimensions = [w, h];
8798
+ this.xpixels = w;
8799
+ this.ypixels = h;
8800
+ this.isMobile = options.isMobile;
8801
+ return this;
8802
+ }
8803
+
8804
+ // Initializes graphie settings for a graph and draws the basic graph
8805
+ // features (axes, grid, tick marks, and axis labels)
8806
+ // Options expected are:
8807
+ // - range: [[a, b], [c, d]] or [a, b]
8808
+ // - scale: [a, b] or number
8809
+ // - gridOpacity: number (0 - 1)
8810
+ // - gridStep: [a, b] or number (relative to units)
8811
+ // - tickStep: [a, b] or number (relative to grid steps)
8812
+ // - tickLen: [a, b] or number (in pixels)
8813
+ // - labelStep: [a, b] or number (relative to tick steps)
8814
+ // - yLabelFormat: fn to format label string for y-axis
8815
+ // - xLabelFormat: fn to format label string for x-axis
8816
+ graphInit(options) {
8817
+ options = options || {};
8818
+ $__default["default"].each(options, function (prop, val) {
8819
+ // allow options to be specified by a single number for shorthand if
8820
+ // the horizontal and vertical components are the same
8821
+ if (
8822
+ // @ts-expect-error - TS2339 - Property 'match' does not exist on type 'string | number | symbol'.
8823
+ !prop.match(/.*Opacity$/) && prop !== "range" && typeof val === "number") {
8824
+ options[prop] = [val, val];
8825
+ }
8826
+
8827
+ // allow symmetric ranges to be specified by the absolute values
8828
+ if (prop === "range" || prop === "gridRange") {
8829
+ if (val.constructor === Array) {
8830
+ // but don't mandate symmetric ranges
8831
+ if (val[0].constructor !== Array) {
8832
+ options[prop] = [[-val[0], val[0]], [-val[1], val[1]]];
8833
+ }
8834
+ } else if (typeof val === "number") {
8835
+ options[prop] = [[-val, val], [-val, val]];
8836
+ }
8837
+ }
8838
+ });
8839
+ const range = options.range || [[-10, 10], [-10, 10]];
8840
+ const gridRange = options.gridRange || options.range;
8841
+ const scale = options.scale || [20, 20];
8842
+ const grid = options.grid != null ? options.grid : true;
8843
+ const gridOpacity = options.gridOpacity || 0.1;
8844
+ const gridStep = options.gridStep || [1, 1];
8845
+ const axes = options.axes != null ? options.axes : true;
8846
+ const axisArrows = options.axisArrows || "";
8847
+ const axisOpacity = options.axisOpacity || 1.0;
8848
+ 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])];
8849
+ const axisLabels = options.axisLabels != null ? options.axisLabels : false;
8850
+ const ticks = options.ticks != null ? options.ticks : true;
8851
+ const tickStep = options.tickStep || [2, 2];
8852
+ const tickLen = options.tickLen || [5, 5];
8853
+ const tickOpacity = options.tickOpacity || 1.0;
8854
+ const labels = options.labels || options.labelStep || false;
8855
+ const labelStep = options.labelStep || [1, 1];
8856
+ const labelOpacity = options.labelOpacity || 1.0;
8857
+ let unityLabels = options.unityLabels || false;
8858
+ const labelFormat = options.labelFormat || function (a) {
8859
+ return a;
8860
+ };
8861
+ let xLabelFormat = options.xLabelFormat || labelFormat;
8862
+ let yLabelFormat = options.yLabelFormat || labelFormat;
8863
+ 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)]];
8864
+ if (!___default["default"].isArray(unityLabels)) {
8865
+ unityLabels = [unityLabels, unityLabels];
8818
8866
  }
8819
- const x = point[0];
8820
- const y = point[1];
8821
- return [x / xScale + xRange[0], yRange[1] - y / yScale];
8822
- };
8823
- const unscaleVector = function (point) {
8824
- if (typeof point === "number") {
8825
- return unscaleVector([point, point]);
8867
+ const minusIgnorer = function (lf) {
8868
+ return function (a) {
8869
+ return (lf(a) + "").replace(/-(\d)/g, "\\llap{-}$1");
8870
+ };
8871
+ };
8872
+ xLabelFormat = minusIgnorer(xLabelFormat);
8873
+ yLabelFormat = minusIgnorer(yLabelFormat);
8874
+ this.init({
8875
+ range: realRange,
8876
+ scale: scale,
8877
+ isMobile: options.isMobile
8878
+ });
8879
+
8880
+ // draw grid
8881
+ if (grid) {
8882
+ this.grid(gridRange[0], gridRange[1], {
8883
+ stroke: options.isMobile ? KhanColors.GRAY_C : "#000000",
8884
+ opacity: options.isMobile ? 1 : gridOpacity,
8885
+ step: gridStep,
8886
+ strokeWidth: options.isMobile ? 1 : 2
8887
+ });
8826
8888
  }
8827
- return [point[0] / xScale, point[1] / yScale];
8828
- };
8829
- const setLabelMargins = function (span, size) {
8830
- const $span = $__default["default"](span);
8831
- const direction = $span.data("labelDirection");
8832
- let [width, height] = size;
8833
- // This can happen when a span
8834
- // is invisible but we still want to update the CSS. At worst, we will
8835
- // be off by a few pixels instead of in a different position entirely.
8836
- if (width === 0 && height === 0) {
8837
- [width, height] = [1, 1];
8838
- Log.log("Label size was 0x0 in graphie.js; using 1x1 instead");
8889
+
8890
+ // draw axes
8891
+ if (axes) {
8892
+ // this is a slight hack until <-> arrowheads work
8893
+ if (axisArrows === "<->" || axisArrows === true) {
8894
+ const thisGraphie = this;
8895
+ this.style({
8896
+ stroke: options.isMobile ? KhanColors.GRAY_G : "#000000",
8897
+ opacity: options.isMobile ? 1 : axisOpacity,
8898
+ strokeWidth: options.isMobile ? 1 : 2,
8899
+ arrows: "->"
8900
+ }, function () {
8901
+ if (range[1][0] < 0 && range[1][1] > 0) {
8902
+ thisGraphie.path([axisCenter, [gridRange[0][0], axisCenter[1]]]);
8903
+ thisGraphie.path([axisCenter, [gridRange[0][1], axisCenter[1]]]);
8904
+ }
8905
+ if (range[0][0] < 0 && range[0][1] > 0) {
8906
+ thisGraphie.path([axisCenter, [axisCenter[0], gridRange[1][0]]]);
8907
+ thisGraphie.path([axisCenter, [axisCenter[0], gridRange[1][1]]]);
8908
+ }
8909
+ });
8910
+
8911
+ // also, we don't support "<-" arrows yet, but why you
8912
+ // would want that on your graph is beyond me.
8913
+ } else if (axisArrows === "->" || axisArrows === "") {
8914
+ const thisGraphie = this;
8915
+ this.style({
8916
+ stroke: "#000000",
8917
+ opacity: axisOpacity,
8918
+ strokeWidth: 2,
8919
+ arrows: axisArrows
8920
+ }, function () {
8921
+ thisGraphie.path([[gridRange[0][0], axisCenter[1]], [gridRange[0][1], axisCenter[1]]]);
8922
+ thisGraphie.path([[axisCenter[0], gridRange[1][0]], [axisCenter[0], gridRange[1][1]]]);
8923
+ });
8924
+ }
8925
+ if (axisLabels && axisLabels.length === 2) {
8926
+ this.label([gridRange[0][1], axisCenter[1]], axisLabels[0], "right");
8927
+ this.label([axisCenter[0], gridRange[1][1]], axisLabels[1], "above");
8928
+ }
8839
8929
  }
8840
- $span.css("visibility", "");
8841
- if (typeof direction === "number") {
8842
- const x = Math.cos(direction);
8843
- const y = Math.sin(direction);
8844
- const scale = Math.min(width / 2 / Math.abs(x), height / 2 / Math.abs(y));
8845
- $span.css({
8846
- marginLeft: -width / 2 + x * scale,
8847
- marginTop: -height / 2 - y * scale
8930
+
8931
+ // draw tick marks
8932
+ if (ticks) {
8933
+ const halfWidthTicks = options.isMobile;
8934
+ const thisGraphie = this;
8935
+ this.style({
8936
+ stroke: options.isMobile ? KhanColors.GRAY_G : "#000000",
8937
+ opacity: options.isMobile ? 1 : tickOpacity,
8938
+ strokeWidth: 1
8939
+ }, function () {
8940
+ // horizontal axis
8941
+ let step = gridStep[0] * tickStep[0];
8942
+ let len = tickLen[0] / scale[1];
8943
+ let start = gridRange[0][0];
8944
+ let stop = gridRange[0][1];
8945
+ if (range[1][0] < 0 && range[1][1] > 0) {
8946
+ for (let x = step + axisCenter[0]; x <= stop; x += step) {
8947
+ if (x < stop || !axisArrows) {
8948
+ thisGraphie.line([x, -len + axisCenter[1]], [x, halfWidthTicks ? 0 : len + axisCenter[1]]);
8949
+ }
8950
+ }
8951
+ for (let x = -step + axisCenter[0]; x >= start; x -= step) {
8952
+ if (x > start || !axisArrows) {
8953
+ thisGraphie.line([x, -len + axisCenter[1]], [x, halfWidthTicks ? 0 : len + axisCenter[1]]);
8954
+ }
8955
+ }
8956
+ }
8957
+
8958
+ // vertical axis
8959
+ step = gridStep[1] * tickStep[1];
8960
+ len = tickLen[1] / scale[0];
8961
+ start = gridRange[1][0];
8962
+ stop = gridRange[1][1];
8963
+ if (range[0][0] < 0 && range[0][1] > 0) {
8964
+ for (let y = step + axisCenter[1]; y <= stop; y += step) {
8965
+ if (y < stop || !axisArrows) {
8966
+ // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
8967
+ this.line([-len + axisCenter[0], y], [halfWidthTicks ? 0 : len + axisCenter[0], y]);
8968
+ }
8969
+ }
8970
+ for (let y = -step + axisCenter[1]; y >= start; y -= step) {
8971
+ if (y > start || !axisArrows) {
8972
+ // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
8973
+ this.line([-len + axisCenter[0], y], [halfWidthTicks ? 0 : len + axisCenter[0], y]);
8974
+ }
8975
+ }
8976
+ }
8848
8977
  });
8849
- } else {
8850
- const multipliers = labelDirections[direction || "center"];
8851
- $span.css({
8852
- marginLeft: Math.round(width * multipliers[0]),
8853
- marginTop: Math.round(height * multipliers[1])
8978
+ }
8979
+
8980
+ // draw axis labels
8981
+ if (labels) {
8982
+ const thisGraphie = this;
8983
+ this.style({
8984
+ stroke: options.isMobile ? KhanColors.GRAY_G : "#000000",
8985
+ opacity: options.isMobile ? 1 : labelOpacity
8986
+ }, function () {
8987
+ // horizontal axis
8988
+ let step = gridStep[0] * tickStep[0] * labelStep[0];
8989
+ let start = gridRange[0][0];
8990
+ let stop = gridRange[0][1];
8991
+ const xAxisPosition = axisCenter[0] < 0 ? "above" : "below";
8992
+ const yAxisPosition = axisCenter[0] < 0 ? "right" : "left";
8993
+ const xShowZero = axisCenter[0] === 0 && axisCenter[1] !== 0;
8994
+ const yShowZero = axisCenter[0] !== 0 && axisCenter[1] === 0;
8995
+ const axisOffCenter = axisCenter[0] !== 0 || axisCenter[1] !== 0;
8996
+ const showUnityX = unityLabels[0] || axisOffCenter;
8997
+ const showUnityY = unityLabels[1] || axisOffCenter;
8998
+
8999
+ // positive x-axis
9000
+ for (let x = (xShowZero ? 0 : step) + axisCenter[0]; x <= stop; x += step) {
9001
+ if (x < stop || !axisArrows) {
9002
+ thisGraphie.label([x, axisCenter[1]], xLabelFormat(x), xAxisPosition);
9003
+ }
9004
+ }
9005
+
9006
+ // negative x-axis
9007
+ for (let x = -step * (showUnityX ? 1 : 2) + axisCenter[0]; x >= start; x -= step) {
9008
+ if (x > start || !axisArrows) {
9009
+ thisGraphie.label([x, axisCenter[1]], xLabelFormat(x), xAxisPosition);
9010
+ }
9011
+ }
9012
+ step = gridStep[1] * tickStep[1] * labelStep[1];
9013
+ start = gridRange[1][0];
9014
+ stop = gridRange[1][1];
9015
+
9016
+ // positive y-axis
9017
+ for (let y = (yShowZero ? 0 : step) + axisCenter[1]; y <= stop; y += step) {
9018
+ if (y < stop || !axisArrows) {
9019
+ thisGraphie.label([axisCenter[0], y], yLabelFormat(y), yAxisPosition);
9020
+ }
9021
+ }
9022
+
9023
+ // negative y-axis
9024
+ for (let y = -step * (showUnityY ? 1 : 2) + axisCenter[1]; y >= start; y -= step) {
9025
+ if (y > start || !axisArrows) {
9026
+ thisGraphie.label([axisCenter[0], y], yLabelFormat(y), yAxisPosition);
9027
+ }
9028
+ }
8854
9029
  });
8855
9030
  }
8856
- };
8857
- const svgPath = function (points, alreadyScaled) {
8858
- return $__default["default"].map(points, function (point, i) {
9031
+ }
9032
+ drawingTransform() {
9033
+ if (this.#drawingTransform == null) {
9034
+ throw new Error("Can't get drawingTransform of an uninitialized Graphie");
9035
+ }
9036
+ return this.#drawingTransform;
9037
+ }
9038
+ bounds() {
9039
+ if (this.#bounds == null) {
9040
+ throw new Error("Can't get bounds of an uninitialized Graphie");
9041
+ }
9042
+ return this.#bounds;
9043
+ }
9044
+ style(attrs, fn) {
9045
+ const processed = this.processAttributes(attrs);
9046
+ if (typeof fn === "function") {
9047
+ const oldStyle = this.currentStyle;
9048
+ this.currentStyle = $__default["default"].extend({}, this.currentStyle, processed);
9049
+ const result = fn.call(this);
9050
+ this.currentStyle = oldStyle;
9051
+ return result;
9052
+ }
9053
+ $__default["default"].extend(this.currentStyle, processed);
9054
+ }
9055
+ label(point, text, direction, latex) {}
9056
+ grid(xr, yr, styleAttributes) {}
9057
+
9058
+ // path is a stub that gets overwritten with a function from drawingTools
9059
+ // in createGraphie
9060
+ path(points) {}
9061
+
9062
+ // line is a stub that gets overwritten with a function from drawingTools
9063
+ // in createGraphie
9064
+ line(start, end) {}
9065
+ svgPath = (points, alreadyScaled) => {
9066
+ return $__default["default"].map(points, (point, i) => {
8859
9067
  if (point === true) {
8860
9068
  return "z";
8861
9069
  }
8862
- const scaled = alreadyScaled ? point : scalePoint(point);
9070
+ const scaled = alreadyScaled ? point : this.scalePoint(point);
8863
9071
  return (i === 0 ? "M" : "L") + KhanMath.bound(scaled[0]) + " " + KhanMath.bound(scaled[1]);
8864
9072
  }).join("");
8865
9073
  };
8866
- const svgParabolaPath = function (a, b, c) {
9074
+ svgParabolaPath = (a, b, c) => {
8867
9075
  const computeParabola = function (x) {
8868
9076
  return (a * x + b) * x + c;
8869
9077
  };
8870
9078
 
8871
9079
  // If points are collinear, plot a line instead
8872
9080
  if (a === 0) {
8873
- const points = ___default["default"].map(xRange, function (x) {
8874
- return [x, computeParabola(x)];
8875
- });
9081
+ const points = [[this.bounds().xMin, computeParabola(this.bounds().xMin)], [this.bounds().xMax, computeParabola(this.bounds().xMax)]];
8876
9082
  // @ts-expect-error - TS2554 - Expected 2 arguments, but got 1.
8877
- return svgPath(points);
9083
+ return this.svgPath(points);
8878
9084
  }
8879
9085
 
8880
9086
  // Calculate x coordinates of points on parabola
8881
9087
  const xVertex = -b / (2 * a);
8882
- const distToEdge = Math.max(Math.abs(xVertex - xRange[0]), Math.abs(xVertex - xRange[1]));
9088
+ const distToEdge = Math.max(Math.abs(xVertex - this.bounds().xMin), Math.abs(xVertex - this.bounds().xMax));
8883
9089
 
8884
9090
  // To guarantee that drawn parabola to spans the viewport, use a point
8885
9091
  // on the edge of the graph furtherest from the vertex
@@ -8899,11 +9105,11 @@ GraphUtils.createGraphie = function (el) {
8899
9105
 
8900
9106
  // Scale and bound
8901
9107
  // @ts-expect-error - TS2345 - Argument of type '(point: number | Coord) => Coord' is not assignable to parameter of type 'Iteratee<any[][], any, any[]>'.
8902
- const points = ___default["default"].map([left, control, right], scalePoint);
9108
+ const points = ___default["default"].map([left, control, right], this.scalePoint);
8903
9109
  const values = ___default["default"].map(___default["default"].flatten(points), KhanMath.bound);
8904
9110
  return "M" + values[0] + "," + values[1] + " Q" + values[2] + "," + values[3] + " " + values[4] + "," + values[5];
8905
9111
  };
8906
- const svgSinusoidPath = function (a, b, c, d) {
9112
+ svgSinusoidPath = (a, b, c, d) => {
8907
9113
  // Plot a sinusoid of the form: f(x) = a * sin(b * x - c) + d
8908
9114
  const quarterPeriod = Math.abs(Math.PI / (2 * b));
8909
9115
  const computeSine = function (x) {
@@ -8912,7 +9118,7 @@ GraphUtils.createGraphie = function (el) {
8912
9118
  const computeDerivative = function (x) {
8913
9119
  return a * b * Math.cos(c - b * x);
8914
9120
  };
8915
- const coordsForOffset = function (initial, i) {
9121
+ const coordsForOffset = (initial, i) => {
8916
9122
  // Return the cubic coordinates (including the two anchor and two
8917
9123
  // control points) for the ith portion of the sinusoid.
8918
9124
  const x0 = initial + quarterPeriod * i;
@@ -8925,16 +9131,16 @@ GraphUtils.createGraphie = function (el) {
8925
9131
 
8926
9132
  // Zip and scale
8927
9133
  // @ts-expect-error - TS2345 - Argument of type '(point: number | Coord) => Coord' is not assignable to parameter of type 'Iteratee<any[][], any, any[]>'.
8928
- return ___default["default"].map(___default["default"].zip(xCoords, yCoords), scalePoint);
9134
+ return ___default["default"].map(___default["default"].zip(xCoords, yCoords), this.scalePoint);
8929
9135
  };
8930
9136
 
8931
9137
  // How many quarter-periods do we need to span the graph?
8932
- const extent = xRange[1] - xRange[0];
9138
+ const extent = this.bounds().width();
8933
9139
  const numQuarterPeriods = Math.ceil(extent / quarterPeriod) + 1;
8934
9140
 
8935
- // Find starting coordinate: first anchor point curve left of xRange[0]
9141
+ // Find starting coordinate: first anchor point curve left of bounds.xMin
8936
9142
  let initial = c / b;
8937
- const distToEdge = initial - xRange[0];
9143
+ const distToEdge = initial - this.bounds().xMin;
8938
9144
  initial -= quarterPeriod * Math.ceil(distToEdge / quarterPeriod);
8939
9145
 
8940
9146
  // First portion of path is special-case, requiring move-to ('M')
@@ -8946,28 +9152,30 @@ GraphUtils.createGraphie = function (el) {
8946
9152
  }
8947
9153
  return path;
8948
9154
  };
8949
-
8950
- // `svgPath` is independent of graphie range, so we export it independently
8951
- GraphUtils.svgPath = svgPath;
8952
- const processAttributes = function (attrs) {
9155
+ scalePoint = point => {
9156
+ return this.drawingTransform().scalePoint(point);
9157
+ };
9158
+ scaleVector = point => {
9159
+ return this.drawingTransform().scaleVector(point);
9160
+ };
9161
+ unscalePoint = point => {
9162
+ return this.drawingTransform().unscalePoint(point);
9163
+ };
9164
+ unscaleVector = point => {
9165
+ return this.drawingTransform().unscaleVector(point);
9166
+ };
9167
+ processAttributes(attrs) {
8953
9168
  const transformers = {
8954
- scale: function (scale) {
8955
- if (typeof scale === "number") {
8956
- scale = [scale, scale];
8957
- }
8958
- xScale = scale[0];
8959
- yScale = scale[1];
8960
-
8961
- // Update the canvas size
8962
- raphael$1.setSize((xRange[1] - xRange[0]) * xScale, (yRange[1] - yRange[0]) * yScale);
9169
+ scale: scale => {
9170
+ this.drawingTransform().setScale(scale);
8963
9171
  },
8964
- clipRect: function (pair) {
9172
+ clipRect: pair => {
8965
9173
  const point = pair[0];
8966
9174
  const size = pair[1];
8967
9175
  point[1] += size[1]; // because our coordinates are flipped
8968
9176
 
8969
9177
  return {
8970
- "clip-rect": scalePoint(point).concat(scaleVector(size)).join(" ")
9178
+ "clip-rect": this.scalePoint(point).concat(this.scaleVector(size)).join(" ")
8971
9179
  };
8972
9180
  },
8973
9181
  strokeWidth: function (val) {
@@ -8975,18 +9183,18 @@ GraphUtils.createGraphie = function (el) {
8975
9183
  "stroke-width": parseFloat(val)
8976
9184
  };
8977
9185
  },
8978
- rx: function (val) {
9186
+ rx: val => {
8979
9187
  return {
8980
- rx: scaleVector([val, 0])[0]
9188
+ rx: this.scaleVector([val, 0])[0]
8981
9189
  };
8982
9190
  },
8983
- ry: function (val) {
9191
+ ry: val => {
8984
9192
  return {
8985
- ry: scaleVector([0, val])[1]
9193
+ ry: this.scaleVector([0, val])[1]
8986
9194
  };
8987
9195
  },
8988
- r: function (val) {
8989
- const scaled = scaleVector([val, val]);
9196
+ r: val => {
9197
+ const scaled = this.scaleVector([val, val]);
8990
9198
  return {
8991
9199
  rx: scaled[0],
8992
9200
  ry: scaled[1]
@@ -9006,7 +9214,66 @@ GraphUtils.createGraphie = function (el) {
9006
9214
  }
9007
9215
  });
9008
9216
  return processed;
9217
+ }
9218
+ }
9219
+ GraphUtils.Graphie = Graphie$1;
9220
+ const labelDirections = {
9221
+ center: [-0.5, -0.5],
9222
+ above: [-0.5, -1.0],
9223
+ "above right": [0.0, -1.0],
9224
+ right: [0.0, -0.5],
9225
+ "below right": [0.0, 0.0],
9226
+ below: [-0.5, 0.0],
9227
+ "below left": [-1.0, 0.0],
9228
+ left: [-1.0, -0.5],
9229
+ "above left": [-1.0, -1.0]
9230
+ };
9231
+
9232
+ /**
9233
+ * Safari applies some SVG-specific styles to things that are not SVGs, so we
9234
+ * need to exclude those styles from things that are not SVGs.
9235
+ *
9236
+ * To see this behavior in action, open https://codepen.io/anon/pen/zENEoa in
9237
+ * Safari.
9238
+ *
9239
+ * Usage `$.extend({}, someStyles, SVG_SPECIFIC_STYLE_MASK)`
9240
+ */
9241
+ const SVG_SPECIFIC_STYLE_MASK = {
9242
+ "stroke-width": null
9243
+ };
9244
+ GraphUtils.createGraphie = function (el) {
9245
+ const thisGraphie = new Graphie$1(el);
9246
+ const setLabelMargins = function (span, size) {
9247
+ const $span = $__default["default"](span);
9248
+ const direction = $span.data("labelDirection");
9249
+ let [width, height] = size;
9250
+ // This can happen when a span
9251
+ // is invisible but we still want to update the CSS. At worst, we will
9252
+ // be off by a few pixels instead of in a different position entirely.
9253
+ if (width === 0 && height === 0) {
9254
+ [width, height] = [1, 1];
9255
+ Log.log("Label size was 0x0 in graphie.js; using 1x1 instead");
9256
+ }
9257
+ $span.css("visibility", "");
9258
+ if (typeof direction === "number") {
9259
+ const x = Math.cos(direction);
9260
+ const y = Math.sin(direction);
9261
+ const scale = Math.min(width / 2 / Math.abs(x), height / 2 / Math.abs(y));
9262
+ $span.css({
9263
+ marginLeft: -width / 2 + x * scale,
9264
+ marginTop: -height / 2 - y * scale
9265
+ });
9266
+ } else {
9267
+ const multipliers = labelDirections[direction || "center"];
9268
+ $span.css({
9269
+ marginLeft: Math.round(width * multipliers[0]),
9270
+ marginTop: Math.round(height * multipliers[1])
9271
+ });
9272
+ }
9009
9273
  };
9274
+
9275
+ // `svgPath` is independent of graphie range, so we export it independently
9276
+ GraphUtils.svgPath = thisGraphie.svgPath;
9010
9277
  const addArrowheads = function arrows(path) {
9011
9278
  const type = path.constructor.prototype;
9012
9279
  if (type === raphael.exports.el) {
@@ -9014,21 +9281,21 @@ GraphUtils.createGraphie = function (el) {
9014
9281
  const w = path.attr("stroke-width");
9015
9282
  const s = 0.6 + 0.4 * w;
9016
9283
  const l = path.getTotalLength();
9017
- const set = raphael$1.set();
9018
- 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");
9284
+ const set = thisGraphie.raphael.set();
9285
+ 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");
9019
9286
  const end = path.getPointAtLength(l - 0.4);
9020
9287
  const almostTheEnd = path.getPointAtLength(l - 0.75 * s);
9021
9288
  const angle = Math.atan2(end.y - almostTheEnd.y, end.x - almostTheEnd.x) * 180 / Math.PI;
9022
9289
  const attrs = path.attr();
9023
9290
  delete attrs.path;
9024
9291
  let subpath = path.getSubpath(0, l - 0.75 * s);
9025
- subpath = raphael$1.path(subpath).attr(attrs);
9292
+ subpath = thisGraphie.raphael.path(subpath).attr(attrs);
9026
9293
  subpath.arrowheadsDrawn = true;
9027
9294
  path.remove();
9028
9295
 
9029
9296
  // For some unknown reason 0 doesn't work for the rotation
9030
9297
  // origin so we use a tiny number.
9031
- head.rotate(angle, graphie.isMobile ? 1e-5 : 0.75, 0).scale(s, s, 0.75, 0).translate(almostTheEnd.x, almostTheEnd.y).attr(attrs).attr({
9298
+ head.rotate(angle, thisGraphie.isMobile ? 1e-5 : 0.75, 0).scale(s, s, 0.75, 0).translate(almostTheEnd.x, almostTheEnd.y).attr(attrs).attr({
9032
9299
  "stroke-linejoin": "round",
9033
9300
  "stroke-linecap": "round"
9034
9301
  });
@@ -9044,519 +9311,415 @@ GraphUtils.createGraphie = function (el) {
9044
9311
  }
9045
9312
  return path;
9046
9313
  };
9047
- const drawingTools = {
9048
- circle: function (center, radius) {
9049
- return raphael$1.ellipse(...scalePoint(center).concat(scaleVector([radius, radius])));
9050
- },
9051
- // (x, y) is coordinate of bottom left corner
9052
- rect: function (x, y, width, height) {
9053
- // Raphael needs (x, y) to be coordinate of upper left corner
9054
- const corner = scalePoint([x, y + height]);
9055
- const dims = scaleVector([width, height]);
9056
- const elem = raphael$1.rect(...corner.concat(dims));
9057
- if (graphie.isMobile) {
9058
- elem.node.style.shapeRendering = "crispEdges";
9059
- }
9060
- return elem;
9061
- },
9062
- ellipse: function (center, radii) {
9063
- return raphael$1.ellipse(...scalePoint(center).concat(scaleVector(radii)));
9064
- },
9065
- fixedEllipse: function (center,
9066
- // Different type than Coord, this is radiusX, radiusY
9067
- radii, maxScale, padding) {
9068
- // Scale point and radius
9069
- const scaledPoint = scalePoint(center);
9070
- const scaledRadii = scaleVector(radii);
9071
- const width = 2 * scaledRadii[0] * maxScale + padding;
9072
- const height = 2 * scaledRadii[1] * maxScale + padding;
9073
-
9074
- // Calculate absolute left, top
9075
- const left = scaledPoint[0] - width / 2;
9076
- const top = scaledPoint[1] - height / 2;
9077
-
9078
- // Wrap in <div>
9079
- const wrapper = document.createElement("div");
9080
- $__default["default"](wrapper).css({
9081
- position: "absolute",
9082
- width: width + "px",
9083
- height: height + "px",
9084
- left: left + "px",
9085
- top: top + "px"
9086
- });
9087
- // wrapper.setAttribute("data-graphie-type", "ellipse");
9314
+ function circle(center, radius) {
9315
+ return thisGraphie.raphael.ellipse(...thisGraphie.scalePoint(center).concat(thisGraphie.scaleVector([radius, radius])));
9316
+ }
9088
9317
 
9089
- // Create Raphael canvas
9090
- const localRaphael = raphael.exports(wrapper, width, height);
9091
- const visibleShape = localRaphael.ellipse(width / 2, height / 2, scaledRadii[0], scaledRadii[1]);
9092
- return {
9093
- wrapper: wrapper,
9094
- visibleShape: visibleShape
9095
- };
9096
- },
9097
- arc: function (center, radius, startAngle, endAngle, sector) {
9098
- startAngle = (startAngle % 360 + 360) % 360;
9099
- endAngle = (endAngle % 360 + 360) % 360;
9100
- const cent = scalePoint(center);
9101
- const radii = scaleVector(radius);
9102
- const startVector = polar(radius, startAngle);
9103
- const endVector = polar(radius, endAngle);
9104
- const startPoint = scalePoint([center[0] + startVector[0], center[1] + startVector[1]]);
9105
- const endPoint = scalePoint([center[0] + endVector[0], center[1] + endVector[1]]);
9106
- const largeAngle = ((endAngle - startAngle) % 360 + 360) % 360 > 180;
9107
- return raphael$1.path("M" + startPoint.join(" ") + "A" + radii.join(" ") + " 0 " + (
9108
- // ellipse rotation
9109
- largeAngle ? 1 : 0) + " 0 " +
9110
- // sweep flag
9111
- endPoint.join(" ") + (sector ? "L" + cent.join(" ") + "z" : ""));
9112
- },
9113
- path: function (points) {
9114
- // @ts-expect-error - TS2554 - Expected 2 arguments, but got 1.
9115
- const p = raphael$1.path(svgPath(points));
9116
- p.graphiePath = points;
9117
- return p;
9118
- },
9119
- fixedPath: function (points, center, createPath) {
9120
- points = ___default["default"].map(points, scalePoint);
9121
- center = center ? scalePoint(center) : null;
9122
- createPath = createPath || svgPath;
9123
- const pathLeft = ___default["default"].min(___default["default"].pluck(points, 0));
9124
- const pathRight = ___default["default"].max(___default["default"].pluck(points, 0));
9125
- const pathTop = ___default["default"].min(___default["default"].pluck(points, 1));
9126
- const pathBottom = ___default["default"].max(___default["default"].pluck(points, 1));
9127
-
9128
- // Apply padding to line
9129
- const padding = [4, 4];
9130
-
9131
- // Calculate and apply additional offset
9132
- const extraOffset = [pathLeft, pathTop];
9133
-
9134
- // Apply padding and offset to points
9135
- points = ___default["default"].map(points, function (point) {
9136
- return kmath.vector.add(kmath.vector.subtract(point, extraOffset), kmath.vector.scale(padding, 0.5));
9137
- });
9138
-
9139
- // Calculate <div> dimensions
9140
- const width = pathRight - pathLeft + padding[0];
9141
- const height = pathBottom - pathTop + padding[1];
9142
- const left = extraOffset[0] - padding[0] / 2;
9143
- const top = extraOffset[1] - padding[1] / 2;
9144
-
9145
- // Create <div>
9146
- const wrapper = document.createElement("div");
9147
- $__default["default"](wrapper).css({
9148
- position: "absolute",
9149
- width: width + "px",
9150
- height: height + "px",
9151
- left: left + "px",
9152
- top: top + "px",
9153
- // If user specified a center, set it
9154
- // NOTE(kevinb): jQuery doesn't like that `transformOrigin `could be `null`
9155
- // so we cast to `any` here.
9156
- transformOrigin: center ? width / 2 + center[0] + "px " + (height / 2 + center[1]) + "px" : null
9157
- });
9318
+ // (x, y) is coordinate of bottom left corner
9319
+ function rect(x, y, width, height) {
9320
+ // Raphael needs (x, y) to be coordinate of upper left corner
9321
+ const corner = thisGraphie.scalePoint([x, y + height]);
9322
+ const dims = thisGraphie.scaleVector([width, height]);
9323
+ const elem = thisGraphie.raphael.rect(...corner.concat(dims));
9324
+ if (thisGraphie.isMobile) {
9325
+ elem.node.style.shapeRendering = "crispEdges";
9326
+ }
9327
+ return elem;
9328
+ }
9329
+ function ellipse(center, radii) {
9330
+ return thisGraphie.raphael.ellipse(...thisGraphie.scalePoint(center).concat(thisGraphie.scaleVector(radii)));
9331
+ }
9332
+ function fixedEllipse(center,
9333
+ // Different type than Coord, this is radiusX, radiusY
9334
+ radii, maxScale, padding) {
9335
+ // Scale point and radius
9336
+ const scaledPoint = thisGraphie.scalePoint(center);
9337
+ const scaledRadii = thisGraphie.scaleVector(radii);
9338
+ const width = 2 * scaledRadii[0] * maxScale + padding;
9339
+ const height = 2 * scaledRadii[1] * maxScale + padding;
9340
+
9341
+ // Calculate absolute left, top
9342
+ const left = scaledPoint[0] - width / 2;
9343
+ const top = scaledPoint[1] - height / 2;
9344
+
9345
+ // Wrap in <div>
9346
+ const wrapper = document.createElement("div");
9347
+ $__default["default"](wrapper).css({
9348
+ position: "absolute",
9349
+ width: width + "px",
9350
+ height: height + "px",
9351
+ left: left + "px",
9352
+ top: top + "px"
9353
+ });
9354
+ // wrapper.setAttribute("data-graphie-type", "ellipse");
9158
9355
 
9159
- // Create Raphael canvas
9160
- const localRaphael = raphael.exports(wrapper, width, height);
9356
+ // Create Raphael canvas
9357
+ const localRaphael = raphael.exports(wrapper, width, height);
9358
+ const visibleShape = localRaphael.ellipse(width / 2, height / 2, scaledRadii[0], scaledRadii[1]);
9359
+ return {
9360
+ wrapper: wrapper,
9361
+ visibleShape: visibleShape
9362
+ };
9363
+ }
9364
+ function arc(center, radius, startAngle, endAngle, sector) {
9365
+ startAngle = (startAngle % 360 + 360) % 360;
9366
+ endAngle = (endAngle % 360 + 360) % 360;
9367
+ const cent = thisGraphie.scalePoint(center);
9368
+ const radii = thisGraphie.scaleVector(radius);
9369
+ const startVector = polar(radius, startAngle);
9370
+ const endVector = polar(radius, endAngle);
9371
+ const startPoint = thisGraphie.scalePoint([center[0] + startVector[0], center[1] + startVector[1]]);
9372
+ const endPoint = thisGraphie.scalePoint([center[0] + endVector[0], center[1] + endVector[1]]);
9373
+ const largeAngle = ((endAngle - startAngle) % 360 + 360) % 360 > 180;
9374
+ return thisGraphie.raphael.path("M" + startPoint.join(" ") + "A" + radii.join(" ") + " 0 " + (
9375
+ // ellipse rotation
9376
+ largeAngle ? 1 : 0) + " 0 " +
9377
+ // sweep flag
9378
+ endPoint.join(" ") + (sector ? "L" + cent.join(" ") + "z" : ""));
9379
+ }
9380
+ function path(points) {
9381
+ // @ts-expect-error - TS2554 - Expected 2 arguments, but got 1.
9382
+ const p = thisGraphie.raphael.path(thisGraphie.svgPath(points));
9383
+ p.graphiePath = points;
9384
+ return p;
9385
+ }
9386
+ function fixedPath(points, center, createPath) {
9387
+ points = ___default["default"].map(points, thisGraphie.scalePoint);
9388
+ center = center ? thisGraphie.scalePoint(center) : null;
9389
+ createPath = createPath || thisGraphie.svgPath;
9390
+ const pathLeft = ___default["default"].min(___default["default"].pluck(points, 0));
9391
+ const pathRight = ___default["default"].max(___default["default"].pluck(points, 0));
9392
+ const pathTop = ___default["default"].min(___default["default"].pluck(points, 1));
9393
+ const pathBottom = ___default["default"].max(___default["default"].pluck(points, 1));
9394
+
9395
+ // Apply padding to line
9396
+ const padding = [4, 4];
9397
+
9398
+ // Calculate and apply additional offset
9399
+ const extraOffset = [pathLeft, pathTop];
9400
+
9401
+ // Apply padding and offset to points
9402
+ points = ___default["default"].map(points, function (point) {
9403
+ return kmath.vector.add(kmath.vector.subtract(point, extraOffset), kmath.vector.scale(padding, 0.5));
9404
+ });
9405
+
9406
+ // Calculate <div> dimensions
9407
+ const width = pathRight - pathLeft + padding[0];
9408
+ const height = pathBottom - pathTop + padding[1];
9409
+ const left = extraOffset[0] - padding[0] / 2;
9410
+ const top = extraOffset[1] - padding[1] / 2;
9411
+
9412
+ // Create <div>
9413
+ const wrapper = document.createElement("div");
9414
+ $__default["default"](wrapper).css({
9415
+ position: "absolute",
9416
+ width: width + "px",
9417
+ height: height + "px",
9418
+ left: left + "px",
9419
+ top: top + "px",
9420
+ // If user specified a center, set it
9421
+ // NOTE(kevinb): jQuery doesn't like that `transformOrigin `could be `null`
9422
+ // so we cast to `any` here.
9423
+ transformOrigin: center ? width / 2 + center[0] + "px " + (height / 2 + center[1]) + "px" : null
9424
+ });
9161
9425
 
9162
- // Calculate path
9163
- const visibleShape = localRaphael.path(createPath(points));
9164
- return {
9165
- wrapper: wrapper,
9166
- visibleShape: visibleShape
9167
- };
9168
- },
9169
- scaledPath: function (points) {
9170
- const p = raphael$1.path(svgPath(points, /* alreadyScaled */true));
9171
- p.graphiePath = points;
9172
- return p;
9173
- },
9174
- line: function (start, end) {
9175
- const l = this.path([start, end]);
9176
- if (graphie.isMobile) {
9177
- l.node.style.shapeRendering = "crispEdges";
9178
- }
9179
- return l;
9180
- },
9181
- parabola: function (a, b, c) {
9182
- // Plot a parabola of the form: f(x) = (a * x + b) * x + c
9183
- return raphael$1.path(svgParabolaPath(a, b, c));
9184
- },
9185
- fixedLine: function (start, end, thickness) {
9186
- // Apply padding to line
9187
- const padding = [thickness, thickness];
9188
-
9189
- // Scale points to get values in pixels
9190
- start = scalePoint(start);
9191
- end = scalePoint(end);
9192
-
9193
- // Calculate and apply additional offset
9194
- const extraOffset = [Math.min(start[0], end[0]), Math.min(start[1], end[1])];
9195
-
9196
- // Apply padding and offset to start, end points
9197
- start = kmath.vector.add(kmath.vector.subtract(start, extraOffset), kmath.vector.scale(padding, 0.5));
9198
- end = kmath.vector.add(kmath.vector.subtract(end, extraOffset), kmath.vector.scale(padding, 0.5));
9199
-
9200
- // Calculate <div> dimensions
9201
- const left = extraOffset[0] - padding[0] / 2;
9202
- const top = extraOffset[1] - padding[1] / 2;
9203
- const width = Math.abs(start[0] - end[0]) + padding[0];
9204
- const height = Math.abs(start[1] - end[1]) + padding[1];
9205
-
9206
- // Create <div>
9207
- const wrapper = document.createElement("div");
9208
- $__default["default"](wrapper).css({
9209
- position: "absolute",
9210
- width: width + "px",
9211
- height: height + "px",
9212
- left: left + "px",
9213
- top: top + "px",
9214
- // Outsiders should feel like the line's 'origin' (i.e., for
9215
- // rotation) is the starting point
9216
- transformOrigin: start[0] + "px " + start[1] + "px"
9217
- });
9218
-
9219
- // Create Raphael canvas
9220
- const localRaphael = raphael.exports(wrapper, width, height);
9221
-
9222
- // Calculate path
9223
- const path = "M" + start[0] + " " + start[1] + " " + "L" + end[0] + " " + end[1];
9224
- const visibleShape = localRaphael.path(path);
9225
- visibleShape.graphiePath = [start, end];
9226
- return {
9227
- wrapper: wrapper,
9228
- visibleShape: visibleShape
9229
- };
9230
- },
9231
- sinusoid: function (a, b, c, d) {
9232
- // Plot a sinusoid of the form: f(x) = a * sin(b * x - c) + d
9233
- return raphael$1.path(svgSinusoidPath(a, b, c, d));
9234
- },
9235
- grid: function (xr, yr) {
9236
- const step = currentStyle.step || [1, 1];
9237
- const set = raphael$1.set();
9238
- let x = step[0] * Math.ceil(xr[0] / step[0]);
9239
- for (; x <= xr[1]; x += step[0]) {
9240
- set.push(this.line([x, yr[0]], [x, yr[1]]));
9241
- }
9242
- let y = step[1] * Math.ceil(yr[0] / step[1]);
9243
- for (; y <= yr[1]; y += step[1]) {
9244
- set.push(this.line([xr[0], y], [xr[1], y]));
9245
- }
9246
- return set;
9247
- },
9248
- label: function (point, text, direction, latex) {
9249
- latex = typeof latex === "undefined" || latex;
9250
- const $span = $__default["default"]("<span>").addClass("graphie-label");
9251
- const pad = currentStyle["label-distance"];
9252
- $span.css($__default["default"].extend({}, {
9253
- position: "absolute",
9254
- padding: (pad != null ? pad : 7) + "px",
9255
- color: "black"
9256
- })).data("labelDirection", direction).appendTo(el);
9257
-
9258
- // @ts-expect-error - TS2339 - Property 'setPosition' does not exist on type 'JQuery<HTMLElement>'.
9259
- $span.setPosition = function (point) {
9260
- const scaledPoint = scalePoint(point);
9261
- $span.css({
9262
- left: scaledPoint[0],
9263
- top: scaledPoint[1]
9264
- });
9265
- };
9426
+ // Create Raphael canvas
9427
+ const localRaphael = raphael.exports(wrapper, width, height);
9266
9428
 
9267
- // @ts-expect-error - TS2339 - Property 'setPosition' does not exist on type 'JQuery<HTMLElement>'.
9268
- $span.setPosition(point);
9269
- const span = $span[0];
9429
+ // Calculate path
9430
+ const visibleShape = localRaphael.path(createPath(points));
9431
+ return {
9432
+ wrapper: wrapper,
9433
+ visibleShape: visibleShape
9434
+ };
9435
+ }
9436
+ function scaledPath(points) {
9437
+ const p = thisGraphie.raphael.path(thisGraphie.svgPath(points, /* alreadyScaled */true));
9438
+ p.graphiePath = points;
9439
+ return p;
9440
+ }
9441
+ function line(start, end) {
9442
+ const l = path([start, end]);
9443
+ if (thisGraphie.isMobile) {
9444
+ l.node.style.shapeRendering = "crispEdges";
9445
+ }
9446
+ return l;
9447
+ }
9448
+ function parabola(a, b, c) {
9449
+ // Plot a parabola of the form: f(x) = (a * x + b) * x + c
9450
+ return thisGraphie.raphael.path(thisGraphie.svgParabolaPath(a, b, c));
9451
+ }
9452
+ function fixedLine(start, end, thickness) {
9453
+ // Apply padding to line
9454
+ const padding = [thickness, thickness];
9270
9455
 
9271
- // @ts-expect-error - TS2339 - Property 'processMath' does not exist on type 'JQuery<HTMLElement>'.
9272
- $span.processMath = function (math, force) {
9273
- processMath$1(span, math, force, function () {
9274
- const width = span.scrollWidth;
9275
- const height = span.scrollHeight;
9276
- setLabelMargins(span, [width, height]);
9277
- });
9278
- };
9456
+ // Scale points to get values in pixels
9457
+ start = thisGraphie.scalePoint(start);
9458
+ end = thisGraphie.scalePoint(end);
9279
9459
 
9280
- // @ts-expect-error - TS2339 - Property 'processText' does not exist on type 'JQuery<HTMLElement>'.
9281
- $span.processText = function (text) {
9282
- $span.html(text);
9460
+ // Calculate and apply additional offset
9461
+ const extraOffset = [Math.min(start[0], end[0]), Math.min(start[1], end[1])];
9462
+
9463
+ // Apply padding and offset to start, end points
9464
+ start = kmath.vector.add(kmath.vector.subtract(start, extraOffset), kmath.vector.scale(padding, 0.5));
9465
+ end = kmath.vector.add(kmath.vector.subtract(end, extraOffset), kmath.vector.scale(padding, 0.5));
9466
+
9467
+ // Calculate <div> dimensions
9468
+ const left = extraOffset[0] - padding[0] / 2;
9469
+ const top = extraOffset[1] - padding[1] / 2;
9470
+ const width = Math.abs(start[0] - end[0]) + padding[0];
9471
+ const height = Math.abs(start[1] - end[1]) + padding[1];
9472
+
9473
+ // Create <div>
9474
+ const wrapper = document.createElement("div");
9475
+ $__default["default"](wrapper).css({
9476
+ position: "absolute",
9477
+ width: width + "px",
9478
+ height: height + "px",
9479
+ left: left + "px",
9480
+ top: top + "px",
9481
+ // Outsiders should feel like the line's 'origin' (i.e., for
9482
+ // rotation) is the starting point
9483
+ transformOrigin: start[0] + "px " + start[1] + "px"
9484
+ });
9485
+
9486
+ // Create Raphael canvas
9487
+ const localRaphael = raphael.exports(wrapper, width, height);
9488
+
9489
+ // Calculate path
9490
+ const path = "M" + start[0] + " " + start[1] + " " + "L" + end[0] + " " + end[1];
9491
+ const visibleShape = localRaphael.path(path);
9492
+ visibleShape.graphiePath = [start, end];
9493
+ return {
9494
+ wrapper: wrapper,
9495
+ visibleShape: visibleShape
9496
+ };
9497
+ }
9498
+ function sinusoid(a, b, c, d) {
9499
+ // Plot a sinusoid of the form: f(x) = a * sin(b * x - c) + d
9500
+ return thisGraphie.raphael.path(thisGraphie.svgSinusoidPath(a, b, c, d));
9501
+ }
9502
+ function grid(xr, yr) {
9503
+ const step = thisGraphie.currentStyle.step || [1, 1];
9504
+ const set = thisGraphie.raphael.set();
9505
+ let x = step[0] * Math.ceil(xr[0] / step[0]);
9506
+ for (; x <= xr[1]; x += step[0]) {
9507
+ set.push(line([x, yr[0]], [x, yr[1]]));
9508
+ }
9509
+ let y = step[1] * Math.ceil(yr[0] / step[1]);
9510
+ for (; y <= yr[1]; y += step[1]) {
9511
+ set.push(line([xr[0], y], [xr[1], y]));
9512
+ }
9513
+ return set;
9514
+ }
9515
+ function label(point, text, direction, latex) {
9516
+ latex = typeof latex === "undefined" || latex;
9517
+ const $span = $__default["default"]("<span>").addClass("graphie-label");
9518
+ const pad = thisGraphie.currentStyle["label-distance"];
9519
+ $span.css($__default["default"].extend({}, {
9520
+ position: "absolute",
9521
+ padding: (pad != null ? pad : 7) + "px",
9522
+ color: "black"
9523
+ })).data("labelDirection", direction).appendTo(thisGraphie.el);
9524
+
9525
+ // @ts-expect-error - TS2339 - Property 'setPosition' does not exist on type 'JQuery<HTMLElement>'.
9526
+ $span.setPosition = function (point) {
9527
+ const scaledPoint = thisGraphie.scalePoint(point);
9528
+ $span.css({
9529
+ left: scaledPoint[0],
9530
+ top: scaledPoint[1]
9531
+ });
9532
+ };
9533
+
9534
+ // @ts-expect-error - TS2339 - Property 'setPosition' does not exist on type 'JQuery<HTMLElement>'.
9535
+ $span.setPosition(point);
9536
+ const span = $span[0];
9537
+
9538
+ // @ts-expect-error - TS2339 - Property 'processMath' does not exist on type 'JQuery<HTMLElement>'.
9539
+ $span.processMath = function (math, force) {
9540
+ processMath$1(span, math, force, function () {
9283
9541
  const width = span.scrollWidth;
9284
9542
  const height = span.scrollHeight;
9285
9543
  setLabelMargins(span, [width, height]);
9286
- };
9287
- if (latex) {
9288
- // @ts-expect-error - TS2339 - Property 'processMath' does not exist on type 'JQuery<HTMLElement>'.
9289
- $span.processMath(text, /* force */false);
9290
- } else {
9291
- // @ts-expect-error - TS2339 - Property 'processText' does not exist on type 'JQuery<HTMLElement>'.
9292
- $span.processText(text);
9293
- }
9294
- return $span;
9295
- },
9296
- plotParametric: function (fn, range, shade, fn2) {
9297
- // Note: fn2 should only be set if 'shade' is true, as it denotes
9298
- // the function between which fn should have its area shaded.
9299
- // In general, plotParametric shouldn't be used to shade the area
9300
- // between two arbitrary parametrics functions over an interval,
9301
- // as the method assumes that fn and fn2 are both of the form
9302
- // fn(t) = (t, fn'(t)) for some initial fn'.
9303
- fn2 = fn2 || function (t) {
9304
- return [t, 0];
9305
- };
9544
+ });
9545
+ };
9306
9546
 
9307
- // We truncate to 500,000, since anything bigger causes
9308
- // overflow in the firefox svg renderer. This is safe
9309
- // since 500,000 is outside the viewport anyway. We
9310
- // write these functions the way we do to handle undefined.
9311
- const clipper = xy => {
9312
- if (Math.abs(xy[1]) > 500000) {
9313
- return [xy[0], Math.min(Math.max(xy[1], -500000), 500000)];
9314
- }
9315
- return xy;
9316
- };
9317
- const clippedFn = x => clipper(fn(x));
9318
- const clippedFn2 = x => clipper(fn2(x));
9319
- if (!currentStyle.strokeLinejoin) {
9320
- currentStyle.strokeLinejoin = "round";
9321
- }
9322
- if (!currentStyle.strokeLinecap) {
9323
- currentStyle.strokeLinecap = "round";
9324
- }
9325
- const min = range[0];
9326
- const max = range[1];
9327
- let step = (max - min) / (currentStyle["plot-points"] || 800);
9328
- if (step === 0) {
9329
- step = 1;
9330
- }
9331
- const paths = raphael$1.set();
9332
- let points = [];
9333
- let lastDiff = GraphUtils.coordDiff(clippedFn(min), clippedFn2(min));
9334
- let lastFlip = min;
9335
- for (let t = min; t <= max; t += step) {
9336
- const top = clippedFn(t);
9337
- const bottom = clippedFn2(t);
9338
- const diff = GraphUtils.coordDiff(top, bottom);
9339
-
9340
- // Find points where it flips
9341
- // Create path that sketches area between the two functions
9342
- if (
9343
- // if there is an asymptote here, meaning that the graph
9344
- // switches signs and has a large difference
9345
- diff[1] < 0 !== lastDiff[1] < 0 && Math.abs(diff[1] - lastDiff[1]) > 2 * yScale ||
9346
- // or the function is undefined
9347
- isNaN(diff[1])) {
9348
- // split the path at this point, and draw it
9349
- if (shade) {
9350
- // @ts-expect-error - TS2345 - Argument of type 'any' is not assignable to parameter of type 'never'.
9351
- points.push(top);
9547
+ // @ts-expect-error - TS2339 - Property 'processText' does not exist on type 'JQuery<HTMLElement>'.
9548
+ $span.processText = function (text) {
9549
+ $span.html(text);
9550
+ const width = span.scrollWidth;
9551
+ const height = span.scrollHeight;
9552
+ setLabelMargins(span, [width, height]);
9553
+ };
9554
+ if (latex) {
9555
+ // @ts-expect-error - TS2339 - Property 'processMath' does not exist on type 'JQuery<HTMLElement>'.
9556
+ $span.processMath(text, /* force */false);
9557
+ } else {
9558
+ // @ts-expect-error - TS2339 - Property 'processText' does not exist on type 'JQuery<HTMLElement>'.
9559
+ $span.processText(text);
9560
+ }
9561
+ return $span;
9562
+ }
9563
+ function plotParametric(fn, range, shade) {
9564
+ let fn2 = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : t => [t, 0];
9565
+ // Note: fn2 should only be set if 'shade' is true, as it denotes
9566
+ // the function between which fn should have its area shaded.
9567
+ // In general, plotParametric shouldn't be used to shade the area
9568
+ // between two arbitrary parametrics functions over an interval,
9569
+ // as the method assumes that fn and fn2 are both of the form
9570
+ // fn(t) = (t, fn'(t)) for some initial fn'.
9571
+
9572
+ // We truncate to 500,000, since anything bigger causes
9573
+ // overflow in the firefox svg renderer. This is safe
9574
+ // since 500,000 is outside the viewport anyway. We
9575
+ // write these functions the way we do to handle undefined.
9576
+ const clipper = xy => {
9577
+ if (Math.abs(xy[1]) > 500000) {
9578
+ return [xy[0], Math.min(Math.max(xy[1], -500000), 500000)];
9579
+ }
9580
+ return xy;
9581
+ };
9582
+ const clippedFn = x => clipper(fn(x));
9583
+ const clippedFn2 = x => clipper(fn2(x));
9584
+ if (!thisGraphie.currentStyle.strokeLinejoin) {
9585
+ thisGraphie.currentStyle.strokeLinejoin = "round";
9586
+ }
9587
+ if (!thisGraphie.currentStyle.strokeLinecap) {
9588
+ thisGraphie.currentStyle.strokeLinecap = "round";
9589
+ }
9590
+ const min = range[0];
9591
+ const max = range[1];
9592
+ let step = (max - min) / (thisGraphie.currentStyle["plot-points"] || 800);
9593
+ if (step === 0) {
9594
+ step = 1;
9595
+ }
9596
+ const paths = thisGraphie.raphael.set();
9597
+ let points = [];
9598
+ let lastDiff = GraphUtils.coordDiff(clippedFn(min), clippedFn2(min));
9599
+ let lastFlip = min;
9600
+ for (let t = min; t <= max; t += step) {
9601
+ const top = clippedFn(t);
9602
+ const bottom = clippedFn2(t);
9603
+ const diff = GraphUtils.coordDiff(top, bottom);
9604
+
9605
+ // Find points where it flips
9606
+ // Create path that sketches area between the two functions
9607
+ if (
9608
+ // if there is an asymptote here, meaning that the graph
9609
+ // switches signs and has a large difference
9610
+ diff[1] < 0 !== lastDiff[1] < 0 && Math.abs(diff[1] - lastDiff[1]) > 2 * thisGraphie.drawingTransform().pixelsPerUnitY() ||
9611
+ // or the function is undefined
9612
+ isNaN(diff[1])) {
9613
+ // split the path at this point, and draw it
9614
+ if (shade) {
9615
+ // @ts-expect-error - TS2345 - Argument of type 'any' is not assignable to parameter of type 'never'.
9616
+ points.push(top);
9352
9617
 
9353
- // backtrack to draw paired function
9354
- for (let u = t - step; u >= lastFlip; u -= step) {
9355
- // @ts-expect-error - TS2345 - Argument of type 'any' is not assignable to parameter of type 'never'.
9356
- points.push(clippedFn2(u));
9357
- }
9358
- lastFlip = t;
9359
- }
9360
- paths.push(this.path(points));
9361
- // restart the path, excluding this point
9362
- points = [];
9363
- if (shade) {
9618
+ // backtrack to draw paired function
9619
+ for (let u = t - step; u >= lastFlip; u -= step) {
9364
9620
  // @ts-expect-error - TS2345 - Argument of type 'any' is not assignable to parameter of type 'never'.
9365
- points.push(top);
9621
+ points.push(clippedFn2(u));
9366
9622
  }
9367
- } else {
9368
- // otherwise, just add the point to the path
9369
- // @ts-expect-error - TS2345 - Argument of type 'any' is not assignable to parameter of type 'never'.
9370
- points.push(top);
9623
+ lastFlip = t;
9371
9624
  }
9372
- lastDiff = diff;
9373
- }
9374
- if (shade) {
9375
- // backtrack to draw paired function
9376
- for (let u = max - step; u >= lastFlip; u -= step) {
9625
+ paths.push(path(points));
9626
+ // restart the path, excluding this point
9627
+ points = [];
9628
+ if (shade) {
9377
9629
  // @ts-expect-error - TS2345 - Argument of type 'any' is not assignable to parameter of type 'never'.
9378
- points.push(clippedFn2(u));
9630
+ points.push(top);
9379
9631
  }
9632
+ } else {
9633
+ // otherwise, just add the point to the path
9634
+ // @ts-expect-error - TS2345 - Argument of type 'any' is not assignable to parameter of type 'never'.
9635
+ points.push(top);
9380
9636
  }
9381
- paths.push(this.path(points));
9382
- return paths;
9383
- },
9384
- plotPolar: function (fn, range) {
9385
- const min = range[0];
9386
- const max = range[1];
9387
-
9388
- // There is probably a better heuristic for this
9389
- if (!currentStyle["plot-points"]) {
9390
- currentStyle["plot-points"] = 2 * (max - min) * xScale;
9391
- }
9392
- return this.plotParametric(function (th) {
9393
- return polar(fn(th), th * 180 / Math.PI);
9394
- }, range);
9395
- },
9396
- plot: function (fn, range, swapAxes, shade, fn2) {
9397
- const min = range[0];
9398
- const max = range[1];
9399
- if (!currentStyle["plot-points"]) {
9400
- currentStyle["plot-points"] = 2 * (max - min) * xScale;
9401
- }
9402
- if (swapAxes) {
9403
- if (fn2) {
9404
- // TODO(charlie): support swapped axis area shading
9405
- throw new PerseusError("Can't shade area between functions with swapped axes.", Errors.Internal);
9406
- }
9407
- return this.plotParametric(function (y) {
9408
- return [fn(y), y];
9409
- }, range, shade);
9637
+ lastDiff = diff;
9638
+ }
9639
+ if (shade) {
9640
+ // backtrack to draw paired function
9641
+ for (let u = max - step; u >= lastFlip; u -= step) {
9642
+ // @ts-expect-error - TS2345 - Argument of type 'any' is not assignable to parameter of type 'never'.
9643
+ points.push(clippedFn2(u));
9410
9644
  }
9645
+ }
9646
+ paths.push(path(points));
9647
+ return paths;
9648
+ }
9649
+ function plot(fn, range, swapAxes, shade, fn2) {
9650
+ const min = range[0];
9651
+ const max = range[1];
9652
+ if (!thisGraphie.currentStyle["plot-points"]) {
9653
+ thisGraphie.currentStyle["plot-points"] = 2 * (max - min) * thisGraphie.drawingTransform().pixelsPerUnitX();
9654
+ }
9655
+ if (swapAxes) {
9411
9656
  if (fn2) {
9412
- if (shade) {
9413
- return this.plotParametric(function (x) {
9414
- return [x, fn(x)];
9415
- }, range, shade, function (x) {
9416
- return [x, fn2(x)];
9417
- });
9418
- }
9419
- throw new PerseusError("fn2 should only be set when 'shade' is True.", Errors.Internal);
9657
+ // TODO(charlie): support swapped axis area shading
9658
+ throw new PerseusError("Can't shade area between functions with swapped axes.", Errors.Internal);
9420
9659
  }
9421
- return this.plotParametric(function (x) {
9422
- return [x, fn(x)];
9660
+ return plotParametric(function (y) {
9661
+ return [fn(y), y];
9423
9662
  }, range, shade);
9424
- },
9425
- /**
9426
- * Given a piecewise function, return a Raphael set of paths that
9427
- * can be used to draw the function, e.g. using style().
9428
- * Calls plotParametric.
9429
- *
9430
- * @param {[]} fnArray array of functions which when called
9431
- * with a parameter i return the value of
9432
- * the function at i
9433
- * @param {[]} rangeArray array of ranges over which the
9434
- * corresponding functions are defined
9435
- * @return {Set<any>} set of paths
9436
- */
9437
- plotPiecewise: function (fnArray, rangeArray) {
9438
- const paths = raphael$1.set();
9439
- const self = this;
9440
- ___default["default"].times(fnArray.length, function (i) {
9441
- const fn = fnArray[i];
9442
- const range = rangeArray[i];
9443
- const fnPaths = self.plotParametric(function (x) {
9663
+ }
9664
+ if (fn2) {
9665
+ if (shade) {
9666
+ return plotParametric(function (x) {
9444
9667
  return [x, fn(x)];
9445
- }, range);
9446
- ___default["default"].each(fnPaths, function (fnPath) {
9447
- paths.push(fnPath);
9668
+ }, range, shade, function (x) {
9669
+ return [x, fn2(x)];
9448
9670
  });
9449
- });
9450
- return paths;
9451
- },
9452
- /**
9453
- * Given an array of coordinates of the form [x, y], create and
9454
- * return a Raphael set of Raphael circle objects at those
9455
- * coordinates
9456
- *
9457
- * @param {Array<[number, number]>} endpointArray
9458
- * @return {Set<any>} set of circles
9459
- */
9460
- plotEndpointCircles: function (endpointArray) {
9461
- const circles = raphael$1.set();
9462
- const self = this;
9463
- ___default["default"].each(endpointArray, function (coord, i) {
9464
- circles.push(self.circle(coord, 0.15));
9465
- });
9466
- return circles;
9467
- },
9468
- plotAsymptotes: function (fn, range) {
9469
- const min = range[0];
9470
- const max = range[1];
9471
- const step = (max - min) / (currentStyle["plot-points"] || 800);
9472
- const asymptotes = raphael$1.set();
9473
- let lastVal = fn(min);
9474
- for (let t = min; t <= max; t += step) {
9475
- const funcVal = fn(t);
9476
- if (funcVal < 0 !== lastVal < 0 && Math.abs(funcVal - lastVal) > 2 * yScale) {
9477
- asymptotes.push(this.line([t, yScale], [t, -yScale]));
9478
- }
9479
- lastVal = funcVal;
9480
- }
9481
- return asymptotes;
9482
- }
9483
- };
9484
- const graphie = new Graphie$1();
9485
- ___default["default"].extend(graphie, {
9486
- raphael: raphael$1,
9487
- init: function (options) {
9488
- let scale = options.scale || [40, 40];
9489
- scale = typeof scale === "number" ? [scale, scale] : scale;
9490
- xScale = scale[0];
9491
- yScale = scale[1];
9492
- if (options.range == null) {
9493
- throw new PerseusError("range should be specified in graph init", Errors.Internal);
9494
- }
9495
- xRange = options.range[0];
9496
- yRange = options.range[1];
9497
- const w = (xRange[1] - xRange[0]) * xScale;
9498
- const h = (yRange[1] - yRange[0]) * yScale;
9499
- raphael$1.setSize(w, h);
9500
- $__default["default"](el).css({
9501
- width: w,
9502
- height: h
9503
- });
9504
- this.range = options.range;
9505
- this.scale = scale;
9506
- this.dimensions = [w, h];
9507
- this.xpixels = w;
9508
- this.ypixels = h;
9509
- this.isMobile = options.isMobile;
9510
- return this;
9511
- },
9512
- style: function (attrs, fn) {
9513
- const processed = processAttributes(attrs);
9514
- if (typeof fn === "function") {
9515
- const oldStyle = currentStyle;
9516
- currentStyle = $__default["default"].extend({}, currentStyle, processed);
9517
- const result = fn.call(graphie);
9518
- currentStyle = oldStyle;
9519
- return result;
9520
9671
  }
9521
- $__default["default"].extend(currentStyle, processed);
9522
- },
9523
- scalePoint: scalePoint,
9524
- scaleVector: scaleVector,
9525
- unscalePoint: unscalePoint,
9526
- unscaleVector: unscaleVector,
9527
- // Custom SVG path functions that are dependent on graphie range
9528
- // `svgPath`, while independent of range, is exported for consistency
9529
- svgPath: svgPath,
9530
- svgParabolaPath: svgParabolaPath,
9531
- svgSinusoidPath: svgSinusoidPath
9532
- });
9533
- $__default["default"].each(drawingTools, function (name) {
9534
- graphie[name] = function () {
9672
+ throw new PerseusError("fn2 should only be set when 'shade' is True.", Errors.Internal);
9673
+ }
9674
+ return plotParametric(function (x) {
9675
+ return [x, fn(x)];
9676
+ }, range, shade);
9677
+ }
9678
+ const drawingTools = {
9679
+ circle,
9680
+ rect,
9681
+ ellipse,
9682
+ fixedEllipse,
9683
+ arc,
9684
+ path,
9685
+ fixedPath,
9686
+ scaledPath,
9687
+ line,
9688
+ parabola,
9689
+ fixedLine,
9690
+ sinusoid,
9691
+ grid,
9692
+ label,
9693
+ plotParametric,
9694
+ plot
9695
+ };
9696
+ function graphify(drawingFn) {
9697
+ return function () {
9535
9698
  for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
9536
9699
  args[_key] = arguments[_key];
9537
9700
  }
9538
9701
  const last = args[args.length - 1];
9539
- const oldStyle = currentStyle;
9702
+ const oldStyle = thisGraphie.currentStyle;
9540
9703
  let result;
9541
9704
 
9542
9705
  // The last argument is probably trying to change the style
9543
9706
  if (typeof last === "object" && !___default["default"].isArray(last)) {
9544
- currentStyle = {
9545
- ...currentStyle,
9546
- ...processAttributes(last)
9707
+ thisGraphie.currentStyle = {
9708
+ ...thisGraphie.currentStyle,
9709
+ ...thisGraphie.processAttributes(last)
9547
9710
  };
9548
9711
  const rest = [].slice.call(args, 0, args.length - 1);
9549
- result = drawingTools[name](...rest);
9712
+ result = drawingFn(...rest);
9550
9713
  } else {
9551
- currentStyle = $__default["default"].extend({}, currentStyle);
9552
- result = drawingTools[name](...args);
9714
+ thisGraphie.currentStyle = $__default["default"].extend({}, thisGraphie.currentStyle);
9715
+ result = drawingFn(...args);
9553
9716
  }
9554
9717
 
9555
9718
  // Bad heuristic for recognizing Raphael elements and sets
9556
9719
  const type = result.constructor.prototype;
9557
9720
  if (type === raphael.exports.el || type === raphael.exports.st) {
9558
- result.attr(currentStyle);
9559
- if (currentStyle.arrows) {
9721
+ result.attr(thisGraphie.currentStyle);
9722
+ if (thisGraphie.currentStyle.arrows) {
9560
9723
  result = addArrowheads(result);
9561
9724
  }
9562
9725
  } else if (result instanceof $__default["default"]) {
@@ -9564,256 +9727,18 @@ GraphUtils.createGraphie = function (el) {
9564
9727
  // does not contain SVG.
9565
9728
  // @ts-expect-error - TS2339 - Property 'css' does not exist on type '{}'.
9566
9729
  result.css({
9567
- ...currentStyle,
9730
+ ...thisGraphie.currentStyle,
9568
9731
  ...SVG_SPECIFIC_STYLE_MASK
9569
9732
  });
9570
9733
  }
9571
- currentStyle = oldStyle;
9734
+ thisGraphie.currentStyle = oldStyle;
9572
9735
  return result;
9573
9736
  };
9737
+ }
9738
+ $__default["default"].each(drawingTools, function (name) {
9739
+ thisGraphie[name] = graphify(drawingTools[name]);
9574
9740
  });
9575
-
9576
- // Initializes graphie settings for a graph and draws the basic graph
9577
- // features (axes, grid, tick marks, and axis labels)
9578
- // Options expected are:
9579
- // - range: [[a, b], [c, d]] or [a, b]
9580
- // - scale: [a, b] or number
9581
- // - gridOpacity: number (0 - 1)
9582
- // - gridStep: [a, b] or number (relative to units)
9583
- // - tickStep: [a, b] or number (relative to grid steps)
9584
- // - tickLen: [a, b] or number (in pixels)
9585
- // - labelStep: [a, b] or number (relative to tick steps)
9586
- // - yLabelFormat: fn to format label string for y-axis
9587
- // - xLabelFormat: fn to format label string for x-axis
9588
- // - smartLabelPositioning: true or false to ignore minus sign
9589
- graphie.graphInit = function (options) {
9590
- options = options || {};
9591
- $__default["default"].each(options, function (prop, val) {
9592
- // allow options to be specified by a single number for shorthand if
9593
- // the horizontal and vertical components are the same
9594
- if (
9595
- // @ts-expect-error - TS2339 - Property 'match' does not exist on type 'string | number | symbol'.
9596
- !prop.match(/.*Opacity$/) && prop !== "range" && typeof val === "number") {
9597
- options[prop] = [val, val];
9598
- }
9599
-
9600
- // allow symmetric ranges to be specified by the absolute values
9601
- if (prop === "range" || prop === "gridRange") {
9602
- if (val.constructor === Array) {
9603
- // but don't mandate symmetric ranges
9604
- if (val[0].constructor !== Array) {
9605
- options[prop] = [[-val[0], val[0]], [-val[1], val[1]]];
9606
- }
9607
- } else if (typeof val === "number") {
9608
- options[prop] = [[-val, val], [-val, val]];
9609
- }
9610
- }
9611
- });
9612
- const range = options.range || [[-10, 10], [-10, 10]];
9613
- const gridRange = options.gridRange || options.range;
9614
- const scale = options.scale || [20, 20];
9615
- const grid = options.grid != null ? options.grid : true;
9616
- const gridOpacity = options.gridOpacity || 0.1;
9617
- const gridStep = options.gridStep || [1, 1];
9618
- const axes = options.axes != null ? options.axes : true;
9619
- const axisArrows = options.axisArrows || "";
9620
- const axisOpacity = options.axisOpacity || 1.0;
9621
- 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])];
9622
- const axisLabels = options.axisLabels != null ? options.axisLabels : false;
9623
- const ticks = options.ticks != null ? options.ticks : true;
9624
- const tickStep = options.tickStep || [2, 2];
9625
- const tickLen = options.tickLen || [5, 5];
9626
- const tickOpacity = options.tickOpacity || 1.0;
9627
- const labels = options.labels || options.labelStep || false;
9628
- const labelStep = options.labelStep || [1, 1];
9629
- const labelOpacity = options.labelOpacity || 1.0;
9630
- let unityLabels = options.unityLabels || false;
9631
- const labelFormat = options.labelFormat || function (a) {
9632
- return a;
9633
- };
9634
- let xLabelFormat = options.xLabelFormat || labelFormat;
9635
- let yLabelFormat = options.yLabelFormat || labelFormat;
9636
- const smartLabelPositioning = options.smartLabelPositioning != null ? options.smartLabelPositioning : true;
9637
- 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)]];
9638
- if (!___default["default"].isArray(unityLabels)) {
9639
- unityLabels = [unityLabels, unityLabels];
9640
- }
9641
- if (smartLabelPositioning) {
9642
- const minusIgnorer = function (lf) {
9643
- return function (a) {
9644
- return (lf(a) + "").replace(/-(\d)/g, "\\llap{-}$1");
9645
- };
9646
- };
9647
- xLabelFormat = minusIgnorer(xLabelFormat);
9648
- yLabelFormat = minusIgnorer(yLabelFormat);
9649
- }
9650
- this.init({
9651
- range: realRange,
9652
- scale: scale,
9653
- isMobile: options.isMobile
9654
- });
9655
-
9656
- // draw grid
9657
- if (grid) {
9658
- this.grid(gridRange[0], gridRange[1], {
9659
- stroke: options.isMobile ? KhanColors.GRAY_C : "#000000",
9660
- opacity: options.isMobile ? 1 : gridOpacity,
9661
- step: gridStep,
9662
- strokeWidth: options.isMobile ? 1 : 2
9663
- });
9664
- }
9665
-
9666
- // draw axes
9667
- if (axes) {
9668
- // this is a slight hack until <-> arrowheads work
9669
- if (axisArrows === "<->" || axisArrows === true) {
9670
- this.style({
9671
- stroke: options.isMobile ? KhanColors.GRAY_G : "#000000",
9672
- opacity: options.isMobile ? 1 : axisOpacity,
9673
- strokeWidth: options.isMobile ? 1 : 2,
9674
- arrows: "->"
9675
- }, function () {
9676
- if (range[1][0] < 0 && range[1][1] > 0) {
9677
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
9678
- this.path([axisCenter, [gridRange[0][0], axisCenter[1]]]);
9679
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
9680
- this.path([axisCenter, [gridRange[0][1], axisCenter[1]]]);
9681
- }
9682
- if (range[0][0] < 0 && range[0][1] > 0) {
9683
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
9684
- this.path([axisCenter, [axisCenter[0], gridRange[1][0]]]);
9685
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
9686
- this.path([axisCenter, [axisCenter[0], gridRange[1][1]]]);
9687
- }
9688
- });
9689
-
9690
- // also, we don't support "<-" arrows yet, but why you
9691
- // would want that on your graph is beyond me.
9692
- } else if (axisArrows === "->" || axisArrows === "") {
9693
- this.style({
9694
- stroke: "#000000",
9695
- opacity: axisOpacity,
9696
- strokeWidth: 2,
9697
- arrows: axisArrows
9698
- }, function () {
9699
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
9700
- this.path([[gridRange[0][0], axisCenter[1]], [gridRange[0][1], axisCenter[1]]]);
9701
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
9702
- this.path([[axisCenter[0], gridRange[1][0]], [axisCenter[0], gridRange[1][1]]]);
9703
- });
9704
- }
9705
- if (axisLabels && axisLabels.length === 2) {
9706
- this.label([gridRange[0][1], axisCenter[1]], axisLabels[0], "right");
9707
- this.label([axisCenter[0], gridRange[1][1]], axisLabels[1], "above");
9708
- }
9709
- }
9710
-
9711
- // draw tick marks
9712
- if (ticks) {
9713
- const halfWidthTicks = options.isMobile;
9714
- this.style({
9715
- stroke: options.isMobile ? KhanColors.GRAY_G : "#000000",
9716
- opacity: options.isMobile ? 1 : tickOpacity,
9717
- strokeWidth: 1
9718
- }, function () {
9719
- // horizontal axis
9720
- let step = gridStep[0] * tickStep[0];
9721
- let len = tickLen[0] / scale[1];
9722
- let start = gridRange[0][0];
9723
- let stop = gridRange[0][1];
9724
- if (range[1][0] < 0 && range[1][1] > 0) {
9725
- for (let x = step + axisCenter[0]; x <= stop; x += step) {
9726
- if (x < stop || !axisArrows) {
9727
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
9728
- this.line([x, -len + axisCenter[1]], [x, halfWidthTicks ? 0 : len + axisCenter[1]]);
9729
- }
9730
- }
9731
- for (let x = -step + axisCenter[0]; x >= start; x -= step) {
9732
- if (x > start || !axisArrows) {
9733
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
9734
- this.line([x, -len + axisCenter[1]], [x, halfWidthTicks ? 0 : len + axisCenter[1]]);
9735
- }
9736
- }
9737
- }
9738
-
9739
- // vertical axis
9740
- step = gridStep[1] * tickStep[1];
9741
- len = tickLen[1] / scale[0];
9742
- start = gridRange[1][0];
9743
- stop = gridRange[1][1];
9744
- if (range[0][0] < 0 && range[0][1] > 0) {
9745
- for (let y = step + axisCenter[1]; y <= stop; y += step) {
9746
- if (y < stop || !axisArrows) {
9747
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
9748
- this.line([-len + axisCenter[0], y], [halfWidthTicks ? 0 : len + axisCenter[0], y]);
9749
- }
9750
- }
9751
- for (let y = -step + axisCenter[1]; y >= start; y -= step) {
9752
- if (y > start || !axisArrows) {
9753
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
9754
- this.line([-len + axisCenter[0], y], [halfWidthTicks ? 0 : len + axisCenter[0], y]);
9755
- }
9756
- }
9757
- }
9758
- });
9759
- }
9760
-
9761
- // draw axis labels
9762
- if (labels) {
9763
- this.style({
9764
- stroke: options.isMobile ? KhanColors.GRAY_G : "#000000",
9765
- opacity: options.isMobile ? 1 : labelOpacity
9766
- }, function () {
9767
- // horizontal axis
9768
- let step = gridStep[0] * tickStep[0] * labelStep[0];
9769
- let start = gridRange[0][0];
9770
- let stop = gridRange[0][1];
9771
- const xAxisPosition = axisCenter[0] < 0 ? "above" : "below";
9772
- const yAxisPosition = axisCenter[0] < 0 ? "right" : "left";
9773
- const xShowZero = axisCenter[0] === 0 && axisCenter[1] !== 0;
9774
- const yShowZero = axisCenter[0] !== 0 && axisCenter[1] === 0;
9775
- const axisOffCenter = axisCenter[0] !== 0 || axisCenter[1] !== 0;
9776
- const showUnityX = unityLabels[0] || axisOffCenter;
9777
- const showUnityY = unityLabels[1] || axisOffCenter;
9778
-
9779
- // positive x-axis
9780
- for (let x = (xShowZero ? 0 : step) + axisCenter[0]; x <= stop; x += step) {
9781
- if (x < stop || !axisArrows) {
9782
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
9783
- this.label([x, axisCenter[1]], xLabelFormat(x), xAxisPosition);
9784
- }
9785
- }
9786
-
9787
- // negative x-axis
9788
- for (let x = -step * (showUnityX ? 1 : 2) + axisCenter[0]; x >= start; x -= step) {
9789
- if (x > start || !axisArrows) {
9790
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
9791
- this.label([x, axisCenter[1]], xLabelFormat(x), xAxisPosition);
9792
- }
9793
- }
9794
- step = gridStep[1] * tickStep[1] * labelStep[1];
9795
- start = gridRange[1][0];
9796
- stop = gridRange[1][1];
9797
-
9798
- // positive y-axis
9799
- for (let y = (yShowZero ? 0 : step) + axisCenter[1]; y <= stop; y += step) {
9800
- if (y < stop || !axisArrows) {
9801
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
9802
- this.label([axisCenter[0], y], yLabelFormat(y), yAxisPosition);
9803
- }
9804
- }
9805
-
9806
- // negative y-axis
9807
- for (let y = -step * (showUnityY ? 1 : 2) + axisCenter[1]; y >= start; y -= step) {
9808
- if (y > start || !axisArrows) {
9809
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
9810
- this.label([axisCenter[0], y], yLabelFormat(y), yAxisPosition);
9811
- }
9812
- }
9813
- });
9814
- }
9815
- };
9816
- return graphie;
9741
+ return thisGraphie;
9817
9742
  };
9818
9743
 
9819
9744
  /* eslint-disable */
@@ -13403,7 +13328,7 @@ ___default["default"].extend(MovableAngle.prototype, {
13403
13328
  let angle = GraphUtils.findAngle(newPoint, vertex);
13404
13329
  angle = Math.round((angle - snapOffset) / snap) * snap + snapOffset;
13405
13330
  const distance = GraphUtils.getDistance(newPoint, vertex);
13406
- return addPoints(vertex, graphie.polar(distance, angle));
13331
+ return addPoints(vertex, polar(distance, angle));
13407
13332
  }
13408
13333
  return true;
13409
13334
  };
@@ -18503,6 +18428,27 @@ const registerEditors = editorsToRegister => {
18503
18428
  editors[editor.widgetName] = editor;
18504
18429
  });
18505
18430
  };
18431
+
18432
+ /**
18433
+ *
18434
+ * @param name - the widget that you are trying to replace
18435
+ * @param replacementName - the name of the widget that takes its place
18436
+ *
18437
+ * e.g. replaceEditor("transformer", "always-correct") will make it so the
18438
+ * transformer widget is replaced by the always correct widget
18439
+ */
18440
+ const replaceEditor = (name, replacementName) => {
18441
+ const substituteEditor = editors[replacementName];
18442
+ if (!substituteEditor && Log) {
18443
+ const errorMsg = `Failed to replace editor ${name} with ${replacementName}`;
18444
+ Log.error(errorMsg, Errors.Internal);
18445
+ return;
18446
+ }
18447
+ editors[name] = substituteEditor;
18448
+ if (Log) {
18449
+ Log.log(`INFO: Replacing editor ${name} with ${replacementName}`);
18450
+ }
18451
+ };
18506
18452
  const getWidget = name => {
18507
18453
  // TODO(alex): Consider referring to these as renderers to avoid
18508
18454
  // overloading "widget"
@@ -18531,7 +18477,7 @@ const getVersion = name => {
18531
18477
  minor: 0
18532
18478
  };
18533
18479
  }
18534
- return null;
18480
+ return;
18535
18481
  };
18536
18482
  const getVersionVector = () => {
18537
18483
  const version = {};
@@ -18815,6 +18761,7 @@ var widgets$1 = /*#__PURE__*/Object.freeze({
18815
18761
  registerWidgets: registerWidgets,
18816
18762
  replaceWidget: replaceWidget,
18817
18763
  registerEditors: registerEditors,
18764
+ replaceEditor: replaceEditor,
18818
18765
  getWidget: getWidget,
18819
18766
  getEditor: getEditor,
18820
18767
  getTransform: getTransform,
@@ -24505,7 +24452,7 @@ var Definition$1 = {
24505
24452
 
24506
24453
  // The props are type `any` on purpose so that this can receive props
24507
24454
  // from any deprecated widget
24508
- class AutoCorrect extends React__default["default"].Component {
24455
+ class DeprecatedStandin extends React__default["default"].Component {
24509
24456
  static validate(userInput, rubric) {
24510
24457
  // Since this mean to replace an existing widget the learner
24511
24458
  // WILL earn points for this widget
@@ -24520,7 +24467,7 @@ class AutoCorrect extends React__default["default"].Component {
24520
24467
  return {};
24521
24468
  };
24522
24469
  simpleValidate = rubric => {
24523
- return AutoCorrect.validate(this.getUserInput(), rubric);
24470
+ return DeprecatedStandin.validate(this.getUserInput(), rubric);
24524
24471
  };
24525
24472
  render() {
24526
24473
  return /*#__PURE__*/React__default["default"].createElement("div", {
@@ -24529,16 +24476,17 @@ class AutoCorrect extends React__default["default"].Component {
24529
24476
  paddingBottom: 8
24530
24477
  }
24531
24478
  }, /*#__PURE__*/React__default["default"].createElement(Banner__default["default"], {
24532
- text: i18n__namespace._("This part of the question is no-longer available. You will not be graded on this part."),
24533
- kind: "warning",
24479
+ text: i18n__namespace._("Sorry, this part of the question is no longer available. 😅 Don't worry, you won't be graded on this part. Keep going!"),
24480
+ kind: "info",
24534
24481
  layout: "full-width"
24535
24482
  }));
24536
24483
  }
24537
24484
  }
24538
- var DeprecatedStandin = {
24485
+ var DeprecatedStandin$1 = {
24539
24486
  name: "deprecated-standin",
24540
24487
  displayName: "Deprecated Standin",
24541
- widget: AutoCorrect
24488
+ widget: DeprecatedStandin,
24489
+ hidden: true
24542
24490
  };
24543
24491
 
24544
24492
  class Dropdown extends React__namespace.Component {
@@ -29113,7 +29061,7 @@ class InteractiveGraph extends React__namespace.Component {
29113
29061
  // Solve for side by using the law of sines
29114
29062
  const side = Math.sin(innerAngles[1] * Math.PI / 180) / Math.sin(innerAngles[2] * Math.PI / 180) * knownSide;
29115
29063
  const outerAngle = GraphUtils.findAngle(coords[rel(1)], coords[rel(-1)]);
29116
- const offset = this.graphie?.polar(side, outerAngle + (onLeft ? 1 : -1) * innerAngles[0]);
29064
+ const offset = polar(side, outerAngle + (onLeft ? 1 : -1) * innerAngles[0]);
29117
29065
  return this.graphie?.addPoints(coords[rel(-1)], offset);
29118
29066
  }
29119
29067
  // @ts-expect-error - TS2339 - Property 'snapTo' does not exist on type 'PerseusGraphType'.
@@ -29137,7 +29085,7 @@ class InteractiveGraph extends React__namespace.Component {
29137
29085
  const innerAngle = lawOfCosines(sides[0], sides[2], sides[1]);
29138
29086
  const outerAngle = GraphUtils.findAngle(coords[rel(1)], coords[rel(-1)]);
29139
29087
  const onLeft = sign(ccw(coords[rel(-1)], coords[rel(1)], coords[i])) === 1;
29140
- const offset = this.graphie?.polar(sides[0], outerAngle + (onLeft ? 1 : -1) * innerAngle);
29088
+ const offset = polar(sides[0], outerAngle + (onLeft ? 1 : -1) * innerAngle);
29141
29089
  return this.graphie?.addPoints(coords[rel(-1)], offset);
29142
29090
  }
29143
29091
  // Snap to grid (already done)
@@ -31160,7 +31108,7 @@ const AnswerPill = props => {
31160
31108
  id: idFactory.get("perseus-label-image-widget-answer-pill"),
31161
31109
  onClick: correct ? undefined : onClick,
31162
31110
  ref: ref,
31163
- style: [style, popperStyle, styles$h.relative, correct && styles$h.correct, incorrect && styles$h.incorrect, (focused || hovered) && BringToFront]
31111
+ style: [style, popperStyle, styles$h.pill, correct && styles$h.correct, incorrect && styles$h.incorrect, (focused || hovered) && BringToFront]
31164
31112
  }, /*#__PURE__*/React__namespace.createElement(Renderer, {
31165
31113
  content: answerString,
31166
31114
  inline: true
@@ -31174,6 +31122,11 @@ const styles$h = aphrodite.StyleSheet.create({
31174
31122
  },
31175
31123
  incorrect: {
31176
31124
  backgroundColor: Color__default["default"].offBlack64
31125
+ },
31126
+ pill: {
31127
+ // Reset the Pill's default height in order to account
31128
+ // for multi-line pills.
31129
+ height: "auto"
31177
31130
  }
31178
31131
  });
31179
31132
 
@@ -44105,7 +44058,7 @@ var Video$1 = {
44105
44058
  };
44106
44059
 
44107
44060
  /* eslint-disable import/no-named-as-default */
44108
- 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];
44061
+ 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];
44109
44062
 
44110
44063
  /**
44111
44064
  * This should be called by all clients, specifying whether extra widgets are
@@ -44123,7 +44076,7 @@ const init = function (options) {
44123
44076
  // This section replaces widgets
44124
44077
  // When it's time to replace the transformer widget
44125
44078
  // uncomment this line
44126
- // Widgets.replaceWidget("transformer", "always-correct");
44079
+ // Widgets.replaceWidget("transformer", "deprecated-standin");
44127
44080
 
44128
44081
  // Pass skipMathJax: true if MathJax is already loaded and configured.
44129
44082
  const skipMathJax = options.skipMathJax;
@@ -44149,7 +44102,7 @@ const init = function (options) {
44149
44102
 
44150
44103
  // This file is processed by a Rollup plugin (replace) to inject the production
44151
44104
  const libName = "@khanacademy/perseus";
44152
- const libVersion = "17.8.0";
44105
+ const libVersion = "18.0.1";
44153
44106
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
44154
44107
 
44155
44108
  const apiVersion = {
@@ -46726,6 +46679,417 @@ const registerAllWidgetsForTesting = () => {
46726
46679
  // Widgets.replaceWidget("transformer", "deprecated-standin");
46727
46680
  };
46728
46681
 
46682
+ /**
46683
+ * This function extracts the answers from the widgets.
46684
+ *
46685
+ * For each widget type, we first check to make sure the path to the answer exists,
46686
+ * then we extract the answer and add it to the list of answers.
46687
+ *
46688
+ * A list is returned because some widgets, like multi-select radio widgets and groups
46689
+ * can have multiple answers.
46690
+ *
46691
+ * @param {PerseusRenderer["widgets"]} widgets
46692
+ * @returns a list of strings that are the answers to the widgets
46693
+ */
46694
+ function getAnswersFromWidgets(widgets) {
46695
+ const answers = [];
46696
+ wonderStuffCore.keys(widgets).forEach(widgetID => {
46697
+ const widget = widgets[widgetID];
46698
+ if (!widget.options) {
46699
+ return;
46700
+ }
46701
+ switch (widget.type) {
46702
+ case "radio":
46703
+ const radio = widget;
46704
+ const options = radio.options;
46705
+ // Answer is the content of the correct choice
46706
+ if (options?.choices?.length) {
46707
+ for (const choice of options.choices) {
46708
+ if (choice?.correct) {
46709
+ answers.push(choice.content);
46710
+ }
46711
+ }
46712
+ }
46713
+ break;
46714
+ case "categorizer":
46715
+ // Answer is a list of items mapped to their category
46716
+ // e.g. ['item 1: category 1', 'item 2: category 2']
46717
+ const categorizer = widget;
46718
+ if (categorizer.options?.categories && categorizer.options?.items && categorizer.options?.values) {
46719
+ const categories = categorizer.options?.categories;
46720
+ const items = categorizer.options?.items;
46721
+ const values = categorizer.options?.values;
46722
+ answers.push(...values.map((value, index) => `${items[index]}: ${categories[value]}`));
46723
+ }
46724
+ break;
46725
+ case "dropdown":
46726
+ // Answer is the content of correct dropdown option
46727
+ const dropdown = widget;
46728
+ if (dropdown.options?.choices) {
46729
+ for (const choice of dropdown.options.choices) {
46730
+ if (choice.correct) {
46731
+ answers.push(choice.content);
46732
+ }
46733
+ }
46734
+ }
46735
+ break;
46736
+ case "numeric-input":
46737
+ // Answer is the numeric value cast to a string
46738
+ const numericInput = widget;
46739
+ if (numericInput.options?.answers) {
46740
+ for (const ans of numericInput.options.answers) {
46741
+ if (ans.status === "correct" && ans.value != null) {
46742
+ answers.push(ans.value.toString());
46743
+ }
46744
+ }
46745
+ }
46746
+ break;
46747
+ case "input-number":
46748
+ // Answer is the correct value
46749
+ const inputNumber = widget;
46750
+ if (inputNumber.options?.value) {
46751
+ answers.push(inputNumber.options.value.toString());
46752
+ }
46753
+ break;
46754
+ case "expression":
46755
+ // Answer is a list of potential correct expressions,
46756
+ // since there can be multiple correct forms of the expression
46757
+ const expression = widget;
46758
+ if (expression.options?.answerForms) {
46759
+ answers.push(...expression.options.answerForms.map(answer => answer.value));
46760
+ }
46761
+ break;
46762
+ case "group":
46763
+ case "graded-group":
46764
+ // Groups can contain multiple widgets, so we recursively
46765
+ // call this function to get the nested answers from the
46766
+ // group's widgets
46767
+ const gradedGroup = widget;
46768
+ if (gradedGroup.options?.widgets) {
46769
+ answers.push(...getAnswersFromWidgets(gradedGroup.options.widgets));
46770
+ }
46771
+ break;
46772
+ case "plotter":
46773
+ // Answer is a map of the correct value for each category
46774
+ // e.g. '{Soccer: 35, Basketball: 20, Golf: 45, Volleyball: 15,
46775
+ // Tennis: 30}'
46776
+ const plotter = widget;
46777
+ if (plotter.options?.categories && plotter.options?.correct && plotter.options.categories.length === plotter.options.correct.length) {
46778
+ const {
46779
+ categories,
46780
+ correct
46781
+ } = plotter.options;
46782
+ answers.push(`{${categories.map((category, index) => `${category}: ${correct[index]}`).join(", ")}}`);
46783
+ }
46784
+ break;
46785
+ case "interactive-graph":
46786
+ case "grapher":
46787
+ // Answer is a list of points that should be on the graph
46788
+ // E.g. 'There should be point(s) on [(-1, 1), (1, 1)]'
46789
+ const grapher = widget;
46790
+ // @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'.
46791
+ if (grapher.options?.correct?.coords) {
46792
+ answers.push(
46793
+ // @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'.
46794
+ `There should be point(s) on [${grapher.options.correct?.coords.join("], [")}]`);
46795
+ }
46796
+ break;
46797
+ case "orderer":
46798
+ // Answer is the content of the each option
46799
+ // in the correct order on newlines
46800
+ // E.g. 'item 1\nitem 2\nitem 3'
46801
+ const orderer = widget;
46802
+ if (orderer.options?.correctOptions) {
46803
+ answers.push(orderer.options.correctOptions.map(option => option.content).join("\n"));
46804
+ }
46805
+ break;
46806
+ case "sorter":
46807
+ // Answer is the content of each option in the correct order
46808
+ // separated by commas
46809
+ // E.g. 'item 1, item 2, item 3'
46810
+ const sorter = widget;
46811
+ if (sorter.options?.correct) {
46812
+ answers.push(sorter.options.correct.join(", "));
46813
+ }
46814
+ break;
46815
+ case "label-image":
46816
+ // Answer is a list of labels, their positions, and their answer
46817
+ // E.g. '{label: "label 1", position: {x: 0.5, y: 0.5}, answer: "answer 1"}'
46818
+ const labelImage = widget;
46819
+ if (labelImage.options?.markers) {
46820
+ answers.push(...labelImage.options.markers.map(m => `{label: "${m.label}", position: {${m.x},${m.y}}, answer: "${m.answers.join(", ")}"}`));
46821
+ }
46822
+ break;
46823
+ case "number-line":
46824
+ // Answer is the correct x position on the number line
46825
+ // E.g. '0.5'
46826
+ const numberLine = widget;
46827
+ if (numberLine.options?.correctX != null) {
46828
+ answers.push(numberLine.options.correctX.toString());
46829
+ }
46830
+ break;
46831
+ case "matrix":
46832
+ // Answer is a list of rows representing the matrix
46833
+ // E.g. '[[1, 2], [3, 4]]'
46834
+ const matrix = widget;
46835
+ if (matrix.options?.answers) {
46836
+ answers.push(`[${matrix.options.answers.join("], [")}]`);
46837
+ }
46838
+ break;
46839
+ case "matcher":
46840
+ // Answer is a table of the left and right items
46841
+ // E.g. '| Left | Right |\n| --- | --- |\n| left item 1 | right item 1 |\n| left item 2 | right item 2 |'
46842
+ const matcher = widget;
46843
+ if (matcher.options?.left && matcher.options?.right) {
46844
+ const {
46845
+ left,
46846
+ right
46847
+ } = matcher.options;
46848
+ const [leftHeader, rightHeader] = matcher.options.labels;
46849
+ const tableHeader = `| ${leftHeader} | ${rightHeader} |\n| --- | --- |`;
46850
+ const tableRows = left.map((leftItem, index) => {
46851
+ return `| ${leftItem} | ${right[index]} |`;
46852
+ });
46853
+ const table = [tableHeader, ...tableRows].join("\n");
46854
+ answers.push(table);
46855
+ }
46856
+ break;
46857
+ }
46858
+ });
46859
+ return answers;
46860
+ }
46861
+
46862
+ /**
46863
+ * Inject a string equivalent of the widgets into the content.
46864
+ *
46865
+ * Content may contain Perseus widgets, that looks like this: '[[☃ Radio 1]]'.
46866
+ * This function replaces those widgets with its equivalent string.
46867
+ *
46868
+ * @param {string} content
46869
+ * @param {PerseusRenderer["widgets"]} widgets
46870
+ * @returns
46871
+ */
46872
+ function injectWidgets(content, widgets) {
46873
+ // The types for taskProgress.itemData are not well defined,
46874
+ // so there is a chance that widgets or content could be undefined.
46875
+ // In that scenario, simply exit early and return the content as is.
46876
+ if (!content) {
46877
+ return "";
46878
+ }
46879
+ if (!widgets) {
46880
+ return content;
46881
+ }
46882
+ let context = content;
46883
+ wonderStuffCore.keys(widgets).forEach(widgetID => {
46884
+ const widget = widgets[widgetID];
46885
+ if (!widget.options) {
46886
+ return;
46887
+ }
46888
+ switch (widget.type) {
46889
+ case "radio":
46890
+ // Replace radio with the radio options
46891
+ // '[[☃ Radio 1]]' -> 'option 1\noption 2\noption 3'
46892
+ const radio = widget;
46893
+ if (radio.options?.choices?.length) {
46894
+ let radioContext = radio.options.choices.map(choice => choice.content).join("\n");
46895
+ if (radio.options?.randomize) {
46896
+ 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";
46897
+ }
46898
+ context = context.replace(`[[☃ ${widgetID}]]`, radioContext);
46899
+ }
46900
+ break;
46901
+ case "image":
46902
+ // Replace image with the image's alt text
46903
+ // '[[☃ Image 1]]' -> '<img id="Image 1" alt="The text for this image">'
46904
+ const image = widget;
46905
+ if (image.options?.alt) {
46906
+ context = context.replace(`[[☃ ${widgetID}]]`, `<img id="${widgetID}" alt="${image.options.alt}">`);
46907
+ }
46908
+ break;
46909
+ case "label-image":
46910
+ // Replace label-image with the label-image's alt text
46911
+ // '[[☃ 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]'
46912
+ const labelImage = widget;
46913
+ if (labelImage.options?.imageAlt) {
46914
+ context = context.replace(`[[☃ ${widgetID}]]`, `[An image with dots that user needs to label. Label choices: [${labelImage.options.choices.join(", ")}]. Image alt text: ${labelImage.options?.imageAlt ?? ""}]`);
46915
+ }
46916
+ break;
46917
+ case "explanation":
46918
+ // Replace explaintion with the explaination text
46919
+ // Note: explainations can have widgets inside of them too
46920
+ // '[[☃ Explanation 1]]' -> 'This is the hidden explanation text'
46921
+ const explanation = widget;
46922
+ if (explanation.options?.explanation) {
46923
+ context = context.replace(`[[☃ ${widgetID}]]`, injectWidgets(explanation.options.explanation, explanation.options.widgets));
46924
+ }
46925
+ break;
46926
+ case "passage":
46927
+ // Replace passage with the passage text
46928
+ // '[[☃ Passage 1]]' -> '# Passage Title\n\nPassage text'
46929
+ const passage = widget;
46930
+ if (passage.options?.passageTitle || passage.options?.passageText) {
46931
+ const {
46932
+ passageTitle,
46933
+ passageText
46934
+ } = passage.options;
46935
+ context = context.replace(`[[☃ ${widgetID}]]`, `# ${passageTitle}\n\n${passageText}`);
46936
+ }
46937
+ break;
46938
+ case "group":
46939
+ case "graded-group":
46940
+ // Replace group with the group's nested content
46941
+ // '[[☃ Group 1]]' -> 'Nested question\n[[☃ Radio 1]]'
46942
+ // -> 'Nested question\noption 1\noption 2\noption 3'
46943
+ const group = widget;
46944
+ if (group.options?.widgets && group.options.content) {
46945
+ context = context.replace(`[[☃ ${widgetID}]]`, injectWidgets(group.options.content, group.options.widgets));
46946
+ }
46947
+ break;
46948
+ case "graded-group-set":
46949
+ // Replace graded-group-set with the the nested groups
46950
+ // '[[☃ Graded Group Set 1]]'
46951
+ // -> 'Nested question 1\noption 1\noption 2\noption 3\nNested question 2\noption 1\noption 2\noption 3'
46952
+ const gradedGroup = widget;
46953
+ if (gradedGroup.options?.gradedGroups) {
46954
+ const gradedGroups = gradedGroup.options.gradedGroups;
46955
+ const gradedGroupsContent = gradedGroups.reduce((acc, group) => {
46956
+ if (group.widgets && group.content) {
46957
+ acc += injectWidgets(group.content, group.widgets) + "\n";
46958
+ }
46959
+ return acc;
46960
+ }, "");
46961
+ context = context.replace(`[[☃ ${widgetID}]]`, gradedGroupsContent);
46962
+ }
46963
+ break;
46964
+ case "categorizer":
46965
+ // Replace categorizer with the options and categories
46966
+ // '[[☃ Categorizer 1]]' -> '''
46967
+ // For each item, select the correct category. Categories: category 1, category 2, category 3.
46968
+ // Items:
46969
+ // item 1
46970
+ // item 2
46971
+ // '''
46972
+ const categorizer = widget;
46973
+ if (categorizer.options?.categories && categorizer.options.items) {
46974
+ const categories = categorizer.options.categories;
46975
+ const items = categorizer.options.items;
46976
+ context = context.replace(`[[☃ ${widgetID}]]`, `For each item, select the correct category. Categories: ${categories.join(", ")}.\nItems:\n${items.join("\n")}\n`);
46977
+ }
46978
+ break;
46979
+ case "dropdown":
46980
+ // Replace dropdown with the dropdown options
46981
+ // '[[☃ Dropdown 1]]' -> '[option 1 | option 2 | option 3]'
46982
+ const dropdown = widget;
46983
+ if (dropdown.options?.choices) {
46984
+ const choices = dropdown.options.choices.map(choice => choice.content);
46985
+ context = context.replace(`[[☃ ${widgetID}]]`, `[${choices.join(" | ")}]`);
46986
+ }
46987
+ break;
46988
+ case "definition":
46989
+ // Replace definition with the word that is being defined
46990
+ // NOTE(chase): this does not include the definition itself,
46991
+ // because I believe the AI can infer that without wasting extra tokens
46992
+ // '[[☃ Definition 1]]' -> 'word'
46993
+ const definition = widget;
46994
+ if (definition.options?.togglePrompt) {
46995
+ context = context.replace(`[[☃ ${widgetID}]]`, definition.options.togglePrompt);
46996
+ }
46997
+ break;
46998
+ case "orderer":
46999
+ // Replace orderer with the orderer options
47000
+ // '[[☃ Orderer 1]]' -> 'option 1\noption 2\noption 3'
47001
+ const orderer = widget;
47002
+ if (orderer.options?.options) {
47003
+ context = context.replace(`[[☃ ${widgetID}]]`, orderer.options.options.map(option => option.content).join("\n"));
47004
+ }
47005
+ break;
47006
+ case "sorter":
47007
+ // Replace sorter with the options
47008
+ // NOTE(chase): These are already in the correct order
47009
+ // '[[☃ Sorter 1]]' -> '[option 1 | option 2 | option 3]'
47010
+ const sorter = widget;
47011
+ if (sorter.options?.correct) {
47012
+ const choices = sorter.options.correct;
47013
+ context = context.replace(`[[☃ ${widgetID}]]`, `[${choices.join(" | ")}]`);
47014
+ }
47015
+ break;
47016
+ case "interactive-graph":
47017
+ // Replace interactive-graph with the graph's range
47018
+ // '[[☃ Interactive Graph 1]]' -> '[Graph with an x range of -10 to 10 and y range of -10 to 10]'
47019
+ const interactiveGraph = widget;
47020
+ if (interactiveGraph.options?.range.length === 2) {
47021
+ const [x, y] = interactiveGraph.options.range;
47022
+ context = context.replace(`[[☃ ${widgetID}]]`, `[Graph with an x range of ${x[0]} to ${x[1]} and y range of ${y[0]} to ${y[1]}]`);
47023
+ }
47024
+ break;
47025
+ case "number-line":
47026
+ // Replace number-line with the number line's range, step, and initial position
47027
+ // '[[☃ Number Line 1]]' -> '[Number line with a range of -10 to 10, a step of 1, and an initial position of 0]'
47028
+ const numberLine = widget;
47029
+ if (numberLine.options?.range.length === 2 && numberLine.options?.tickStep && numberLine.options?.initialX) {
47030
+ const [min, max] = numberLine.options.range;
47031
+ const step = numberLine.options.tickStep;
47032
+ const initialPosition = numberLine.options.initialX;
47033
+ context = context.replace(`[[☃ ${widgetID}]]`, `[Number line with a range of ${min} to ${max}, a step of ${step}, and an initial position of ${initialPosition}]`);
47034
+ }
47035
+ break;
47036
+ case "matrix":
47037
+ // Replace matrix with the matrix's dimensions
47038
+ // '[[☃ Matrix 1]]' -> '[Matrix with 2 rows and 3 columns. The user can click on each cell to enter a value]'
47039
+ const matrix = widget;
47040
+ if (matrix.options?.matrixBoardSize.length === 2) {
47041
+ const [rows, columns] = matrix.options.matrixBoardSize;
47042
+ context = context.replace(`[[☃ ${widgetID}]]`, `[Matrix with ${rows} rows and ${columns} columns. The user can click on each cell to enter a value]`);
47043
+ }
47044
+ break;
47045
+ case "matcher":
47046
+ // Replace matcher with a table representing the left and right columns
47047
+ // '[[☃ Matcher 1]]' -> '| Left Column | Right Column |\n| --- | --- |\n| option 1 | option 2 |\n| option 3 | option 4 |'
47048
+ const matcher = widget;
47049
+ if (matcher.options?.left && matcher.options?.right) {
47050
+ const {
47051
+ left,
47052
+ right
47053
+ } = matcher.options;
47054
+ const [leftHeader, rightHeader] = matcher.options.labels;
47055
+ const tableHeader = `| ${leftHeader} | ${rightHeader} |\n| --- | --- |`;
47056
+ const tableRows = left.map((leftItem, index) => {
47057
+ return `| ${leftItem} | ${right[index]} |`;
47058
+ });
47059
+ const table = [tableHeader, ...tableRows].join("\n");
47060
+ 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.";
47061
+ context = context.replace(`[[☃ ${widgetID}]]`, `${matcherWidgetExplanation}\n${table}`);
47062
+ }
47063
+ break;
47064
+ case "numeric-input":
47065
+ case "input-number":
47066
+ case "expression":
47067
+ // These widgets are just input fields, so we replace them with a ?
47068
+ // 'Solve 2+2 [[☃ Numeric Input 1]]' -> 'Solve 2+2 ?'
47069
+ // (we've found that using "?" gives the best results on fill-
47070
+ // in-the-blank questions).
47071
+ context = context.replace(`[[☃ ${widgetID}]]`, "?");
47072
+ break;
47073
+ default:
47074
+ // Tell the bot that it doesn't understand this widget
47075
+ 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.]]`);
47076
+ }
47077
+ });
47078
+ return context;
47079
+ }
47080
+
47081
+ /**
47082
+ * Helper to parse Perseus JSON
47083
+ * Why not just use JSON.parse? We want:
47084
+ * - To make sure types are correct
47085
+ * - To give us a central place to validate/transform output if needed
47086
+ * @param {string} input - the stringified Perseus JSON
47087
+ * @returns {PerseusItem} the parsed PerseusItem object
47088
+ */
47089
+ function parsePerseusJSON(input) {
47090
+ return JSON.parse(input);
47091
+ }
47092
+
46729
47093
  const EditorJsonify = {
46730
47094
  serialize: function () {
46731
47095
  // Omit props that get passed to all widgets
@@ -46783,10 +47147,12 @@ exports.buildMapper = buildMapper;
46783
47147
  exports.components = components;
46784
47148
  exports.constants = constants;
46785
47149
  exports.displaySigFigs = displaySigFigs;
47150
+ exports.getAnswersFromWidgets = getAnswersFromWidgets;
46786
47151
  exports.globalConstants = globalConstants;
46787
47152
  exports.globalStyles = globalStyles;
46788
47153
  exports.icons = iconPaths;
46789
47154
  exports.init = init;
47155
+ exports.injectWidgets = injectWidgets;
46790
47156
  exports.interactiveSizes = interactiveSizes$1;
46791
47157
  exports.isItemRenderableByVersion = isItemRenderableByVersion;
46792
47158
  exports.itemDataVersion = itemDataVersion;
@@ -46794,6 +47160,7 @@ exports.itemToTree = itemToTree;
46794
47160
  exports.itemVersion = ItemVersion;
46795
47161
  exports.libVersion = libVersion;
46796
47162
  exports.mediaQueries = mediaQueries;
47163
+ exports.parsePerseusJSON = parsePerseusJSON;
46797
47164
  exports.plotterPlotTypes = plotterPlotTypes;
46798
47165
  exports.preprocessTex = preprocessTex;
46799
47166
  exports.registerAllWidgetsForTesting = registerAllWidgetsForTesting;