@khanacademy/perseus 35.0.0 → 35.1.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 (40) hide show
  1. package/dist/es/index.js +756 -615
  2. package/dist/es/index.js.map +1 -1
  3. package/dist/index.js +756 -613
  4. package/dist/index.js.map +1 -1
  5. package/dist/perseus-api.d.ts +2 -0
  6. package/dist/perseus-types.d.ts +7 -0
  7. package/dist/server-item-renderer.d.ts +2 -0
  8. package/dist/types.d.ts +3 -4
  9. package/dist/validation.types.d.ts +5 -2
  10. package/dist/widgets/cs-program/cs-program-validator.d.ts +3 -0
  11. package/dist/widgets/cs-program/cs-program.d.ts +2 -2
  12. package/dist/widgets/expression/expression.d.ts +14 -2
  13. package/dist/widgets/grapher/grapher-validator.d.ts +4 -0
  14. package/dist/widgets/grapher/util.d.ts +0 -2
  15. package/dist/widgets/iframe/iframe-validator.d.ts +3 -0
  16. package/dist/widgets/iframe/iframe.d.ts +3 -2
  17. package/dist/widgets/interactive-graph.d.ts +3 -3
  18. package/dist/widgets/interactive-graphs/backgrounds/axis-ticks.d.ts +1 -1
  19. package/dist/widgets/interactive-graphs/graph-locked-layer.d.ts +2 -0
  20. package/dist/widgets/interactive-graphs/interactive-graph-question-builder.d.ts +7 -0
  21. package/dist/widgets/interactive-graphs/interactive-graph-validator.d.ts +4 -0
  22. package/dist/widgets/interactive-graphs/locked-figures/locked-ellipse.d.ts +5 -1
  23. package/dist/widgets/interactive-graphs/locked-figures/locked-function.d.ts +5 -1
  24. package/dist/widgets/interactive-graphs/locked-figures/locked-line.d.ts +2 -0
  25. package/dist/widgets/interactive-graphs/locked-figures/locked-point.d.ts +5 -1
  26. package/dist/widgets/interactive-graphs/locked-figures/locked-polygon.d.ts +5 -1
  27. package/dist/widgets/interactive-graphs/locked-figures/locked-vector.d.ts +5 -1
  28. package/dist/widgets/interactive-graphs/mafs-state-to-interactive-graph.d.ts +3 -0
  29. package/dist/widgets/label-image/label-image.d.ts +4 -0
  30. package/dist/widgets/matcher/matcher-validator.d.ts +4 -0
  31. package/dist/widgets/matcher/matcher.d.ts +3 -2
  32. package/dist/widgets/matrix/matrix-validator.d.ts +5 -0
  33. package/dist/widgets/matrix/matrix.d.ts +1 -0
  34. package/dist/widgets/number-line/number-line-validator.d.ts +4 -0
  35. package/dist/widgets/number-line/number-line.d.ts +3 -2
  36. package/dist/widgets/plotter/plotter-validator.d.ts +4 -0
  37. package/dist/widgets/plotter/plotter.d.ts +1 -0
  38. package/dist/widgets/table/table-validator.d.ts +5 -0
  39. package/dist/widgets/table/table.d.ts +1 -0
  40. package/package.json +1 -1
package/dist/es/index.js CHANGED
@@ -3146,7 +3146,7 @@ function constrainedTickStepsFromTickSteps(tickSteps, ranges) {
3146
3146
  /**
3147
3147
  * Approximate equality on numbers and primitives.
3148
3148
  */
3149
- function eq$3(x, y) {
3149
+ function eq$4(x, y) {
3150
3150
  if (typeof x === "number" && typeof y === "number") {
3151
3151
  return Math.abs(x - y) < 1e-9;
3152
3152
  }
@@ -3173,7 +3173,7 @@ function deepEq$5(x, y) {
3173
3173
  return false;
3174
3174
  }
3175
3175
  if (typeof x === "function" && typeof y === "function") {
3176
- return eq$3(x, y);
3176
+ return eq$4(x, y);
3177
3177
  }
3178
3178
  if (typeof x === "function" || typeof y === "function") {
3179
3179
  return false;
@@ -3190,7 +3190,7 @@ function deepEq$5(x, y) {
3190
3190
  if (typeof x === "object" && !!x || typeof y === "object" && !!y) {
3191
3191
  return false;
3192
3192
  }
3193
- return eq$3(x, y);
3193
+ return eq$4(x, y);
3194
3194
  }
3195
3195
 
3196
3196
  /**
@@ -3490,7 +3490,7 @@ const Util = {
3490
3490
  gridStepFromTickStep,
3491
3491
  tickStepFromNumTicks,
3492
3492
  constrainedTickStepsFromTickSteps,
3493
- eq: eq$3,
3493
+ eq: eq$4,
3494
3494
  deepEq: deepEq$5,
3495
3495
  parseQueryString,
3496
3496
  updateQueryString: updateQueryString$2,
@@ -10813,7 +10813,7 @@ class WrappedLine extends WrappedDrawing {
10813
10813
  * A collection of geomtry-related utility functions
10814
10814
  */
10815
10815
  const {
10816
- eq: eq$2,
10816
+ eq: eq$3,
10817
10817
  deepEq: deepEq$4
10818
10818
  } = Util;
10819
10819
 
@@ -10823,7 +10823,7 @@ const {
10823
10823
 
10824
10824
  // Given a number, return whether it is positive (1), negative (-1), or zero (0)
10825
10825
  function sign(val) {
10826
- if (eq$2(val, 0)) {
10826
+ if (eq$3(val, 0)) {
10827
10827
  return 0;
10828
10828
  }
10829
10829
  return val > 0 ? 1 : -1;
@@ -10835,7 +10835,7 @@ function ccw(a, b, c) {
10835
10835
  return (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1]);
10836
10836
  }
10837
10837
  function collinear(a, b, c) {
10838
- return eq$2(ccw(a, b, c), 0);
10838
+ return eq$3(ccw(a, b, c), 0);
10839
10839
  }
10840
10840
 
10841
10841
  // Given rect bounding points A and B, whether point C is inside the rect
@@ -10980,7 +10980,7 @@ function similar(coords1, coords2, tolerance) {
10980
10980
  return pair[0] / pair[1];
10981
10981
  });
10982
10982
  const same = _.all(factors, function (factor) {
10983
- return eq$2(factors[0], factor);
10983
+ return eq$3(factors[0], factor);
10984
10984
  });
10985
10985
  const congruentEnough = _.all(sidePairs, function (pair) {
10986
10986
  return number.equal(pair[0], pair[1], tolerance);
@@ -11032,7 +11032,7 @@ function rotate(array, n) {
11032
11032
  return array.slice(n).concat(array.slice(0, n));
11033
11033
  }
11034
11034
  function getLineEquation(first, second) {
11035
- if (eq$2(first[0], second[0])) {
11035
+ if (eq$3(first[0], second[0])) {
11036
11036
  return "x = " + first[0].toFixed(3);
11037
11037
  }
11038
11038
  const m = (second[1] - first[1]) / (second[0] - first[0]);
@@ -24664,6 +24664,31 @@ const parseUrl = url => new URL(url, location().origin);
24664
24664
  */
24665
24665
  const toAbsoluteUrl = url => parseUrl(url).href;
24666
24666
 
24667
+ function csProgramValidator(state) {
24668
+ // The iframe can tell us whether it's correct or incorrect,
24669
+ // and pass an optional message
24670
+ if (state.status === "correct") {
24671
+ return {
24672
+ type: "points",
24673
+ earned: 1,
24674
+ total: 1,
24675
+ message: state.message || null
24676
+ };
24677
+ }
24678
+ if (state.status === "incorrect") {
24679
+ return {
24680
+ type: "points",
24681
+ earned: 0,
24682
+ total: 1,
24683
+ message: state.message || null
24684
+ };
24685
+ }
24686
+ return {
24687
+ type: "invalid",
24688
+ message: "Keep going, you're not there yet!"
24689
+ };
24690
+ }
24691
+
24667
24692
  /**
24668
24693
  * This widget is for embedding Khan Academy CS programs.
24669
24694
  */
@@ -24714,29 +24739,8 @@ class CSProgram extends React.Component {
24714
24739
  };
24715
24740
  }
24716
24741
  // The widget's grading function
24717
- static validate(state, rubric) {
24718
- // The iframe can tell us whether it's correct or incorrect,
24719
- // and pass an optional message
24720
- if (state.status === "correct") {
24721
- return {
24722
- type: "points",
24723
- earned: 1,
24724
- total: 1,
24725
- message: state.message || null
24726
- };
24727
- }
24728
- if (state.status === "incorrect") {
24729
- return {
24730
- type: "points",
24731
- earned: 0,
24732
- total: 1,
24733
- message: state.message || null
24734
- };
24735
- }
24736
- return {
24737
- type: "invalid",
24738
- message: "Keep going, you're not there yet!"
24739
- };
24742
+ static validate(state) {
24743
+ return csProgramValidator(state);
24740
24744
  }
24741
24745
  componentDidMount() {
24742
24746
  $(window).on("message", this.handleMessageEvent);
@@ -24744,11 +24748,11 @@ class CSProgram extends React.Component {
24744
24748
  componentWillUnmount() {
24745
24749
  $(window).off("message", this.handleMessageEvent);
24746
24750
  }
24747
- simpleValidate(rubric) {
24748
- return CSProgram.validate({
24751
+ simpleValidate() {
24752
+ return csProgramValidator({
24749
24753
  status: this.props.status,
24750
24754
  message: this.props.message
24751
- }, rubric);
24755
+ });
24752
24756
  }
24753
24757
  render() {
24754
24758
  if (!this.props.programID) {
@@ -26339,49 +26343,6 @@ function functionForType(
26339
26343
  type) {
26340
26344
  return functionTypeMapping[type];
26341
26345
  }
26342
- const validate$1 = (state, rubric) => {
26343
- if (state.type !== rubric.correct.type) {
26344
- return {
26345
- type: "points",
26346
- earned: 0,
26347
- total: 1,
26348
- message: null
26349
- };
26350
- }
26351
-
26352
- // We haven't moved the coords
26353
- if (state.coords == null) {
26354
- return {
26355
- type: "invalid",
26356
- message: null
26357
- };
26358
- }
26359
-
26360
- // Get new function handler for grading
26361
- const grader = functionForType(state.type);
26362
- const guessCoeffs = grader.getCoefficients(state.coords, state.asymptote);
26363
- const correctCoeffs = grader.getCoefficients(rubric.correct.coords, rubric.correct.asymptote);
26364
- if (guessCoeffs == null || correctCoeffs == null) {
26365
- return {
26366
- type: "invalid",
26367
- message: null
26368
- };
26369
- }
26370
- if (grader.areEqual(guessCoeffs, correctCoeffs)) {
26371
- return {
26372
- type: "points",
26373
- earned: 1,
26374
- total: 1,
26375
- message: null
26376
- };
26377
- }
26378
- return {
26379
- type: "points",
26380
- earned: 0,
26381
- total: 1,
26382
- message: null
26383
- };
26384
- };
26385
26346
  const getEquationString = props => {
26386
26347
  const plot = props.plot;
26387
26348
  if (plot.type && plot.coords) {
@@ -26499,7 +26460,6 @@ var util = /*#__PURE__*/Object.freeze({
26499
26460
  DEFAULT_BACKGROUND_IMAGE: DEFAULT_BACKGROUND_IMAGE,
26500
26461
  allTypes: allTypes,
26501
26462
  functionForType: functionForType,
26502
- validate: validate$1,
26503
26463
  getEquationString: getEquationString,
26504
26464
  pointsFromNormalized: pointsFromNormalized,
26505
26465
  maybePointsFromNormalized: maybePointsFromNormalized,
@@ -26510,6 +26470,52 @@ var util = /*#__PURE__*/Object.freeze({
26510
26470
  typeToButton: typeToButton
26511
26471
  });
26512
26472
 
26473
+ function grapherValidator(state, rubric) {
26474
+ if (state.type !== rubric.correct.type) {
26475
+ return {
26476
+ type: "points",
26477
+ earned: 0,
26478
+ total: 1,
26479
+ message: null
26480
+ };
26481
+ }
26482
+
26483
+ // We haven't moved the coords
26484
+ if (state.coords == null) {
26485
+ return {
26486
+ type: "invalid",
26487
+ message: null
26488
+ };
26489
+ }
26490
+
26491
+ // Get new function handler for grading
26492
+ const grader = functionForType(state.type);
26493
+ const guessCoeffs = grader.getCoefficients(state.coords, state.asymptote);
26494
+ const correctCoeffs = grader.getCoefficients(rubric.correct.coords,
26495
+ // @ts-expect-error - TS(2339) - Property 'asymptote' does not exist on type '{ type: "absolute_value"; coords: [Coord, Coord]; }'
26496
+ rubric.correct.asymptote);
26497
+ if (guessCoeffs == null || correctCoeffs == null) {
26498
+ return {
26499
+ type: "invalid",
26500
+ message: null
26501
+ };
26502
+ }
26503
+ if (grader.areEqual(guessCoeffs, correctCoeffs)) {
26504
+ return {
26505
+ type: "points",
26506
+ earned: 1,
26507
+ total: 1,
26508
+ message: null
26509
+ };
26510
+ }
26511
+ return {
26512
+ type: "points",
26513
+ earned: 0,
26514
+ total: 1,
26515
+ message: null
26516
+ };
26517
+ }
26518
+
26513
26519
  // @ts-expect-error - TS2339 - Property 'MovablePoint' does not exist on type 'typeof Graphie'.
26514
26520
  const MovablePoint$3 = Graphie.MovablePoint;
26515
26521
  // @ts-expect-error - TS2339 - Property 'MovableLine' does not exist on type 'typeof Graphie'.
@@ -26795,7 +26801,7 @@ class Grapher extends React.Component {
26795
26801
  };
26796
26802
  }
26797
26803
  static validate(state, rubric) {
26798
- return validate$1(state, rubric);
26804
+ return grapherValidator(state, rubric);
26799
26805
  }
26800
26806
  static getUserInputFromProps(props) {
26801
26807
  return props.plot;
@@ -26816,7 +26822,7 @@ class Grapher extends React.Component {
26816
26822
  return tickStep;
26817
26823
  }
26818
26824
  simpleValidate(rubric) {
26819
- return validate$1(this.getUserInput(), rubric);
26825
+ return grapherValidator(this.getUserInput(), rubric);
26820
26826
  }
26821
26827
  getUserInput() {
26822
26828
  return Grapher.getUserInputFromProps(this.props);
@@ -27057,6 +27063,31 @@ var Group$1 = {
27057
27063
  isLintable: true
27058
27064
  };
27059
27065
 
27066
+ function iframeValidator(state) {
27067
+ // The iframe can tell us whether it's correct or incorrect,
27068
+ // and pass an optional message
27069
+ if (state.status === "correct") {
27070
+ return {
27071
+ type: "points",
27072
+ earned: 1,
27073
+ total: 1,
27074
+ message: state.message || null
27075
+ };
27076
+ }
27077
+ if (state.status === "incorrect") {
27078
+ return {
27079
+ type: "points",
27080
+ earned: 0,
27081
+ total: 1,
27082
+ message: state.message || null
27083
+ };
27084
+ }
27085
+ return {
27086
+ type: "invalid",
27087
+ message: "Keep going, you're not there yet!"
27088
+ };
27089
+ }
27090
+
27060
27091
  /**
27061
27092
  * This is an iframe widget. It is used for rendering an iframe that
27062
27093
  * then communicates its state via window.postMessage
@@ -27095,10 +27126,9 @@ class Iframe extends React.Component {
27095
27126
  // @ts-expect-error - TS2345 - Argument of type 'readonly unknown[]' is not assignable to parameter of type 'any[]'.
27096
27127
  return change.apply(this, args);
27097
27128
  };
27098
- this.simpleValidate = rubric => {
27099
- // @ts-expect-error - TS2339 - Property 'validate' does not exist on type 'typeof Iframe'.
27100
- return Iframe.validate(this.getUserInput(), rubric);
27101
- };
27129
+ }
27130
+ static validate(state) {
27131
+ return iframeValidator(state);
27102
27132
  }
27103
27133
  componentDidMount() {
27104
27134
  $(window).on("message", this.handleMessageEvent);
@@ -27112,6 +27142,9 @@ class Iframe extends React.Component {
27112
27142
  message: this.props.message
27113
27143
  };
27114
27144
  }
27145
+ simpleValidate() {
27146
+ return iframeValidator(this.getUserInput());
27147
+ }
27115
27148
  render() {
27116
27149
  const style = {
27117
27150
  width: String(this.props.width),
@@ -27173,10 +27206,6 @@ class Iframe extends React.Component {
27173
27206
  });
27174
27207
  }
27175
27208
  }
27176
-
27177
- /**
27178
- * This is the widget's grading function
27179
- */
27180
27209
  Iframe.defaultProps = {
27181
27210
  status: "incomplete",
27182
27211
  // optional message
@@ -27184,32 +27213,6 @@ Iframe.defaultProps = {
27184
27213
  allowFullScreen: false,
27185
27214
  allowTopNavigation: false
27186
27215
  };
27187
- _.extend(Iframe, {
27188
- validate: function (state, rubric) {
27189
- // The iframe can tell us whether it's correct or incorrect,
27190
- // and pass an optional message
27191
- if (state.status === "correct") {
27192
- return {
27193
- type: "points",
27194
- earned: 1,
27195
- total: 1,
27196
- message: state.message || null
27197
- };
27198
- }
27199
- if (state.status === "incorrect") {
27200
- return {
27201
- type: "points",
27202
- earned: 0,
27203
- total: 1,
27204
- message: state.message || null
27205
- };
27206
- }
27207
- return {
27208
- type: "invalid",
27209
- message: "Keep going, you're not there yet!"
27210
- };
27211
- }
27212
- });
27213
27216
  var Iframe$1 = {
27214
27217
  name: "iframe",
27215
27218
  displayName: "Iframe (deprecated)",
@@ -28716,7 +28719,8 @@ const tickSize = 10;
28716
28719
  const tickLabelSize = 14;
28717
28720
  const YGridTick = ({
28718
28721
  y,
28719
- range
28722
+ range,
28723
+ tickStep
28720
28724
  }) => {
28721
28725
  // If the graph requires out-of-bounds labels, we want to make sure to set the
28722
28726
  // coordinates to the edge of the visible range of the graph. Otherwise,
@@ -28751,9 +28755,10 @@ const YGridTick = ({
28751
28755
 
28752
28756
  // If the graph displays both the y and x axis lines within the graph, we want
28753
28757
  // to hide the label at -1 on the y-axis to prevent overlap with the x-axis label
28754
- const showLabel = shouldShowLabel(y, range);
28758
+ const showLabel = shouldShowLabel(y, range, tickStep);
28755
28759
  return /*#__PURE__*/React.createElement("g", {
28756
- className: "tick"
28760
+ className: "tick",
28761
+ "aria-hidden": true
28757
28762
  }, /*#__PURE__*/React.createElement("line", {
28758
28763
  x1: x1,
28759
28764
  y1: y1,
@@ -28812,7 +28817,8 @@ const XGridTick = ({
28812
28817
  const xPositionText = xPosition + xAdjustment;
28813
28818
  const yPositionText = yPosition + yAdjustment;
28814
28819
  return /*#__PURE__*/React.createElement("g", {
28815
- className: "tick"
28820
+ className: "tick",
28821
+ "aria-hidden": true
28816
28822
  }, /*#__PURE__*/React.createElement("line", {
28817
28823
  x1: x1,
28818
28824
  y1: y1,
@@ -28831,13 +28837,13 @@ const XGridTick = ({
28831
28837
  };
28832
28838
 
28833
28839
  // Determines whether to show the label for the given tick
28834
- // Currently, the only condition is to hide the label at -1
28835
- // on the y-axis when the x-axis is within the graph
28836
- const shouldShowLabel = (number, range) => {
28840
+ // Currently, the only condition is to hide the label at -tickStep
28841
+ // on the y-axis when the y-axis is within the graph bounds
28842
+ const shouldShowLabel = (currentTick, range, tickStep) => {
28837
28843
  let showLabel = true;
28838
28844
 
28839
- // If the x-axis is within the graph and the y-axis is at -1, hide the label
28840
- if (range[X][MIN] < -1 && range[X][MAX] > 0 && number === -1) {
28845
+ // If the y-axis is within the graph and currentTick equals -tickStep, hide the label
28846
+ if (range[X][MIN] < -tickStep && range[X][MAX] > 0 && currentTick === -tickStep) {
28841
28847
  showLabel = false;
28842
28848
  }
28843
28849
  return showLabel;
@@ -28879,7 +28885,8 @@ const AxisTicks = () => {
28879
28885
  return /*#__PURE__*/React.createElement(YGridTick, {
28880
28886
  y: y,
28881
28887
  key: `y-grid-tick-${y}`,
28882
- range: range
28888
+ range: range,
28889
+ tickStep: tickStep[Y]
28883
28890
  });
28884
28891
  })), /*#__PURE__*/React.createElement("g", {
28885
28892
  className: "x-axis-ticks"
@@ -29632,7 +29639,8 @@ function LockedLabel(props) {
29632
29639
  color: lockedFigureColors[color],
29633
29640
  fontSize: font.size[size],
29634
29641
  backgroundColor: "rgba(255, 255, 255, 0.8)"
29635
- }
29642
+ },
29643
+ "aria-hidden": true
29636
29644
  }, /*#__PURE__*/React.createElement(TeX, null, text));
29637
29645
  }
29638
29646
 
@@ -29642,7 +29650,7 @@ function GraphLockedLabelsLayer(props) {
29642
29650
  lockedFigures
29643
29651
  } = props;
29644
29652
  return lockedFigures.map((figure, i) => {
29645
- var _flags$mafs, _flags$mafs2, _flags$mafs3, _flags$mafs4, _flags$mafs5;
29653
+ var _flags$mafs, _flags$mafs2, _flags$mafs3, _flags$mafs4, _flags$mafs5, _flags$mafs6;
29646
29654
  if (figure.type === "label") {
29647
29655
  return /*#__PURE__*/React.createElement(LockedLabel, _extends({
29648
29656
  key: `label-${i}`
@@ -29657,8 +29665,10 @@ function GraphLockedLabelsLayer(props) {
29657
29665
  flags != null && (_flags$mafs3 = flags["mafs"]) != null && _flags$mafs3["locked-vector-labels"] && figure.type === "vector" ||
29658
29666
  // Ellipse flag + ellipse type
29659
29667
  flags != null && (_flags$mafs4 = flags["mafs"]) != null && _flags$mafs4["locked-ellipse-labels"] && figure.type === "ellipse" ||
29668
+ // Polygon flag + polygon type
29669
+ flags != null && (_flags$mafs5 = flags["mafs"]) != null && _flags$mafs5["locked-polygon-labels"] && figure.type === "polygon" ||
29660
29670
  // Function flag + function type
29661
- flags != null && (_flags$mafs5 = flags["mafs"]) != null && _flags$mafs5["locked-function-labels"] && figure.type === "function") {
29671
+ flags != null && (_flags$mafs6 = flags["mafs"]) != null && _flags$mafs6["locked-function-labels"] && figure.type === "function") {
29662
29672
  var _figure$labels, _figure$points$0$labe, _figure$points$1$labe;
29663
29673
  return /*#__PURE__*/React.createElement(React.Fragment, {
29664
29674
  key: i
@@ -29675,15 +29685,23 @@ function GraphLockedLabelsLayer(props) {
29675
29685
  }
29676
29686
 
29677
29687
  const LockedEllipse = props => {
29688
+ var _flags$mafs;
29678
29689
  const {
29679
29690
  center,
29680
29691
  radius,
29681
29692
  angle,
29682
29693
  color: color$1,
29683
29694
  fillStyle,
29684
- strokeStyle
29695
+ strokeStyle,
29696
+ ariaLabel,
29697
+ flags
29685
29698
  } = props;
29686
- return /*#__PURE__*/React.createElement(Ellipse, {
29699
+ const hasAria = ariaLabel && (flags == null || (_flags$mafs = flags["mafs"]) == null ? void 0 : _flags$mafs["locked-figures-aria"]);
29700
+ return /*#__PURE__*/React.createElement("g", {
29701
+ className: "locked-ellipse",
29702
+ "aria-label": hasAria ? ariaLabel : undefined,
29703
+ "aria-hidden": !hasAria
29704
+ }, /*#__PURE__*/React.createElement(Ellipse, {
29687
29705
  center: center,
29688
29706
  radius: radius,
29689
29707
  angle: angle,
@@ -29699,10 +29717,11 @@ const LockedEllipse = props => {
29699
29717
  fill: fillStyle === "white" ? color.white : lockedFigureColors[color$1]
29700
29718
  }
29701
29719
  }
29702
- });
29720
+ }));
29703
29721
  };
29704
29722
 
29705
29723
  const LockedFunction = props => {
29724
+ var _props$flags;
29706
29725
  const [equation, setEquation] = useState();
29707
29726
  const {
29708
29727
  color,
@@ -29715,6 +29734,7 @@ const LockedFunction = props => {
29715
29734
  style: strokeStyle,
29716
29735
  domain
29717
29736
  };
29737
+ const hasAria = props.ariaLabel && ((_props$flags = props.flags) == null || (_props$flags = _props$flags["mafs"]) == null ? void 0 : _props$flags["locked-figures-aria"]);
29718
29738
  useEffect(() => {
29719
29739
  // Parsing the equation in a "useEffect" hook saves about 2ms each frame
29720
29740
  // when the learner is interacting with the graph (i.e. moving points).
@@ -29724,7 +29744,9 @@ const LockedFunction = props => {
29724
29744
  return null;
29725
29745
  }
29726
29746
  return /*#__PURE__*/React.createElement("g", {
29727
- className: "locked-function"
29747
+ className: "locked-function",
29748
+ "aria-label": hasAria ? props.ariaLabel : undefined,
29749
+ "aria-hidden": !hasAria
29728
29750
  }, directionalAxis === "x" && /*#__PURE__*/React.createElement(Plot$3.OfX, _extends({
29729
29751
  y: x => equation.eval({
29730
29752
  x
@@ -29817,6 +29839,7 @@ function isBetween(x, low, high) {
29817
29839
  }
29818
29840
 
29819
29841
  const LockedLine = props => {
29842
+ var _flags$mafs;
29820
29843
  const {
29821
29844
  color: color$1,
29822
29845
  lineStyle,
@@ -29824,9 +29847,12 @@ const LockedLine = props => {
29824
29847
  points,
29825
29848
  showPoint1,
29826
29849
  showPoint2,
29850
+ ariaLabel,
29851
+ flags,
29827
29852
  range
29828
29853
  } = props;
29829
29854
  const [point1, point2] = points;
29855
+ const hasAria = ariaLabel && (flags == null || (_flags$mafs = flags["mafs"]) == null ? void 0 : _flags$mafs["locked-figures-aria"]);
29830
29856
  let line;
29831
29857
  if (kind === "ray") {
29832
29858
  // Rays extend to the end of the graph in one direction.
@@ -29866,7 +29892,9 @@ const LockedLine = props => {
29866
29892
  }), endArrowHead);
29867
29893
  }
29868
29894
  return /*#__PURE__*/React.createElement("g", {
29869
- className: kind === "ray" ? "locked-ray" : "locked-line"
29895
+ className: kind === "ray" ? "locked-ray" : "locked-line",
29896
+ "aria-label": hasAria ? ariaLabel : undefined,
29897
+ "aria-hidden": !hasAria
29870
29898
  }, line, showPoint1 && /*#__PURE__*/React.createElement(Point$2, {
29871
29899
  x: point1.coord[X],
29872
29900
  y: point1.coord[Y],
@@ -29891,13 +29919,21 @@ const LockedLine = props => {
29891
29919
  };
29892
29920
 
29893
29921
  const LockedPoint = props => {
29922
+ var _flags$mafs;
29894
29923
  const {
29924
+ flags,
29895
29925
  color: color$1,
29896
29926
  coord,
29897
- filled
29927
+ filled,
29928
+ ariaLabel
29898
29929
  } = props;
29899
29930
  const [x, y] = coord;
29900
- return /*#__PURE__*/React.createElement(Point$2, {
29931
+ const hasAria = ariaLabel && (flags == null || (_flags$mafs = flags["mafs"]) == null ? void 0 : _flags$mafs["locked-figures-aria"]);
29932
+ return /*#__PURE__*/React.createElement("g", {
29933
+ className: "locked-point",
29934
+ "aria-label": hasAria ? ariaLabel : undefined,
29935
+ "aria-hidden": !hasAria
29936
+ }, /*#__PURE__*/React.createElement(Point$2, {
29901
29937
  x: x,
29902
29938
  y: y,
29903
29939
  svgCircleProps: {
@@ -29907,10 +29943,11 @@ const LockedPoint = props => {
29907
29943
  strokeWidth: spacing.xxxxSmall_2
29908
29944
  }
29909
29945
  }
29910
- });
29946
+ }));
29911
29947
  };
29912
29948
 
29913
29949
  const LockedPolygon = props => {
29950
+ var _props$flags;
29914
29951
  const {
29915
29952
  points,
29916
29953
  color: color$1,
@@ -29918,8 +29955,11 @@ const LockedPolygon = props => {
29918
29955
  fillStyle,
29919
29956
  strokeStyle
29920
29957
  } = props;
29958
+ const hasAria = props.ariaLabel && ((_props$flags = props.flags) == null || (_props$flags = _props$flags["mafs"]) == null ? void 0 : _props$flags["locked-figures-aria"]);
29921
29959
  return /*#__PURE__*/React.createElement("g", {
29922
- className: "locked-polygon"
29960
+ className: "locked-polygon",
29961
+ "aria-label": hasAria ? props.ariaLabel : undefined,
29962
+ "aria-hidden": !hasAria
29923
29963
  }, /*#__PURE__*/React.createElement(Polygon, {
29924
29964
  points: [...points],
29925
29965
  fillOpacity: lockedFigureFillStyles[fillStyle],
@@ -29943,13 +29983,19 @@ const LockedPolygon = props => {
29943
29983
  };
29944
29984
 
29945
29985
  const LockedVector = props => {
29986
+ var _flags$mafs;
29946
29987
  const {
29947
29988
  color,
29948
- points
29989
+ points,
29990
+ ariaLabel,
29991
+ flags
29949
29992
  } = props;
29950
29993
  const [tail, tip] = points;
29994
+ const hasAria = ariaLabel && (flags == null || (_flags$mafs = flags["mafs"]) == null ? void 0 : _flags$mafs["locked-figures-aria"]);
29951
29995
  return /*#__PURE__*/React.createElement("g", {
29952
- className: "locked-vector"
29996
+ className: "locked-vector",
29997
+ "aria-label": hasAria ? ariaLabel : undefined,
29998
+ "aria-hidden": !hasAria
29953
29999
  }, /*#__PURE__*/React.createElement(Vector, {
29954
30000
  tail: tail,
29955
30001
  tip: tip,
@@ -29959,6 +30005,7 @@ const LockedVector = props => {
29959
30005
 
29960
30006
  const GraphLockedLayer = props => {
29961
30007
  const {
30008
+ flags,
29962
30009
  lockedFigures
29963
30010
  } = props;
29964
30011
  return /*#__PURE__*/React.createElement(React.Fragment, null, lockedFigures.map((figure, index) => {
@@ -29966,28 +30013,40 @@ const GraphLockedLayer = props => {
29966
30013
  case "point":
29967
30014
  return /*#__PURE__*/React.createElement(LockedPoint, _extends({
29968
30015
  key: `point-${index}`
29969
- }, figure));
30016
+ }, figure, {
30017
+ flags: flags
30018
+ }));
29970
30019
  case "line":
29971
30020
  return /*#__PURE__*/React.createElement(LockedLine, _extends({
29972
30021
  key: `line-${index}`,
29973
30022
  range: props.range
29974
- }, figure));
30023
+ }, figure, {
30024
+ flags: flags
30025
+ }));
29975
30026
  case "vector":
29976
30027
  return /*#__PURE__*/React.createElement(LockedVector, _extends({
29977
30028
  key: `vector-${index}`
29978
- }, figure));
30029
+ }, figure, {
30030
+ flags: flags
30031
+ }));
29979
30032
  case "ellipse":
29980
30033
  return /*#__PURE__*/React.createElement(LockedEllipse, _extends({
29981
30034
  key: `ellipse-${index}`
29982
- }, figure));
30035
+ }, figure, {
30036
+ flags: flags
30037
+ }));
29983
30038
  case "polygon":
29984
30039
  return /*#__PURE__*/React.createElement(LockedPolygon, _extends({
29985
30040
  key: `polygon-${index}`
29986
- }, figure));
30041
+ }, figure, {
30042
+ flags: flags
30043
+ }));
29987
30044
  case "function":
29988
30045
  return /*#__PURE__*/React.createElement(LockedFunction, _extends({
29989
30046
  key: `function-${index}`
29990
- }, figure));
30047
+ }, figure, {
30048
+ flags: flags
30049
+ }));
29991
30050
  case "label":
29992
30051
  // This is rendered outside the SVG element, since
29993
30052
  // TeX cannot be rendered inside an SVG.
@@ -31453,7 +31512,7 @@ function QuadraticGraph(props) {
31453
31512
  // to content creators the currently selected "correct answer". ex: y = 0.200x^2 + 0.000x + 0.000
31454
31513
  // While we should technically never have invalid coordinates, we want to ensure that we have a fallback
31455
31514
  const coeffRef = React.useRef([0, 0, 0]);
31456
- const coeffs = getQuadraticCoefficients(coords);
31515
+ const coeffs = getQuadraticCoefficients$1(coords);
31457
31516
  if (coeffs !== undefined) {
31458
31517
  coeffRef.current = coeffs;
31459
31518
  }
@@ -31477,7 +31536,7 @@ function QuadraticGraph(props) {
31477
31536
  // These equations were originally set up in 2013 and may require some
31478
31537
  // additional comments to help clarify the quadratic formula manipulations
31479
31538
  // Origin: https://phabricator.khanacademy.org/D2413
31480
- const getQuadraticCoefficients = coords => {
31539
+ const getQuadraticCoefficients$1 = coords => {
31481
31540
  const p1 = coords[0];
31482
31541
  const p2 = coords[1];
31483
31542
  const p3 = coords[2];
@@ -31517,7 +31576,7 @@ function SinusoidGraph(props) {
31517
31576
  phase: 1,
31518
31577
  verticalOffset: 0
31519
31578
  });
31520
- const coeffs = getSinusoidCoefficients(coords);
31579
+ const coeffs = getSinusoidCoefficients$1(coords);
31521
31580
 
31522
31581
  // If the coefficients are valid, update the reference
31523
31582
  if (coeffs !== undefined) {
@@ -31546,7 +31605,7 @@ sinusoidCoefficients) {
31546
31605
  } = sinusoidCoefficients;
31547
31606
  return a * Math.sin(b * x - c) + d;
31548
31607
  };
31549
- const getSinusoidCoefficients = coords => {
31608
+ const getSinusoidCoefficients$1 = coords => {
31550
31609
  // It's assumed that p1 is the root and p2 is the first peak
31551
31610
  const p1 = coords[0];
31552
31611
  const p2 = coords[1];
@@ -32007,6 +32066,7 @@ const MafsGraph = props => {
32007
32066
  })),
32008
32067
  // Only render the axis ticks and arrows if the markings are set to a full "graph"
32009
32068
  props.markings === "graph" && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(AxisTicks, null), /*#__PURE__*/React.createElement(AxisArrows, null)), /*#__PURE__*/React.createElement("svg", nestedSVGAttributes, props.lockedFigures && /*#__PURE__*/React.createElement(GraphLockedLayer, {
32069
+ flags: props.flags,
32010
32070
  lockedFigures: props.lockedFigures,
32011
32071
  range: state.range
32012
32072
  }))), ((_props$flags = props.flags) == null || (_props$flags = _props$flags["mafs"]) == null ? void 0 : _props$flags["interactive-graph-locked-features-labels"]) && props.lockedFigures && /*#__PURE__*/React.createElement(GraphLockedLabelsLayer, {
@@ -32262,6 +32322,83 @@ const renderGraph = props => {
32262
32322
  }
32263
32323
  };
32264
32324
 
32325
+ // Converts the state of a StatefulMafsGraph back to the format used to
32326
+ // represent graph state in the widget JSON.
32327
+ //
32328
+ // Rather than be tightly bound to how data was structured in
32329
+ // the legacy interactive graph, this lets us store state
32330
+ // however we want and we just transform it before handing it off
32331
+ // the the parent InteractiveGraph.
32332
+ //
32333
+ // The transformed data is used in the interactive graph widget editor, to
32334
+ // set the `correct` field of the graph options. In the learner-facing UI, the
32335
+ // data is also stored by the Renderer, and passed back down to the graph
32336
+ // widget via the `graph` prop. Because the data returned by this function
32337
+ // completely replaces the Renderer's representation of the widget, rather than
32338
+ // being merged into it, we take it upon ourselves to merge in the original
32339
+ // widget data here. If we didn't do this merging, the graph's configuration
32340
+ // would reset to the defaults when the learner interacted with it.
32341
+ function mafsStateToInteractiveGraph(state, originalGraph) {
32342
+ switch (state.type) {
32343
+ case "angle":
32344
+ invariant(originalGraph.type === "angle");
32345
+ return _extends({}, originalGraph, {
32346
+ coords: state.coords
32347
+ });
32348
+ case "quadratic":
32349
+ invariant(originalGraph.type === "quadratic");
32350
+ return _extends({}, originalGraph, {
32351
+ coords: state.coords
32352
+ });
32353
+ case "circle":
32354
+ invariant(originalGraph.type === "circle");
32355
+ return _extends({}, originalGraph, {
32356
+ center: state.center,
32357
+ radius: getRadius(state)
32358
+ });
32359
+ case "linear":
32360
+ invariant(originalGraph.type === "linear");
32361
+ return _extends({}, originalGraph, {
32362
+ coords: state.coords
32363
+ });
32364
+ case "ray":
32365
+ invariant(originalGraph.type === "ray");
32366
+ return _extends({}, originalGraph, {
32367
+ coords: state.coords
32368
+ });
32369
+ case "sinusoid":
32370
+ invariant(originalGraph.type === "sinusoid");
32371
+ return _extends({}, originalGraph, {
32372
+ coords: state.coords
32373
+ });
32374
+ case "segment":
32375
+ invariant(originalGraph.type === "segment");
32376
+ return _extends({}, originalGraph, {
32377
+ coords: state.coords
32378
+ });
32379
+ case "linear-system":
32380
+ invariant(originalGraph.type === "linear-system");
32381
+ return _extends({}, originalGraph, {
32382
+ coords: state.coords
32383
+ });
32384
+ case "polygon":
32385
+ invariant(originalGraph.type === "polygon");
32386
+ return _extends({}, originalGraph, {
32387
+ coords: state.coords
32388
+ });
32389
+ case "point":
32390
+ invariant(originalGraph.type === "point");
32391
+ return _extends({}, originalGraph, {
32392
+ coords: state.coords
32393
+ });
32394
+ case "none":
32395
+ invariant(originalGraph.type === "none");
32396
+ return _extends({}, originalGraph);
32397
+ default:
32398
+ throw new UnreachableCaseError(state);
32399
+ }
32400
+ }
32401
+
32265
32402
  function initializeGraphState(params) {
32266
32403
  var _graph$snapTo;
32267
32404
  const {
@@ -32887,7 +33024,7 @@ function doMovePoint(state, action) {
32887
33024
  // Bind the new destination to the graph range/snapStep and then get the quadratic coefficients
32888
33025
  const boundDestination = boundAndSnapToGrid(action.destination, state);
32889
33026
  _newCoords2[action.index] = boundDestination;
32890
- const QuadraticCoefficients = getQuadraticCoefficients(_newCoords2);
33027
+ const QuadraticCoefficients = getQuadraticCoefficients$1(_newCoords2);
32891
33028
 
32892
33029
  // If the new destination results in an invalid quadratic equation, we don't want to move the point
32893
33030
  if (QuadraticCoefficients === undefined) {
@@ -33034,11 +33171,11 @@ const getChange = (coords, delta, constraintOpts) => {
33034
33171
  const [dx, dy] = getDeltaVertex(maxMoves, minMoves, delta);
33035
33172
  return [dx, dy];
33036
33173
  };
33037
- const eq$1 = Util.eq;
33174
+ const eq$2 = Util.eq;
33038
33175
 
33039
33176
  // Less than or approximately equal
33040
33177
  function leq$1(a, b) {
33041
- return a < b || eq$1(a, b);
33178
+ return a < b || eq$2(a, b);
33042
33179
  }
33043
33180
  function boundAndSnapToGrid(point, {
33044
33181
  snapStep,
@@ -33341,20 +33478,6 @@ function setAtIndex(args) {
33341
33478
  return copy;
33342
33479
  }
33343
33480
 
33344
- // Rather than be tightly bound to how data was structured in
33345
- // the legacy interactive graph, this lets us store state
33346
- // however we want and we just transform it before handing it off
33347
- // the the parent InteractiveGraph
33348
- function mafsStateToInteractiveGraph(state) {
33349
- if (state.graph.type === "circle") {
33350
- return _extends({}, state, {
33351
- graph: _extends({}, state.graph, {
33352
- radius: getRadius(state.graph)
33353
- })
33354
- });
33355
- }
33356
- return _extends({}, state);
33357
- }
33358
33481
  const StatefulMafsGraph = /*#__PURE__*/React.forwardRef((props, ref) => {
33359
33482
  const {
33360
33483
  onChange,
@@ -33367,12 +33490,12 @@ const StatefulMafsGraph = /*#__PURE__*/React.forwardRef((props, ref) => {
33367
33490
  const prevState = useRef(state);
33368
33491
  useEffect(() => {
33369
33492
  if (prevState.current !== state) {
33370
- onChange(mafsStateToInteractiveGraph({
33371
- graph: state
33372
- }));
33493
+ onChange({
33494
+ graph: mafsStateToInteractiveGraph(state, graph)
33495
+ });
33373
33496
  }
33374
33497
  prevState.current = state;
33375
- }, [onChange, state]);
33498
+ }, [onChange, state, graph]);
33376
33499
 
33377
33500
  // Destructuring first to keep useEffect from making excess calls
33378
33501
  const [xSnap, ySnap] = props.snapStep;
@@ -33424,12 +33547,234 @@ const StatefulMafsGraph = /*#__PURE__*/React.forwardRef((props, ref) => {
33424
33547
  }));
33425
33548
  });
33426
33549
 
33550
+ const eq$1 = Util.eq;
33551
+ const deepEq$1 = Util.deepEq;
33552
+ function interactiveGraphValidator(userInput, rubric) {
33553
+ // None-type graphs are not graded
33554
+ if (userInput.type === "none" && rubric.correct.type === "none") {
33555
+ return {
33556
+ type: "points",
33557
+ earned: 0,
33558
+ total: 0,
33559
+ message: null
33560
+ };
33561
+ }
33562
+
33563
+ // When nothing has moved, there will neither be coords nor the
33564
+ // circle's center/radius fields. When those fields are absent, skip
33565
+ // all these checks; just go mark the answer as empty.
33566
+ const hasValue = Boolean(
33567
+ // @ts-expect-error - TS2339 - Property 'coords' does not exist on type 'PerseusGraphType'.
33568
+ userInput.coords ||
33569
+ // @ts-expect-error - TS2339 - Property 'center' does not exist on type 'PerseusGraphType'. | TS2339 - Property 'radius' does not exist on type 'PerseusGraphType'.
33570
+ userInput.center && userInput.radius);
33571
+ if (userInput.type === rubric.correct.type && hasValue) {
33572
+ if (userInput.type === "linear" && rubric.correct.type === "linear" && userInput.coords != null) {
33573
+ const guess = userInput.coords;
33574
+ const correct = rubric.correct.coords;
33575
+
33576
+ // If both of the guess points are on the correct line, it's
33577
+ // correct.
33578
+ if (
33579
+ // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
33580
+ collinear(correct[0], correct[1], guess[0]) &&
33581
+ // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
33582
+ collinear(correct[0], correct[1], guess[1])) {
33583
+ return {
33584
+ type: "points",
33585
+ earned: 1,
33586
+ total: 1,
33587
+ message: null
33588
+ };
33589
+ }
33590
+ } else if (userInput.type === "linear-system" && rubric.correct.type === "linear-system" && userInput.coords != null) {
33591
+ const guess = userInput.coords;
33592
+ const correct = rubric.correct.coords;
33593
+ if (collinear(correct[0][0], correct[0][1], guess[0][0]) && collinear(correct[0][0], correct[0][1], guess[0][1]) && collinear(correct[1][0], correct[1][1], guess[1][0]) && collinear(correct[1][0], correct[1][1], guess[1][1]) || collinear(correct[0][0], correct[0][1], guess[1][0]) && collinear(correct[0][0], correct[0][1], guess[1][1]) && collinear(correct[1][0], correct[1][1], guess[0][0]) && collinear(correct[1][0], correct[1][1], guess[0][1])) {
33594
+ return {
33595
+ type: "points",
33596
+ earned: 1,
33597
+ total: 1,
33598
+ message: null
33599
+ };
33600
+ }
33601
+ } else if (userInput.type === "quadratic" && rubric.correct.type === "quadratic" && userInput.coords != null) {
33602
+ // If the parabola coefficients match, it's correct.
33603
+ const guessCoeffs = getQuadraticCoefficients(userInput.coords);
33604
+ const correctCoeffs = getQuadraticCoefficients(
33605
+ // @ts-expect-error - TS2345 - Argument of type 'readonly Coord[] | undefined' is not assignable to parameter of type 'readonly Coord[]'.
33606
+ rubric.correct.coords);
33607
+ if (deepEq$1(guessCoeffs, correctCoeffs)) {
33608
+ return {
33609
+ type: "points",
33610
+ earned: 1,
33611
+ total: 1,
33612
+ message: null
33613
+ };
33614
+ }
33615
+ } else if (userInput.type === "sinusoid" && rubric.correct.type === "sinusoid" && userInput.coords != null) {
33616
+ const guessCoeffs = getSinusoidCoefficients(userInput.coords);
33617
+ const correctCoeffs = getSinusoidCoefficients(
33618
+ // @ts-expect-error - TS2345 - Argument of type 'readonly Coord[] | undefined' is not assignable to parameter of type 'readonly Coord[]'.
33619
+ rubric.correct.coords);
33620
+ const canonicalGuessCoeffs = canonicalSineCoefficients$1(guessCoeffs);
33621
+ const canonicalCorrectCoeffs = canonicalSineCoefficients$1(correctCoeffs);
33622
+ // If the canonical coefficients match, it's correct.
33623
+ if (deepEq$1(canonicalGuessCoeffs, canonicalCorrectCoeffs)) {
33624
+ return {
33625
+ type: "points",
33626
+ earned: 1,
33627
+ total: 1,
33628
+ message: null
33629
+ };
33630
+ }
33631
+ } else if (userInput.type === "circle" && rubric.correct.type === "circle") {
33632
+ if (deepEq$1(userInput.center, rubric.correct.center) && eq$1(userInput.radius, rubric.correct.radius)) {
33633
+ return {
33634
+ type: "points",
33635
+ earned: 1,
33636
+ total: 1,
33637
+ message: null
33638
+ };
33639
+ }
33640
+ } else if (userInput.type === "point" && rubric.correct.type === "point" && userInput.coords != null) {
33641
+ let correct = rubric.correct.coords;
33642
+ if (correct == null) {
33643
+ throw new Error("Point graph rubric has null coords");
33644
+ }
33645
+ const guess = userInput.coords.slice();
33646
+ correct = correct.slice();
33647
+ // Everything's already rounded so we shouldn't need to do an
33648
+ // eq() comparison but _.isEqual(0, -0) is false, so we'll use
33649
+ // eq() anyway. The sort should be fine because it'll stringify
33650
+ // it and -0 converted to a string is "0"
33651
+ guess == null || guess.sort();
33652
+ // @ts-expect-error - TS2339 - Property 'sort' does not exist on type 'readonly Coord[]'.
33653
+ correct.sort();
33654
+ if (deepEq$1(guess, correct)) {
33655
+ return {
33656
+ type: "points",
33657
+ earned: 1,
33658
+ total: 1,
33659
+ message: null
33660
+ };
33661
+ }
33662
+ } else if (userInput.type === "polygon" && rubric.correct.type === "polygon" && userInput.coords != null) {
33663
+ var _userInput$coords, _rubric$correct$coord;
33664
+ const guess = (_userInput$coords = userInput.coords) == null ? void 0 : _userInput$coords.slice();
33665
+ // @ts-expect-error - TS2322 - Type 'Coord[] | undefined' is not assignable to type 'Coord[]'.
33666
+ const correct = (_rubric$correct$coord = rubric.correct.coords) == null ? void 0 : _rubric$correct$coord.slice();
33667
+ let match;
33668
+ if (rubric.correct.match === "similar") {
33669
+ match = similar(guess, correct, Number.POSITIVE_INFINITY);
33670
+ } else if (rubric.correct.match === "congruent") {
33671
+ match = similar(guess, correct, number.DEFAULT_TOLERANCE);
33672
+ } else if (rubric.correct.match === "approx") {
33673
+ match = similar(guess, correct, 0.1);
33674
+ } else {
33675
+ /* exact */
33676
+ guess.sort();
33677
+ correct.sort();
33678
+ match = deepEq$1(guess, correct);
33679
+ }
33680
+ if (match) {
33681
+ return {
33682
+ type: "points",
33683
+ earned: 1,
33684
+ total: 1,
33685
+ message: null
33686
+ };
33687
+ }
33688
+ } else if (userInput.type === "segment" && rubric.correct.type === "segment" && userInput.coords != null) {
33689
+ var _rubric$correct;
33690
+ let guess = Util.deepClone(userInput.coords);
33691
+ let correct = Util.deepClone((_rubric$correct = rubric.correct) == null ? void 0 : _rubric$correct.coords);
33692
+ guess = _.invoke(guess, "sort").sort();
33693
+ // @ts-expect-error - TS2345 - Argument of type '(readonly Coord[])[] | undefined' is not assignable to parameter of type 'Collection<any>'.
33694
+ correct = _.invoke(correct, "sort").sort();
33695
+ if (deepEq$1(guess, correct)) {
33696
+ return {
33697
+ type: "points",
33698
+ earned: 1,
33699
+ total: 1,
33700
+ message: null
33701
+ };
33702
+ }
33703
+ } else if (userInput.type === "ray" && rubric.correct.type === "ray" && userInput.coords != null) {
33704
+ const guess = userInput.coords;
33705
+ const correct = rubric.correct.coords;
33706
+ if (
33707
+ // @ts-expect-error - TS2532 - Object is possibly 'undefined'.
33708
+ deepEq$1(guess[0], correct[0]) &&
33709
+ // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
33710
+ collinear(correct[0], correct[1], guess[1])) {
33711
+ return {
33712
+ type: "points",
33713
+ earned: 1,
33714
+ total: 1,
33715
+ message: null
33716
+ };
33717
+ }
33718
+ } else if (userInput.type === "angle" && rubric.correct.type === "angle") {
33719
+ const guess = userInput.coords;
33720
+ const correct = rubric.correct.coords;
33721
+ let match;
33722
+ if (rubric.correct.match === "congruent") {
33723
+ const angles = _.map([guess, correct], function (coords) {
33724
+ const angle = GraphUtils.findAngle(
33725
+ // @ts-expect-error - TS2532 - Object is possibly 'undefined'.
33726
+ coords[2],
33727
+ // @ts-expect-error - TS2532 - Object is possibly 'undefined'.
33728
+ coords[0],
33729
+ // @ts-expect-error - TS2532 - Object is possibly 'undefined'.
33730
+ coords[1]);
33731
+ return (angle + 360) % 360;
33732
+ });
33733
+ // @ts-expect-error - TS2556 - A spread argument must either have a tuple type or be passed to a rest parameter.
33734
+ match = eq$1(...angles);
33735
+ } else {
33736
+ /* exact */
33737
+ match =
33738
+ // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
33739
+ deepEq$1(guess[1], correct[1]) &&
33740
+ // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
33741
+ collinear(correct[1], correct[0], guess[0]) &&
33742
+ // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
33743
+ collinear(correct[1], correct[2], guess[2]);
33744
+ }
33745
+ if (match) {
33746
+ return {
33747
+ type: "points",
33748
+ earned: 1,
33749
+ total: 1,
33750
+ message: null
33751
+ };
33752
+ }
33753
+ }
33754
+ }
33755
+
33756
+ // The input wasn't correct, so check if it's a blank input or if it's
33757
+ // actually just wrong
33758
+ if (!hasValue || _.isEqual(userInput, rubric.graph)) {
33759
+ // We're where we started.
33760
+ return {
33761
+ type: "invalid",
33762
+ message: null
33763
+ };
33764
+ }
33765
+ return {
33766
+ type: "points",
33767
+ earned: 0,
33768
+ total: 1,
33769
+ message: null
33770
+ };
33771
+ }
33772
+
33427
33773
  const TRASH_ICON_URI = "https://ka-perseus-graphie.s3.amazonaws.com/b1452c0d79fd0f7ff4c3af9488474a0a0decb361.png";
33428
33774
  const defaultBackgroundImage = {
33429
33775
  url: null
33430
33776
  };
33431
33777
  const eq = Util.eq;
33432
- const deepEq$1 = Util.deepEq;
33433
33778
  const UNLIMITED = "unlimited";
33434
33779
 
33435
33780
  // Sample background image:
@@ -33470,6 +33815,39 @@ const makeInvalidTypeError = (functionName, graphType) => {
33470
33815
 
33471
33816
  // There's no transform function in exports
33472
33817
 
33818
+ // TODO: there's another, very similar getSinusoidCoefficients function
33819
+ // they should probably be merged
33820
+ function getSinusoidCoefficients(coords) {
33821
+ // It's assumed that p1 is the root and p2 is the first peak
33822
+ const p1 = coords[0];
33823
+ const p2 = coords[1];
33824
+
33825
+ // Resulting coefficients are canonical for this sine curve
33826
+ const amplitude = p2[1] - p1[1];
33827
+ const angularFrequency = Math.PI / (2 * (p2[0] - p1[0]));
33828
+ const phase = p1[0] * angularFrequency;
33829
+ const verticalOffset = p1[1];
33830
+ return [amplitude, angularFrequency, phase, verticalOffset];
33831
+ }
33832
+
33833
+ // TODO: there's another, very similar getQuadraticCoefficients function
33834
+ // they should probably be merged
33835
+ function getQuadraticCoefficients(coords) {
33836
+ const p1 = coords[0];
33837
+ const p2 = coords[1];
33838
+ const p3 = coords[2];
33839
+ const denom = (p1[0] - p2[0]) * (p1[0] - p3[0]) * (p2[0] - p3[0]);
33840
+ if (denom === 0) {
33841
+ // Many of the callers assume that the return value is always defined.
33842
+ // @ts-expect-error - TS2322 - Type 'undefined' is not assignable to type 'QuadraticCoefficient'.
33843
+ return;
33844
+ }
33845
+ const a = (p3[0] * (p2[1] - p1[1]) + p2[0] * (p1[1] - p3[1]) + p1[0] * (p3[1] - p2[1])) / denom;
33846
+ const b = (p3[0] * p3[0] * (p1[1] - p2[1]) + p2[0] * p2[0] * (p3[1] - p1[1]) + p1[0] * p1[0] * (p2[1] - p3[1])) / denom;
33847
+ const c = (p2[0] * p3[0] * (p2[0] - p3[0]) * p1[1] + p3[0] * p1[0] * (p3[0] - p1[0]) * p2[1] + p1[0] * p2[0] * (p1[0] - p2[0]) * p3[1]) / denom;
33848
+ return [a, b, c];
33849
+ }
33850
+
33473
33851
  // (LEMS-2190): Move the Mafs Angle Graph coordinate reversal logic in interactive-graph-state.ts
33474
33852
  // to this file when we remove the legacy graph. This logic allows us to support bi-directional angles
33475
33853
  // for the new (non-reflexive) Mafs graphs, while maintaining the same scoring behaviour as the legacy graph.
@@ -34553,7 +34931,6 @@ class LegacyInteractiveGraph extends React.Component {
34553
34931
  $(this.angle).on("move", () => {
34554
34932
  var _this$angle;
34555
34933
  this.onChange({
34556
- // @ts-expect-error Type '{ coords: any; type: "angle"; showAngles?: boolean | undefined; allowReflexAngles?: boolean | undefined; angleOffsetDeg?: number | undefined; snapDegrees?: number | undefined; match?: "congruent" | undefined; }' is not assignable to type 'InteractiveGraphState | undefined'.
34557
34934
  graph: _extends({}, graph, {
34558
34935
  coords: (_this$angle = this.angle) == null ? void 0 : _this$angle.getClockwiseCoords()
34559
34936
  })
@@ -34627,7 +35004,7 @@ class LegacyInteractiveGraph extends React.Component {
34627
35004
  return InteractiveGraph.getUserInputFromProps(this.props);
34628
35005
  }
34629
35006
  simpleValidate(rubric) {
34630
- return InteractiveGraph.validate(this.getUserInput(), rubric, this);
35007
+ return interactiveGraphValidator(this.getUserInput(), rubric);
34631
35008
  }
34632
35009
  render() {
34633
35010
  const box = getInteractiveBoxFromSizeClass(this.props.containerSizeClass);
@@ -34706,7 +35083,7 @@ class InteractiveGraph extends React.Component {
34706
35083
  throw new PerseusError("Cannot getUserInput from a graph that has never rendered", Errors.NotAllowed);
34707
35084
  }
34708
35085
  simpleValidate(rubric) {
34709
- return InteractiveGraph.validate(this.getUserInput(), rubric, this);
35086
+ return interactiveGraphValidator(this.getUserInput(), rubric);
34710
35087
  }
34711
35088
  render() {
34712
35089
  var _this$props$apiOption;
@@ -34731,33 +35108,6 @@ class InteractiveGraph extends React.Component {
34731
35108
  ref: this.legacyGraphRef
34732
35109
  }, this.props));
34733
35110
  }
34734
- static getQuadraticCoefficients(coords) {
34735
- const p1 = coords[0];
34736
- const p2 = coords[1];
34737
- const p3 = coords[2];
34738
- const denom = (p1[0] - p2[0]) * (p1[0] - p3[0]) * (p2[0] - p3[0]);
34739
- if (denom === 0) {
34740
- // Many of the callers assume that the return value is always defined.
34741
- // @ts-expect-error - TS2322 - Type 'undefined' is not assignable to type 'QuadraticCoefficient'.
34742
- return;
34743
- }
34744
- const a = (p3[0] * (p2[1] - p1[1]) + p2[0] * (p1[1] - p3[1]) + p1[0] * (p3[1] - p2[1])) / denom;
34745
- const b = (p3[0] * p3[0] * (p1[1] - p2[1]) + p2[0] * p2[0] * (p3[1] - p1[1]) + p1[0] * p1[0] * (p2[1] - p3[1])) / denom;
34746
- const c = (p2[0] * p3[0] * (p2[0] - p3[0]) * p1[1] + p3[0] * p1[0] * (p3[0] - p1[0]) * p2[1] + p1[0] * p2[0] * (p1[0] - p2[0]) * p3[1]) / denom;
34747
- return [a, b, c];
34748
- }
34749
- static getSinusoidCoefficients(coords) {
34750
- // It's assumed that p1 is the root and p2 is the first peak
34751
- const p1 = coords[0];
34752
- const p2 = coords[1];
34753
-
34754
- // Resulting coefficients are canonical for this sine curve
34755
- const amplitude = p2[1] - p1[1];
34756
- const angularFrequency = Math.PI / (2 * (p2[0] - p1[0]));
34757
- const phase = p1[0] * angularFrequency;
34758
- const verticalOffset = p1[1];
34759
- return [amplitude, angularFrequency, phase, verticalOffset];
34760
- }
34761
35111
 
34762
35112
  /**
34763
35113
  * @param {object} graph Like props.graph or props.correct
@@ -34971,7 +35321,7 @@ class InteractiveGraph extends React.Component {
34971
35321
  const coords =
34972
35322
  // @ts-expect-error - TS2339 - Property 'coords' does not exist on type 'PerseusGraphType'.
34973
35323
  props.graph.coords || InteractiveGraph.defaultQuadraticCoords(props);
34974
- return InteractiveGraph.getQuadraticCoefficients(coords);
35324
+ return getQuadraticCoefficients(coords);
34975
35325
  }
34976
35326
  static defaultQuadraticCoords(props) {
34977
35327
  const coords = [[0.25, 0.75], [0.5, 0.25], [0.75, 0.75]];
@@ -34986,7 +35336,7 @@ class InteractiveGraph extends React.Component {
34986
35336
  const coords =
34987
35337
  // @ts-expect-error - TS2339 - Property 'coords' does not exist on type 'PerseusGraphType'.
34988
35338
  props.graph.coords || InteractiveGraph.defaultSinusoidCoords(props);
34989
- return InteractiveGraph.getSinusoidCoefficients(coords);
35339
+ return getSinusoidCoefficients(coords);
34990
35340
  }
34991
35341
  static defaultSinusoidCoords(props) {
34992
35342
  const coords = [[0.5, 0.5], [0.65, 0.6]];
@@ -35066,222 +35416,8 @@ class InteractiveGraph extends React.Component {
35066
35416
  const angle = GraphUtils.findAngle(coords[2], coords[0], coords[1]);
35067
35417
  return angle.toFixed(0) + "\u00B0 angle" + " at (" + coords[1].join(", ") + ")";
35068
35418
  }
35069
- static validate(userInput, rubric, component) {
35070
- // None-type graphs are not graded
35071
- if (userInput.type === "none" && rubric.correct.type === "none") {
35072
- return {
35073
- type: "points",
35074
- earned: 0,
35075
- total: 0,
35076
- message: null
35077
- };
35078
- }
35079
-
35080
- // When nothing has moved, there will neither be coords nor the
35081
- // circle's center/radius fields. When those fields are absent, skip
35082
- // all these checks; just go mark the answer as empty.
35083
- const hasValue = Boolean(
35084
- // @ts-expect-error - TS2339 - Property 'coords' does not exist on type 'PerseusGraphType'.
35085
- userInput.coords ||
35086
- // @ts-expect-error - TS2339 - Property 'center' does not exist on type 'PerseusGraphType'. | TS2339 - Property 'radius' does not exist on type 'PerseusGraphType'.
35087
- userInput.center && userInput.radius);
35088
- if (userInput.type === rubric.correct.type && hasValue) {
35089
- if (userInput.type === "linear" && rubric.correct.type === "linear" && userInput.coords != null) {
35090
- const guess = userInput.coords;
35091
- const correct = rubric.correct.coords;
35092
-
35093
- // If both of the guess points are on the correct line, it's
35094
- // correct.
35095
- if (
35096
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
35097
- collinear(correct[0], correct[1], guess[0]) &&
35098
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
35099
- collinear(correct[0], correct[1], guess[1])) {
35100
- return {
35101
- type: "points",
35102
- earned: 1,
35103
- total: 1,
35104
- message: null
35105
- };
35106
- }
35107
- } else if (userInput.type === "linear-system" && rubric.correct.type === "linear-system" && userInput.coords != null) {
35108
- const guess = userInput.coords;
35109
- const correct = rubric.correct.coords;
35110
- if (collinear(correct[0][0], correct[0][1], guess[0][0]) && collinear(correct[0][0], correct[0][1], guess[0][1]) && collinear(correct[1][0], correct[1][1], guess[1][0]) && collinear(correct[1][0], correct[1][1], guess[1][1]) || collinear(correct[0][0], correct[0][1], guess[1][0]) && collinear(correct[0][0], correct[0][1], guess[1][1]) && collinear(correct[1][0], correct[1][1], guess[0][0]) && collinear(correct[1][0], correct[1][1], guess[0][1])) {
35111
- return {
35112
- type: "points",
35113
- earned: 1,
35114
- total: 1,
35115
- message: null
35116
- };
35117
- }
35118
- } else if (userInput.type === "quadratic" && rubric.correct.type === "quadratic" && userInput.coords != null) {
35119
- // If the parabola coefficients match, it's correct.
35120
- const guessCoeffs = this.getQuadraticCoefficients(userInput.coords);
35121
- const correctCoeffs = this.getQuadraticCoefficients(
35122
- // @ts-expect-error - TS2345 - Argument of type 'readonly Coord[] | undefined' is not assignable to parameter of type 'readonly Coord[]'.
35123
- rubric.correct.coords);
35124
- if (deepEq$1(guessCoeffs, correctCoeffs)) {
35125
- return {
35126
- type: "points",
35127
- earned: 1,
35128
- total: 1,
35129
- message: null
35130
- };
35131
- }
35132
- } else if (userInput.type === "sinusoid" && rubric.correct.type === "sinusoid" && userInput.coords != null) {
35133
- const guessCoeffs = this.getSinusoidCoefficients(userInput.coords);
35134
- const correctCoeffs = this.getSinusoidCoefficients(
35135
- // @ts-expect-error - TS2345 - Argument of type 'readonly Coord[] | undefined' is not assignable to parameter of type 'readonly Coord[]'.
35136
- rubric.correct.coords);
35137
- const canonicalGuessCoeffs = canonicalSineCoefficients$1(guessCoeffs);
35138
- const canonicalCorrectCoeffs = canonicalSineCoefficients$1(correctCoeffs);
35139
- // If the canonical coefficients match, it's correct.
35140
- if (deepEq$1(canonicalGuessCoeffs, canonicalCorrectCoeffs)) {
35141
- return {
35142
- type: "points",
35143
- earned: 1,
35144
- total: 1,
35145
- message: null
35146
- };
35147
- }
35148
- } else if (userInput.type === "circle" && rubric.correct.type === "circle") {
35149
- if (deepEq$1(userInput.center, rubric.correct.center) && eq(userInput.radius, rubric.correct.radius)) {
35150
- return {
35151
- type: "points",
35152
- earned: 1,
35153
- total: 1,
35154
- message: null
35155
- };
35156
- }
35157
- } else if (userInput.type === "point" && rubric.correct.type === "point" && userInput.coords != null) {
35158
- let correct = InteractiveGraph.getPointCoords(rubric.correct, component);
35159
- const guess = userInput.coords.slice();
35160
- correct = correct.slice();
35161
- // Everything's already rounded so we shouldn't need to do an
35162
- // eq() comparison but _.isEqual(0, -0) is false, so we'll use
35163
- // eq() anyway. The sort should be fine because it'll stringify
35164
- // it and -0 converted to a string is "0"
35165
- guess == null || guess.sort();
35166
- // @ts-expect-error - TS2339 - Property 'sort' does not exist on type 'readonly Coord[]'.
35167
- correct.sort();
35168
- if (deepEq$1(guess, correct)) {
35169
- return {
35170
- type: "points",
35171
- earned: 1,
35172
- total: 1,
35173
- message: null
35174
- };
35175
- }
35176
- } else if (userInput.type === "polygon" && rubric.correct.type === "polygon" && userInput.coords != null) {
35177
- var _userInput$coords, _rubric$correct$coord;
35178
- const guess = (_userInput$coords = userInput.coords) == null ? void 0 : _userInput$coords.slice();
35179
- // @ts-expect-error - TS2322 - Type 'Coord[] | undefined' is not assignable to type 'Coord[]'.
35180
- const correct = (_rubric$correct$coord = rubric.correct.coords) == null ? void 0 : _rubric$correct$coord.slice();
35181
- let match;
35182
- if (rubric.correct.match === "similar") {
35183
- match = similar(guess, correct, Number.POSITIVE_INFINITY);
35184
- } else if (rubric.correct.match === "congruent") {
35185
- match = similar(guess, correct, number.DEFAULT_TOLERANCE);
35186
- } else if (rubric.correct.match === "approx") {
35187
- match = similar(guess, correct, 0.1);
35188
- } else {
35189
- /* exact */
35190
- guess.sort();
35191
- correct.sort();
35192
- match = deepEq$1(guess, correct);
35193
- }
35194
- if (match) {
35195
- return {
35196
- type: "points",
35197
- earned: 1,
35198
- total: 1,
35199
- message: null
35200
- };
35201
- }
35202
- } else if (userInput.type === "segment" && rubric.correct.type === "segment" && userInput.coords != null) {
35203
- var _rubric$correct;
35204
- let guess = Util.deepClone(userInput.coords);
35205
- let correct = Util.deepClone((_rubric$correct = rubric.correct) == null ? void 0 : _rubric$correct.coords);
35206
- guess = _.invoke(guess, "sort").sort();
35207
- // @ts-expect-error - TS2345 - Argument of type '(readonly Coord[])[] | undefined' is not assignable to parameter of type 'Collection<any>'.
35208
- correct = _.invoke(correct, "sort").sort();
35209
- if (deepEq$1(guess, correct)) {
35210
- return {
35211
- type: "points",
35212
- earned: 1,
35213
- total: 1,
35214
- message: null
35215
- };
35216
- }
35217
- } else if (userInput.type === "ray" && rubric.correct.type === "ray" && userInput.coords != null) {
35218
- const guess = userInput.coords;
35219
- const correct = rubric.correct.coords;
35220
- if (
35221
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'.
35222
- deepEq$1(guess[0], correct[0]) &&
35223
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
35224
- collinear(correct[0], correct[1], guess[1])) {
35225
- return {
35226
- type: "points",
35227
- earned: 1,
35228
- total: 1,
35229
- message: null
35230
- };
35231
- }
35232
- } else if (userInput.type === "angle" && rubric.correct.type === "angle") {
35233
- const guess = userInput.coords;
35234
- const correct = rubric.correct.coords;
35235
- let match;
35236
- if (rubric.correct.match === "congruent") {
35237
- const angles = _.map([guess, correct], function (coords) {
35238
- const angle = GraphUtils.findAngle(
35239
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'.
35240
- coords[2],
35241
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'.
35242
- coords[0],
35243
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'.
35244
- coords[1]);
35245
- return (angle + 360) % 360;
35246
- });
35247
- // @ts-expect-error - TS2556 - A spread argument must either have a tuple type or be passed to a rest parameter.
35248
- match = eq(...angles);
35249
- } else {
35250
- /* exact */
35251
- match =
35252
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
35253
- deepEq$1(guess[1], correct[1]) &&
35254
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
35255
- collinear(correct[1], correct[0], guess[0]) &&
35256
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
35257
- collinear(correct[1], correct[2], guess[2]);
35258
- }
35259
- if (match) {
35260
- return {
35261
- type: "points",
35262
- earned: 1,
35263
- total: 1,
35264
- message: null
35265
- };
35266
- }
35267
- }
35268
- }
35269
-
35270
- // The input wasn't correct, so check if it's a blank input or if it's
35271
- // actually just wrong
35272
- if (!hasValue || _.isEqual(userInput, rubric.graph)) {
35273
- // We're where we started.
35274
- return {
35275
- type: "invalid",
35276
- message: null
35277
- };
35278
- }
35279
- return {
35280
- type: "points",
35281
- earned: 0,
35282
- total: 1,
35283
- message: null
35284
- };
35419
+ static validate(userInput, rubric) {
35420
+ return interactiveGraphValidator(userInput, rubric);
35285
35421
  }
35286
35422
  static getUserInputFromProps(props) {
35287
35423
  return props.graph;
@@ -37977,6 +38113,16 @@ const styles$d = StyleSheet.create({
37977
38113
  }
37978
38114
  });
37979
38115
 
38116
+ function matcherValidator(state, rubric) {
38117
+ const correct = _.isEqual(state.left, rubric.left) && _.isEqual(state.right, rubric.right);
38118
+ return {
38119
+ type: "points",
38120
+ earned: correct ? 1 : 0,
38121
+ total: 1,
38122
+ message: null
38123
+ };
38124
+ }
38125
+
37980
38126
  const {
37981
38127
  shuffle: shuffle$1,
37982
38128
  seededRNG
@@ -38040,9 +38186,11 @@ class Matcher extends React.Component {
38040
38186
  this.refs.right.moveOptionToIndex(option, index);
38041
38187
  };
38042
38188
  }
38189
+ static validate(state, rubric) {
38190
+ return matcherValidator(state, rubric);
38191
+ }
38043
38192
  simpleValidate(rubric) {
38044
- // @ts-expect-error - TS2339 - Property 'validate' does not exist on type 'typeof Matcher'.
38045
- return Matcher.validate(this.getUserInput(), rubric);
38193
+ return matcherValidator(this.getUserInput(), rubric);
38046
38194
  }
38047
38195
  render() {
38048
38196
  // To minimize layout shift, we display a spinner until our math
@@ -38149,17 +38297,6 @@ Matcher.defaultProps = {
38149
38297
  onChange: function () {},
38150
38298
  linterContext: linterContextDefault
38151
38299
  };
38152
- _.extend(Matcher, {
38153
- validate: function (state, rubric) {
38154
- const correct = _.isEqual(state.left, rubric.left) && _.isEqual(state.right, rubric.right);
38155
- return {
38156
- type: "points",
38157
- earned: correct ? 1 : 0,
38158
- total: 1,
38159
- message: null
38160
- };
38161
- }
38162
- });
38163
38300
  const padding = 5;
38164
38301
  const border = "1px solid #444";
38165
38302
  const styles$c = StyleSheet.create({
@@ -38195,7 +38332,59 @@ var Matcher$1 = {
38195
38332
  isLintable: true
38196
38333
  };
38197
38334
 
38198
- // Type imports
38335
+ function matrixValidator(state, rubric, strings) {
38336
+ const solution = rubric.answers;
38337
+ const supplied = state.answers;
38338
+ const solutionSize = getMatrixSize(solution);
38339
+ const suppliedSize = getMatrixSize(supplied);
38340
+ const incorrectSize = solutionSize[0] !== suppliedSize[0] || solutionSize[1] !== suppliedSize[1];
38341
+ const createValidator = KhanAnswerTypes.number.createValidatorFunctional;
38342
+ let message = null;
38343
+ let hasEmptyCell = false;
38344
+ let incorrect = false;
38345
+ _(suppliedSize[0]).times(row => {
38346
+ _(suppliedSize[1]).times(col => {
38347
+ if (supplied[row][col] == null || supplied[row][col].toString().length === 0) {
38348
+ hasEmptyCell = true;
38349
+ }
38350
+ if (!incorrectSize) {
38351
+ const validator = createValidator(
38352
+ // @ts-expect-error - TS2345 - Argument of type 'number' is not assignable to parameter of type 'string'.
38353
+ solution[row][col], {
38354
+ simplify: true
38355
+ }, strings);
38356
+ const result = validator(supplied[row][col]);
38357
+ if (result.message) {
38358
+ // @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'null'.
38359
+ message = result.message;
38360
+ }
38361
+ if (!result.correct) {
38362
+ incorrect = true;
38363
+ }
38364
+ }
38365
+ });
38366
+ });
38367
+ if (hasEmptyCell) {
38368
+ return {
38369
+ type: "invalid",
38370
+ message: strings.fillAllCells
38371
+ };
38372
+ }
38373
+ if (incorrectSize) {
38374
+ return {
38375
+ type: "points",
38376
+ earned: 0,
38377
+ total: 1,
38378
+ message: null
38379
+ };
38380
+ }
38381
+ return {
38382
+ type: "points",
38383
+ earned: incorrect ? 0 : 1,
38384
+ total: 1,
38385
+ message: message
38386
+ };
38387
+ }
38199
38388
 
38200
38389
  const {
38201
38390
  assert: assert$2
@@ -38245,7 +38434,7 @@ const getRefForPath$1 = function getRefForPath(path) {
38245
38434
  const column = getColumnFromPath$1(path);
38246
38435
  return "answer" + row + "," + column;
38247
38436
  };
38248
- const getMatrixSize = function getMatrixSize(matrix) {
38437
+ function getMatrixSize(matrix) {
38249
38438
  const matrixSize = [1, 1];
38250
38439
 
38251
38440
  // We need to find the widest row and tallest column to get the correct
@@ -38267,7 +38456,7 @@ const getMatrixSize = function getMatrixSize(matrix) {
38267
38456
  }
38268
38457
  });
38269
38458
  return matrixSize;
38270
- };
38459
+ }
38271
38460
  class Matrix extends React.Component {
38272
38461
  constructor(...args) {
38273
38462
  super(...args);
@@ -38408,57 +38597,7 @@ class Matrix extends React.Component {
38408
38597
  };
38409
38598
  }
38410
38599
  static validate(state, rubric, strings) {
38411
- const solution = rubric.answers;
38412
- const supplied = state.answers;
38413
- const solutionSize = getMatrixSize(solution);
38414
- const suppliedSize = getMatrixSize(supplied);
38415
- const incorrectSize = solutionSize[0] !== suppliedSize[0] || solutionSize[1] !== suppliedSize[1];
38416
- const createValidator = KhanAnswerTypes.number.createValidatorFunctional;
38417
- let message = null;
38418
- let hasEmptyCell = false;
38419
- let incorrect = false;
38420
- _(suppliedSize[0]).times(row => {
38421
- _(suppliedSize[1]).times(col => {
38422
- if (supplied[row][col] == null || supplied[row][col].toString().length === 0) {
38423
- hasEmptyCell = true;
38424
- }
38425
- if (!incorrectSize) {
38426
- const validator = createValidator(
38427
- // @ts-expect-error - TS2345 - Argument of type 'number' is not assignable to parameter of type 'string'.
38428
- solution[row][col], {
38429
- simplify: true
38430
- }, strings);
38431
- const result = validator(supplied[row][col]);
38432
- if (result.message) {
38433
- // @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'null'.
38434
- message = result.message;
38435
- }
38436
- if (!result.correct) {
38437
- incorrect = true;
38438
- }
38439
- }
38440
- });
38441
- });
38442
- if (hasEmptyCell) {
38443
- return {
38444
- type: "invalid",
38445
- message: strings.fillAllCells
38446
- };
38447
- }
38448
- if (incorrectSize) {
38449
- return {
38450
- type: "points",
38451
- earned: 0,
38452
- total: 1,
38453
- message: null
38454
- };
38455
- }
38456
- return {
38457
- type: "points",
38458
- earned: incorrect ? 0 : 1,
38459
- total: 1,
38460
- message: message
38461
- };
38600
+ return matrixValidator(state, rubric, strings);
38462
38601
  }
38463
38602
  componentDidMount() {
38464
38603
  // Used in the `onBlur` and `onFocus` handlers
@@ -38470,7 +38609,7 @@ class Matrix extends React.Component {
38470
38609
  };
38471
38610
  }
38472
38611
  simpleValidate(rubric) {
38473
- return Matrix.validate(this.getUserInput(), rubric, this.context.strings);
38612
+ return matrixValidator(this.getUserInput(), rubric, this.context.strings);
38474
38613
  }
38475
38614
  render() {
38476
38615
  // Set the input sizes through JS so we can control the size of the
@@ -39712,6 +39851,45 @@ var Molecule$1 = {
39712
39851
  widget: MoleculeWidget
39713
39852
  };
39714
39853
 
39854
+ function numberLineValidator(state, rubric) {
39855
+ const range = rubric.range;
39856
+ const divisionRange = state.divisionRange;
39857
+ const start = rubric.initialX != null ? rubric.initialX : range[0];
39858
+ const startRel = rubric.isInequality ? "ge" : "eq";
39859
+ const correctRel = rubric.correctRel || "eq";
39860
+ const correctPos = number.equal(state.numLinePosition, rubric.correctX || 0);
39861
+ const outsideAllowedRange = state.numDivisions > divisionRange[1] || state.numDivisions < divisionRange[0];
39862
+
39863
+ // TODO: I don't think isTickCrtl is a thing anymore
39864
+ if (state.isTickCrtl && outsideAllowedRange) {
39865
+ return {
39866
+ type: "invalid",
39867
+ message: "Number of divisions is outside the allowed range."
39868
+ };
39869
+ }
39870
+ if (correctPos && correctRel === state.rel) {
39871
+ return {
39872
+ type: "points",
39873
+ earned: 1,
39874
+ total: 1,
39875
+ message: null
39876
+ };
39877
+ }
39878
+ if (state.numLinePosition === start && state.rel === startRel) {
39879
+ // We're where we started.
39880
+ return {
39881
+ type: "invalid",
39882
+ message: null
39883
+ };
39884
+ }
39885
+ return {
39886
+ type: "points",
39887
+ earned: 0,
39888
+ total: 1,
39889
+ message: null
39890
+ };
39891
+ }
39892
+
39715
39893
  // @ts-expect-error - TS2339 - Property 'MovablePoint' does not exist on type 'typeof Graphie'.
39716
39894
  const MovablePoint = Graphie.MovablePoint;
39717
39895
  // @ts-expect-error - TS2339 - Property 'Line' does not exist on type 'typeof Graphie'.
@@ -40120,6 +40298,9 @@ class NumberLine extends React.Component {
40120
40298
  });
40121
40299
  };
40122
40300
  }
40301
+ static validate(state, rubric) {
40302
+ return numberLineValidator(state, rubric);
40303
+ }
40123
40304
  getUserInput() {
40124
40305
  return {
40125
40306
  numLinePosition: this.props.numLinePosition,
@@ -40129,8 +40310,7 @@ class NumberLine extends React.Component {
40129
40310
  };
40130
40311
  }
40131
40312
  simpleValidate(rubric) {
40132
- // @ts-expect-error - TS2339 - Property 'validate' does not exist on type 'typeof NumberLine'.
40133
- return NumberLine.validate(this.getUserInput(), rubric);
40313
+ return numberLineValidator(this.getUserInput(), rubric);
40134
40314
  }
40135
40315
  render() {
40136
40316
  const {
@@ -40197,46 +40377,6 @@ NumberLine.defaultProps = {
40197
40377
  rel: "ge",
40198
40378
  apiOptions: ApiOptions.defaults
40199
40379
  };
40200
- _.extend(NumberLine, {
40201
- validate: function (state, rubric) {
40202
- const range = rubric.range;
40203
- const divisionRange = state.divisionRange;
40204
- const start = rubric.initialX != null ? rubric.initialX : range[0];
40205
- const startRel = rubric.isInequality ? "ge" : "eq";
40206
- const correctRel = rubric.correctRel || "eq";
40207
- const correctPos = number.equal(state.numLinePosition, rubric.correctX || 0);
40208
- const outsideAllowedRange = state.numDivisions > divisionRange[1] || state.numDivisions < divisionRange[0];
40209
-
40210
- // TODO: I don't think isTickCrtl is a thing anymore
40211
- if (state.isTickCrtl && outsideAllowedRange) {
40212
- return {
40213
- type: "invalid",
40214
- message: "Number of divisions is outside the allowed range."
40215
- };
40216
- }
40217
- if (correctPos && correctRel === state.rel) {
40218
- return {
40219
- type: "points",
40220
- earned: 1,
40221
- total: 1,
40222
- message: null
40223
- };
40224
- }
40225
- if (state.numLinePosition === start && state.rel === startRel) {
40226
- // We're where we started.
40227
- return {
40228
- type: "invalid",
40229
- message: null
40230
- };
40231
- }
40232
- return {
40233
- type: "points",
40234
- earned: 0,
40235
- total: 1,
40236
- message: null
40237
- };
40238
- }
40239
- });
40240
40380
  const numberLineTransform = editorProps => {
40241
40381
  const props = _.pick(editorProps, ["range", "labelRange", "labelStyle", "labelTicks", "divisionRange", "snapDivisions", "isTickCtrl", "isInequality", "showTooltips"]);
40242
40382
  const numLinePosition = editorProps.initialX != null ? editorProps.initialX : editorProps.range[0];
@@ -43302,10 +43442,25 @@ var PhetSimulation$1 = {
43302
43442
  isLintable: true
43303
43443
  };
43304
43444
 
43305
- /* eslint-disable react/no-unsafe */
43306
43445
  const {
43307
43446
  deepEq
43308
43447
  } = Util;
43448
+ function plotterValidator(userInput, rubric) {
43449
+ if (deepEq(userInput, rubric.starting)) {
43450
+ return {
43451
+ type: "invalid",
43452
+ message: null
43453
+ };
43454
+ }
43455
+ return {
43456
+ type: "points",
43457
+ earned: deepEq(userInput, rubric.correct) ? 1 : 0,
43458
+ total: 1,
43459
+ message: null
43460
+ };
43461
+ }
43462
+
43463
+ /* eslint-disable react/no-unsafe */
43309
43464
  class Plotter extends React.Component {
43310
43465
  constructor(..._args) {
43311
43466
  super(..._args);
@@ -44037,6 +44192,9 @@ class Plotter extends React.Component {
44037
44192
  });
44038
44193
  };
44039
44194
  }
44195
+ static validate(userInput, rubric) {
44196
+ return plotterValidator(userInput, rubric);
44197
+ }
44040
44198
  componentDidMount() {
44041
44199
  this._isMounted = true;
44042
44200
  this.setupGraphie(this.state);
@@ -44064,8 +44222,7 @@ class Plotter extends React.Component {
44064
44222
  return this.state.values;
44065
44223
  }
44066
44224
  simpleValidate(rubric) {
44067
- // @ts-expect-error - TS2339 - Property 'validate' does not exist on type 'typeof Plotter'.
44068
- return Plotter.validate(this.getUserInput(), rubric);
44225
+ return plotterValidator(this.getUserInput(), rubric);
44069
44226
  }
44070
44227
  render() {
44071
44228
  // TODO(kevinb) actually compute the size of the graphie correctly and
@@ -44084,6 +44241,8 @@ class Plotter extends React.Component {
44084
44241
  });
44085
44242
  }
44086
44243
  }
44244
+
44245
+ // We don't need to change any of the original props for static mode
44087
44246
  Plotter.contextType = PerseusI18nContext;
44088
44247
  Plotter.defaultProps = {
44089
44248
  type: "bar",
@@ -44098,24 +44257,6 @@ Plotter.defaultProps = {
44098
44257
  plotDimensions: [380, 300],
44099
44258
  labelInterval: 1
44100
44259
  };
44101
- _.extend(Plotter, {
44102
- validate: function (userInput, rubric) {
44103
- if (deepEq(userInput, rubric.starting)) {
44104
- return {
44105
- type: "invalid",
44106
- message: null
44107
- };
44108
- }
44109
- return {
44110
- type: "points",
44111
- earned: deepEq(userInput, rubric.correct) ? 1 : 0,
44112
- total: 1,
44113
- message: null
44114
- };
44115
- }
44116
- });
44117
-
44118
- // We don't need to change any of the original props for static mode
44119
44260
  const staticTransform = _.identity;
44120
44261
  var Plotter$1 = {
44121
44262
  name: "plotter",
@@ -44285,6 +44426,66 @@ var Sorter$1 = {
44285
44426
  isLintable: true
44286
44427
  };
44287
44428
 
44429
+ function tableValidator(state, rubric, strings) {
44430
+ const filterNonEmpty = function filterNonEmpty(table) {
44431
+ return _.filter(table, function (row) {
44432
+ // Check if row has a cell that is nonempty
44433
+ return _.some(row, _.identity);
44434
+ });
44435
+ };
44436
+ const solution = filterNonEmpty(rubric.answers);
44437
+ const supplied = filterNonEmpty(state);
44438
+ const hasEmptyCell = _.some(supplied, function (row) {
44439
+ return _.some(row, function (cell) {
44440
+ return cell === "";
44441
+ });
44442
+ });
44443
+ if (hasEmptyCell || !supplied.length) {
44444
+ return {
44445
+ type: "invalid",
44446
+ message: null
44447
+ };
44448
+ }
44449
+ if (supplied.length !== solution.length) {
44450
+ return {
44451
+ type: "points",
44452
+ earned: 0,
44453
+ total: 1,
44454
+ message: null
44455
+ };
44456
+ }
44457
+ const createValidator = KhanAnswerTypes.number.createValidatorFunctional;
44458
+ let message = null;
44459
+ const allCorrect = _.every(solution, function (rowSolution) {
44460
+ for (let i = 0; i < supplied.length; i++) {
44461
+ const rowSupplied = supplied[i];
44462
+ const correct = _.every(rowSupplied, function (cellSupplied, i) {
44463
+ const cellSolution = rowSolution[i];
44464
+ const validator = createValidator(cellSolution, {
44465
+ simplify: true
44466
+ }, strings);
44467
+ const result = validator(cellSupplied);
44468
+ if (result.message) {
44469
+ // @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'null'.
44470
+ message = result.message;
44471
+ }
44472
+ return result.correct;
44473
+ });
44474
+ if (correct) {
44475
+ supplied.splice(i, 1);
44476
+ return true;
44477
+ }
44478
+ }
44479
+ return false;
44480
+ });
44481
+ return {
44482
+ type: "points",
44483
+ earned: allCorrect ? 1 : 0,
44484
+ total: 1,
44485
+ message: message
44486
+ };
44487
+ }
44488
+
44288
44489
  const {
44289
44490
  assert
44290
44491
  } = InteractiveUtil;
@@ -44406,12 +44607,14 @@ class Table extends React.Component {
44406
44607
  }, cb);
44407
44608
  };
44408
44609
  }
44610
+ static validate(state, rubric, strings) {
44611
+ return tableValidator(state, rubric, strings);
44612
+ }
44409
44613
  getUserInput() {
44410
44614
  return _.map(this.props.answers, _.clone);
44411
44615
  }
44412
44616
  simpleValidate(rubric) {
44413
- // @ts-expect-error - TS2339 - Property 'validate' does not exist on type 'typeof Table'.
44414
- return Table.validate(this.getUserInput(), rubric, this.context.strings);
44617
+ return tableValidator(this.getUserInput(), rubric, this.context.strings);
44415
44618
  }
44416
44619
  render() {
44417
44620
  const rows = this._getRows();
@@ -44490,68 +44693,6 @@ Table.defaultProps = function () {
44490
44693
  linterContext: linterContextDefault
44491
44694
  };
44492
44695
  }();
44493
- _.extend(Table, {
44494
- validate: function (state, rubric, strings) {
44495
- const filterNonEmpty = function filterNonEmpty(table) {
44496
- return _.filter(table, function (row) {
44497
- // Check if row has a cell that is nonempty
44498
- return _.some(row, _.identity);
44499
- });
44500
- };
44501
- const solution = filterNonEmpty(rubric.answers);
44502
- const supplied = filterNonEmpty(state);
44503
- const hasEmptyCell = _.some(supplied, function (row) {
44504
- return _.some(row, function (cell) {
44505
- return cell === "";
44506
- });
44507
- });
44508
- if (hasEmptyCell || !supplied.length) {
44509
- return {
44510
- type: "invalid",
44511
- message: null
44512
- };
44513
- }
44514
- if (supplied.length !== solution.length) {
44515
- return {
44516
- type: "points",
44517
- earned: 0,
44518
- total: 1,
44519
- message: null
44520
- };
44521
- }
44522
- const createValidator = KhanAnswerTypes.number.createValidatorFunctional;
44523
- let message = null;
44524
- const allCorrect = _.every(solution, function (rowSolution) {
44525
- let i;
44526
- for (i = 0; i < supplied.length; i++) {
44527
- const rowSupplied = supplied[i];
44528
- const correct = _.every(rowSupplied, function (cellSupplied, i) {
44529
- const cellSolution = rowSolution[i];
44530
- const validator = createValidator(cellSolution, {
44531
- simplify: true
44532
- }, strings);
44533
- const result = validator(cellSupplied);
44534
- if (result.message) {
44535
- // @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'null'.
44536
- message = result.message;
44537
- }
44538
- return result.correct;
44539
- });
44540
- if (correct) {
44541
- supplied.splice(i, 1);
44542
- return true;
44543
- }
44544
- }
44545
- return false;
44546
- });
44547
- return {
44548
- type: "points",
44549
- earned: allCorrect ? 1 : 0,
44550
- total: 1,
44551
- message: message
44552
- };
44553
- }
44554
- });
44555
44696
  const propTransform = editorProps => {
44556
44697
  // Remove answers before passing to widget
44557
44698
  const rows = editorProps.answers.length;
@@ -44781,7 +44922,7 @@ const init = function init(options) {
44781
44922
 
44782
44923
  // This file is processed by a Rollup plugin (replace) to inject the production
44783
44924
  const libName = "@khanacademy/perseus";
44784
- const libVersion = "35.0.0";
44925
+ const libVersion = "35.1.1";
44785
44926
  addLibraryVersionToPerseusDebug(libName, libVersion);
44786
44927
 
44787
44928
  const apiVersion = {