@khanacademy/perseus 21.0.0 → 21.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/es/index.js CHANGED
@@ -7,7 +7,7 @@ import * as i18n from '@khanacademy/wonder-blocks-i18n';
7
7
  import Tooltip$1 from '@khanacademy/wonder-blocks-tooltip';
8
8
  import classNames from 'classnames';
9
9
  import * as React from 'react';
10
- import React__default, { useState, useEffect, useRef } from 'react';
10
+ import React__default, { useState, useEffect, useRef, createContext } from 'react';
11
11
  import _, { debounce as debounce$1 } from 'underscore';
12
12
  import Clickable from '@khanacademy/wonder-blocks-clickable';
13
13
  import { Popover, PopoverContentCore, PopoverContent } from '@khanacademy/wonder-blocks-popover';
@@ -3258,6 +3258,15 @@ const textarea = {
3258
3258
  */
3259
3259
  const unescapeMathMode$1 = label => label.startsWith("$") && label.endsWith("$") ? label.slice(1, -1) : label;
3260
3260
  const random$1 = seededRNG$1(new Date().getTime() & 0xffffffff);
3261
+
3262
+ // TODO(benchristel): in the future, we may want to make deepClone work for
3263
+ // Record<string, Cloneable> as well. Currently, it only does arrays.
3264
+ function deepClone(obj) {
3265
+ if (Array.isArray(obj)) {
3266
+ return obj.map(deepClone);
3267
+ }
3268
+ return obj;
3269
+ }
3261
3270
  const Util = {
3262
3271
  inputPathsEqual,
3263
3272
  nestedMap: nestedMap$2,
@@ -3303,7 +3312,8 @@ const Util = {
3303
3312
  getDataUrl: getDataUrl,
3304
3313
  textarea,
3305
3314
  unescapeMathMode: unescapeMathMode$1,
3306
- random: random$1
3315
+ random: random$1,
3316
+ deepClone
3307
3317
  };
3308
3318
 
3309
3319
  // Derived from the MIT-licensed:
@@ -24901,9 +24911,22 @@ class Explanation extends React.Component {
24901
24911
  // Special styling is needed to fit the button in a block of text without throwing off the line spacing.
24902
24912
  // While the button is not normally included in a block of text, it needs to be able to accommodate such a case.
24903
24913
  const buttonStyleOverrides = {
24904
- height: "22px",
24914
+ height: "auto",
24905
24915
  marginLeft: "-2px",
24906
- padding: "0 2px"
24916
+ // transfer space on the left side to the padding
24917
+ marginRight: "2px",
24918
+ // adds space between the right border and any text that follows
24919
+ paddingLeft: "2px" // adds space between the left border and the button text
24920
+ };
24921
+
24922
+ const labelStyle = {
24923
+ fontSize: "18px",
24924
+ lineHeight: "20px",
24925
+ // The following property adjusts the large space between the button text and the caret icon.
24926
+ // Since we are unable to adjust the styling of the icon (where the extra space exists),
24927
+ // we are adjusting it on the text label by using a negative margin.
24928
+ marginRight: "-6px",
24929
+ "white-space": "normal"
24907
24930
  };
24908
24931
  const contentStyling = [styles$p.content, this.state.expanded ? styles$p.contentExpanded : styles$p.contentCollapsed, allowTransition && (this.state.expanded ? styles$p.transitionExpanded : styles$p.transitionCollapsed)];
24909
24932
  return /*#__PURE__*/React.createElement(UniqueIDProvider, {
@@ -24914,8 +24937,9 @@ class Explanation extends React.Component {
24914
24937
  "aria-controls": ids.get("content"),
24915
24938
  endIcon: caretIcon,
24916
24939
  kind: "tertiary",
24940
+ labelStyle: labelStyle,
24917
24941
  onClick: this._onClick,
24918
- size: "large",
24942
+ size: "small",
24919
24943
  style: buttonStyleOverrides
24920
24944
  }, promptText), /*#__PURE__*/React.createElement(View, {
24921
24945
  id: ids.get("content"),
@@ -28190,6 +28214,36 @@ const moveAll = delta => ({
28190
28214
  type: MOVE_ALL,
28191
28215
  delta
28192
28216
  });
28217
+ const MOVE_POINT = "move-point";
28218
+ function movePoint(index, destination) {
28219
+ return {
28220
+ type: MOVE_POINT,
28221
+ index,
28222
+ destination
28223
+ };
28224
+ }
28225
+ const CHANGE_SNAP_STEP = "change-snap-step";
28226
+ function changeSnapStep(snapStep) {
28227
+ return {
28228
+ type: CHANGE_SNAP_STEP,
28229
+ snapStep
28230
+ };
28231
+ }
28232
+ const CHANGE_RANGE = "change-range";
28233
+ function changeRange(range) {
28234
+ return {
28235
+ type: CHANGE_RANGE,
28236
+ range
28237
+ };
28238
+ }
28239
+
28240
+ const GraphStateContext = /*#__PURE__*/createContext({
28241
+ state: {},
28242
+ dispatch: () => {}
28243
+ });
28244
+ function useGraphState() {
28245
+ return React__default.useContext(GraphStateContext);
28246
+ }
28193
28247
 
28194
28248
  /**
28195
28249
  * 44 is touch best practice and AAA compliant for WCAG
@@ -28216,6 +28270,11 @@ const normalizeCoords = (coordsList, ranges) => coordsList.map(coords => coords.
28216
28270
  const extent = ranges[i][1] - ranges[i][0];
28217
28271
  return (coord + ranges[i][1]) / extent;
28218
28272
  }));
28273
+ function snap(snapStep, point) {
28274
+ const [requestedX, requestedY] = point;
28275
+ const [snapX, snapY] = snapStep;
28276
+ return [Math.round(requestedX / snapX) * snapX, Math.round(requestedY / snapY) * snapY];
28277
+ }
28219
28278
 
28220
28279
  const useTransform = (...points) => {
28221
28280
  const {
@@ -28271,6 +28330,9 @@ const getLines = points => points.map((point, i) => {
28271
28330
 
28272
28331
  const defaultStroke = "var(--movable-line-stroke-color)";
28273
28332
  const MovableLine = props => {
28333
+ const {
28334
+ state
28335
+ } = useGraphState();
28274
28336
  const {
28275
28337
  start,
28276
28338
  end,
@@ -28295,7 +28357,7 @@ const MovableLine = props => {
28295
28357
  onMove: newPoint => {
28296
28358
  onMove(vec.sub(newPoint, midpoint));
28297
28359
  },
28298
- constrain: p => p
28360
+ constrain: p => snap(state.snapStep, p)
28299
28361
  });
28300
28362
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("g", {
28301
28363
  ref: line,
@@ -28346,7 +28408,13 @@ function SVGLine(props) {
28346
28408
  });
28347
28409
  }
28348
28410
 
28411
+ // The hitbox size of 48px by 48px is preserved from the legacy interactive
28412
+ // graph.
28413
+ const hitboxSizePx = 48;
28349
28414
  const StyledMovablePoint = props => {
28415
+ const {
28416
+ state
28417
+ } = useGraphState();
28350
28418
  const hitboxRef = useRef(null);
28351
28419
  const {
28352
28420
  point,
@@ -28359,7 +28427,7 @@ const StyledMovablePoint = props => {
28359
28427
  gestureTarget: hitboxRef,
28360
28428
  point,
28361
28429
  onMove,
28362
- constrain: p => p
28430
+ constrain: p => snap(state.snapStep, p)
28363
28431
  });
28364
28432
  const [[x, y]] = useTransform(point);
28365
28433
  return /*#__PURE__*/React.createElement("g", {
@@ -28374,7 +28442,7 @@ const StyledMovablePoint = props => {
28374
28442
  }
28375
28443
  }, /*#__PURE__*/React.createElement("circle", {
28376
28444
  className: "movable-point-hitbox",
28377
- r: TARGET_SIZE / 2,
28445
+ r: hitboxSizePx / 2,
28378
28446
  cx: x,
28379
28447
  cy: y
28380
28448
  }), /*#__PURE__*/React.createElement("circle", {
@@ -28412,7 +28480,9 @@ const SegmentGraph = props => {
28412
28480
  onMoveLine: delta => {
28413
28481
  dispatch(moveLine(i, delta));
28414
28482
  },
28415
- onMovePoint: (endpointIndex, destination) => dispatch(moveControlPoint(endpointIndex, destination, i)),
28483
+ onMovePoint: (endpointIndex, destination) => {
28484
+ dispatch(moveControlPoint(endpointIndex, destination, i));
28485
+ },
28416
28486
  "data-testid": "segment" + i
28417
28487
  })));
28418
28488
  };
@@ -28701,7 +28771,8 @@ const PolygonGraph = props => {
28701
28771
  type,
28702
28772
  showAngles,
28703
28773
  showSides,
28704
- range
28774
+ range,
28775
+ snapStep
28705
28776
  } = props.graphState;
28706
28777
  const points = coords != null ? coords : [[0, 0]];
28707
28778
  const pointsSum = points.reduce((acc, point) => vec.add(acc, point), [0, 0]);
@@ -28716,7 +28787,7 @@ const PolygonGraph = props => {
28716
28787
  const delta = vec.sub(newPoint, midpoint);
28717
28788
  dispatch(moveAll(delta));
28718
28789
  },
28719
- constrain: p => p
28790
+ constrain: p => snap(snapStep, p)
28720
28791
  });
28721
28792
  const active = hovered || focused || dragging;
28722
28793
  const lines = getLines(points);
@@ -28768,22 +28839,179 @@ const PolygonGraph = props => {
28768
28839
  }), points.map((point, i) => /*#__PURE__*/React.createElement(StyledMovablePoint, {
28769
28840
  key: "point-" + i,
28770
28841
  point: point,
28771
- onMove: destination => dispatch(moveControlPoint(i, destination)),
28842
+ onMove: destination => dispatch(movePoint(i, destination)),
28772
28843
  "data-testid": type + i
28773
28844
  })));
28774
28845
  };
28775
28846
 
28776
- const renderLineLabel = (n, [min, max]) => n !== -1 && n !== min && n !== max;
28777
- const axisOptions = (props, index) => ({
28778
- axis: props.markings === "graph",
28779
- lines: props.step[index],
28780
- subdivisions: props.step[index] / props.gridStep[index],
28781
- labels: n => renderLineLabel(n, props.range[index]) ? n : ""
28782
- });
28783
- const Grid = props => props.markings === "none" ? null : /*#__PURE__*/React.createElement(Coordinates.Cartesian, {
28784
- xAxis: axisOptions(props, 0),
28785
- yAxis: axisOptions(props, 1)
28786
- });
28847
+ function PointGraph(props) {
28848
+ const {
28849
+ dispatch
28850
+ } = props;
28851
+ return /*#__PURE__*/React.createElement(React.Fragment, null, props.graphState.coords.map((point, i) => /*#__PURE__*/React.createElement(StyledMovablePoint, {
28852
+ key: i,
28853
+ point: point,
28854
+ onMove: destination => dispatch(movePoint(i, destination))
28855
+ })));
28856
+ }
28857
+
28858
+ function Arrow(props) {
28859
+ const {
28860
+ userTransform,
28861
+ viewTransform
28862
+ } = useTransformContext();
28863
+ const point = [props.x, props.y];
28864
+ const userTransformedPoint = vec.transform(point, userTransform);
28865
+ const viewTransformedPoint = vec.transform(userTransformedPoint, viewTransform);
28866
+ return /*#__PURE__*/React__default.createElement("g", {
28867
+ transform: `translate(${viewTransformedPoint[0]} ${viewTransformedPoint[1]}) rotate(${props.rotate}) scale(1.25)`
28868
+ }, /*#__PURE__*/React__default.createElement("g", {
28869
+ transform: "translate(-1)"
28870
+ }, /*#__PURE__*/React__default.createElement("path", {
28871
+ d: "M-3 4 C-2.75 2.5 0 0.25 0.75 0C0 -0.25 -2.75 -2.5 -3 -4",
28872
+ fill: "none"
28873
+ })));
28874
+ }
28875
+ function AxisArrows() {
28876
+ const {
28877
+ state
28878
+ } = useGraphState();
28879
+ const range = state.range;
28880
+ const [xMin, xMax] = range[0];
28881
+ const [yMin, yMax] = range[1];
28882
+ return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(Arrow, {
28883
+ x: xMax,
28884
+ y: 0,
28885
+ rotate: 0
28886
+ }), /*#__PURE__*/React__default.createElement(Arrow, {
28887
+ x: 0,
28888
+ y: yMin,
28889
+ rotate: 90
28890
+ }), /*#__PURE__*/React__default.createElement(Arrow, {
28891
+ x: xMin,
28892
+ y: 0,
28893
+ rotate: 180
28894
+ }), /*#__PURE__*/React__default.createElement(Arrow, {
28895
+ x: 0,
28896
+ y: yMax,
28897
+ rotate: 270
28898
+ }));
28899
+ }
28900
+
28901
+ const tickSize = 10;
28902
+ const tickStyle = {
28903
+ stroke: "black",
28904
+ strokeWidth: 1
28905
+ };
28906
+ const YGridTick = ({
28907
+ y
28908
+ }) => {
28909
+ const pointOnAxis = [0, y];
28910
+ const [[xPosition, yPosition]] = useTransform(pointOnAxis);
28911
+ return /*#__PURE__*/React.createElement("g", {
28912
+ className: "y-axis-ticks"
28913
+ }, /*#__PURE__*/React.createElement("line", {
28914
+ x1: xPosition - tickSize / 2,
28915
+ y1: yPosition,
28916
+ x2: xPosition + tickSize / 2,
28917
+ y2: yPosition,
28918
+ style: tickStyle
28919
+ }));
28920
+ };
28921
+ const XGridTick = ({
28922
+ x
28923
+ }) => {
28924
+ const pointOnAxis = [x, 0];
28925
+ const [[xPosition, yPosition]] = useTransform(pointOnAxis);
28926
+ return /*#__PURE__*/React.createElement("g", {
28927
+ className: "x-axis-ticks"
28928
+ }, /*#__PURE__*/React.createElement("line", {
28929
+ x1: xPosition,
28930
+ y1: yPosition + tickSize / 2,
28931
+ x2: xPosition,
28932
+ y2: yPosition - tickSize / 2,
28933
+ style: tickStyle
28934
+ }));
28935
+ };
28936
+ function generateTickLocations(tickStep, min, max) {
28937
+ const ticks = [];
28938
+ for (let i = 0 + tickStep; i < max; i += tickStep) {
28939
+ ticks.push(i);
28940
+ }
28941
+ for (let i = 0 - tickStep; i > min; i -= tickStep) {
28942
+ ticks.push(i);
28943
+ }
28944
+ return ticks;
28945
+ }
28946
+ const AxisTicks = props => {
28947
+ const range = props.range;
28948
+ const [xMin, xMax] = range[0];
28949
+ const [yMin, yMax] = range[1];
28950
+ const yTickStep = props.tickStep[1];
28951
+ const xTickStep = props.tickStep[0];
28952
+ const yGridTicks = generateTickLocations(yTickStep, yMin, yMax);
28953
+ const xGridTicks = generateTickLocations(xTickStep, xMin, xMax);
28954
+ return /*#__PURE__*/React.createElement("g", {
28955
+ className: "axis-ticks"
28956
+ }, yGridTicks.map(y => {
28957
+ return /*#__PURE__*/React.createElement(YGridTick, {
28958
+ y: y,
28959
+ key: `y-grid-tick-${y}`
28960
+ });
28961
+ }), xGridTicks.map(x => {
28962
+ return /*#__PURE__*/React.createElement(XGridTick, {
28963
+ x: x,
28964
+ key: `x-grid-tick-${x}`
28965
+ });
28966
+ }));
28967
+ };
28968
+
28969
+ /**
28970
+ * lineLabelText get the text that should
28971
+ * be displayed for a specific tick place
28972
+ *
28973
+ * @param {number} n the tick number
28974
+ * @param {number} tickStep how frequently a label should appear
28975
+ * @param {vec.Vector2} range the min/max range on the axis
28976
+ */
28977
+ const lineLabelText = (n, tickStep, range) => {
28978
+ const [min, max] = range;
28979
+ const isOnStep = n % tickStep === 0;
28980
+ const isNegativeOne = n === -1;
28981
+ const isMin = n === min;
28982
+ const isMax = n === max;
28983
+ const shouldRender = isOnStep && !isNegativeOne && !isMin && !isMax;
28984
+ return shouldRender ? `${n}` : "";
28985
+ };
28986
+
28987
+ /**
28988
+ * axisOptions determine axis options for Mafs
28989
+ *
28990
+ * axisIndex is for grabbing data in an array that contains
28991
+ * data for multiple axes. For example range: [[-10, 10], [-10, 10]]
28992
+ * range[0] is data for the x axis and range[1] is data for the y axis
28993
+ *
28994
+ * @param {GridProps} props
28995
+ * @param {number} axisIndex which axis we're getting options for
28996
+ */
28997
+ const axisOptions = (props, axisIndex) => {
28998
+ const axisStep = props.tickStep[axisIndex];
28999
+ const axisRange = props.range[axisIndex];
29000
+ return {
29001
+ axis: props.markings === "graph",
29002
+ lines: props.gridStep[axisIndex],
29003
+ labels: n => lineLabelText(n, axisStep, axisRange)
29004
+ };
29005
+ };
29006
+ const Grid = props => {
29007
+ return props.markings === "none" ? null : /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Coordinates.Cartesian, {
29008
+ xAxis: axisOptions(props, 0),
29009
+ yAxis: axisOptions(props, 1)
29010
+ }), /*#__PURE__*/React.createElement(AxisTicks, {
29011
+ range: props.range,
29012
+ tickStep: props.tickStep
29013
+ }), props.markings === "graph" && /*#__PURE__*/React.createElement(AxisArrows, null));
29014
+ };
28787
29015
 
28788
29016
  // Displays a stylized open eye: lint warnings are visible
28789
29017
  const VisibleIcon = () => /*#__PURE__*/React.createElement("svg", {
@@ -29480,53 +29708,50 @@ const LegacyGrid = ({
29480
29708
  return null;
29481
29709
  };
29482
29710
 
29483
- /** Determine if coords is type CollinearTuple[] */
29484
- const isCollinearTuples = coords => Array.isArray(coords[0][0]);
29485
-
29486
- // Generic type makes returned state match input state
29487
29711
  function interactiveGraphReducer(state, action) {
29712
+ switch (action.type) {
29713
+ case MOVE_CONTROL_POINT:
29714
+ return doMoveControlPoint(state, action);
29715
+ case MOVE_LINE:
29716
+ return doMoveLine(state, action);
29717
+ case MOVE_ALL:
29718
+ return doMoveAll(state, action);
29719
+ case MOVE_POINT:
29720
+ return doMovePoint(state, action);
29721
+ case CHANGE_SNAP_STEP:
29722
+ return doChangeSnapStep(state, action);
29723
+ case CHANGE_RANGE:
29724
+ return doChangeRange(state, action);
29725
+ default:
29726
+ throw new UnreachableCaseError(action);
29727
+ }
29728
+ }
29729
+ function doMoveControlPoint(state, action) {
29488
29730
  const {
29489
29731
  snapStep,
29490
29732
  range
29491
29733
  } = state;
29492
- if (!state.coords) {
29493
- throw new Error("Graph state must have been initialized with coords");
29494
- }
29495
- switch (action.type) {
29496
- case MOVE_CONTROL_POINT:
29734
+ switch (state.type) {
29735
+ case "segment":
29736
+ case "linear":
29737
+ case "linear-system":
29738
+ case "ray":
29497
29739
  {
29498
- const newCoords = action.itemIndex !== undefined && state.coords && isCollinearTuples(state.coords) ? updateAtIndex({
29740
+ const newCoords = updateAtIndex({
29499
29741
  array: state.coords,
29500
29742
  index: action.itemIndex,
29501
29743
  update: tuple => setAtIndex({
29502
29744
  array: tuple,
29503
29745
  index: action.pointIndex,
29504
- newValue: snap({
29505
- snapStep,
29506
- point: bound$1({
29507
- snapStep,
29508
- range,
29509
- point: action.destination
29510
- })
29511
- })
29512
- })
29513
- }) : setAtIndex({
29514
- array: state.coords,
29515
- index: action.pointIndex,
29516
- newValue: snap({
29517
- snapStep,
29518
- point: bound$1({
29746
+ newValue: snap(snapStep, bound$1({
29519
29747
  snapStep,
29520
29748
  range,
29521
29749
  point: action.destination
29522
- })
29750
+ }))
29523
29751
  })
29524
29752
  });
29525
-
29526
- // Cannot type narrow both conditions within function parameters,
29527
- // so this may seem redundant, but it's necessary for type safety.
29528
- const coordsToCheck = isCollinearTuples(newCoords) && action.itemIndex !== undefined ? newCoords[action.itemIndex] : newCoords;
29529
- if (!isCollinearTuples(coordsToCheck) && coordsOverlap(coordsToCheck)) {
29753
+ const coordsToCheck = newCoords[action.itemIndex];
29754
+ if (coordsOverlap(coordsToCheck)) {
29530
29755
  return state;
29531
29756
  }
29532
29757
  return _extends({}, state, {
@@ -29534,16 +29759,30 @@ function interactiveGraphReducer(state, action) {
29534
29759
  coords: newCoords
29535
29760
  });
29536
29761
  }
29537
- case MOVE_LINE:
29762
+ case "circle":
29763
+ throw new Error("FIXME implement circle reducer");
29764
+ case "point":
29765
+ case "polygon":
29766
+ throw new Error(`Don't use moveControlPoint for ${state.type} graphs. Use movePoint instead!`);
29767
+ default:
29768
+ throw new UnreachableCaseError(state);
29769
+ }
29770
+ }
29771
+ function doMoveLine(state, action) {
29772
+ const {
29773
+ snapStep,
29774
+ range
29775
+ } = state;
29776
+ switch (state.type) {
29777
+ case "segment":
29778
+ case "linear":
29779
+ case "linear-system":
29780
+ case "ray":
29538
29781
  {
29539
- var _state$coords;
29540
- if (!isCollinearTuples(state.coords)) {
29541
- throw new Error("Cannot call this action unless coords is array of CollinearTuple.");
29542
- }
29543
29782
  if (action.itemIndex === undefined) {
29544
29783
  throw new Error("Please provide index of line to move");
29545
29784
  }
29546
- const currentLine = (_state$coords = state.coords) == null ? void 0 : _state$coords[action.itemIndex];
29785
+ const currentLine = state.coords[action.itemIndex];
29547
29786
  if (!currentLine) {
29548
29787
  throw new Error("No line to move");
29549
29788
  }
@@ -29551,45 +29790,89 @@ function interactiveGraphReducer(state, action) {
29551
29790
  snapStep,
29552
29791
  range
29553
29792
  });
29554
- const newLine = [snap({
29555
- snapStep,
29556
- point: vec.add(currentLine[0], change)
29557
- }), snap({
29558
- snapStep,
29559
- point: vec.add(currentLine[1], change)
29560
- })];
29793
+ const newLine = [snap(snapStep, vec.add(currentLine[0], change)), snap(snapStep, vec.add(currentLine[1], change))];
29561
29794
  const newCoords = setAtIndex({
29562
29795
  array: state.coords,
29563
29796
  index: action.itemIndex,
29564
29797
  newValue: newLine
29565
29798
  });
29566
29799
  return _extends({}, state, {
29800
+ type: state.type,
29567
29801
  hasBeenInteractedWith: true,
29568
29802
  coords: newCoords
29569
29803
  });
29570
29804
  }
29571
- case MOVE_ALL:
29805
+ default:
29806
+ // The MoveLine action doesn't make sense for other graph types;
29807
+ // ignore it if it somehow happens
29808
+ return state;
29809
+ }
29810
+ }
29811
+ function doMoveAll(state, action) {
29812
+ const {
29813
+ snapStep,
29814
+ range
29815
+ } = state;
29816
+ switch (state.type) {
29817
+ case "polygon":
29572
29818
  {
29573
- if (isCollinearTuples(state.coords)) {
29574
- throw new Error("Cannot call this action unless coords is array of vectors.");
29575
- }
29576
29819
  const change = getChange(state.coords, action.delta, {
29577
29820
  snapStep,
29578
29821
  range
29579
29822
  });
29580
- const newCoords = state.coords.map(point => snap({
29581
- snapStep,
29582
- point: vec.add(point, change)
29583
- }));
29823
+ const newCoords = state.coords.map(point => snap(snapStep, vec.add(point, change)));
29584
29824
  return _extends({}, state, {
29585
29825
  hasBeenInteractedWith: true,
29586
29826
  coords: newCoords
29587
29827
  });
29588
29828
  }
29589
29829
  default:
29590
- throw new UnreachableCaseError(action);
29830
+ // MoveAll is not supported for other state types; just ignore it.
29831
+ return state;
29832
+ }
29833
+ }
29834
+ function doMovePoint(state, action) {
29835
+ switch (state.type) {
29836
+ case "point":
29837
+ case "polygon":
29838
+ {
29839
+ return _extends({}, state, {
29840
+ hasBeenInteractedWith: true,
29841
+ coords: setAtIndex({
29842
+ array: state.coords,
29843
+ index: action.index,
29844
+ newValue: snap(state.snapStep, bound$1({
29845
+ snapStep: state.snapStep,
29846
+ range: state.range,
29847
+ point: action.destination
29848
+ }))
29849
+ })
29850
+ });
29851
+ }
29852
+ default:
29853
+ throw new Error("The movePoint action is only for point and polygon graphs");
29591
29854
  }
29592
29855
  }
29856
+ function doChangeSnapStep(state, action) {
29857
+ if (
29858
+ // Deep equality check since these are arrays
29859
+ _.isEqual(state.snapStep, action.snapStep)) {
29860
+ return state;
29861
+ }
29862
+ return _extends({}, state, {
29863
+ snapStep: action.snapStep
29864
+ });
29865
+ }
29866
+ function doChangeRange(state, action) {
29867
+ if (
29868
+ // Deep equality check since these are arrays
29869
+ _.isEqual(state.range, action.range)) {
29870
+ return state;
29871
+ }
29872
+ return _extends({}, state, {
29873
+ range: action.range
29874
+ });
29875
+ }
29593
29876
  const getChange = (coords, delta, constraintOpts) => {
29594
29877
  const [deltaX, deltaY] = delta;
29595
29878
  const maxMoves = coords.map(point => maxMove(_extends({}, constraintOpts, {
@@ -29606,15 +29889,6 @@ const getChange = (coords, delta, constraintOpts) => {
29606
29889
  const dy = clamp(deltaY, minYMove, maxYMove);
29607
29890
  return [dx, dy];
29608
29891
  };
29609
- function snap({
29610
- snapStep,
29611
- point
29612
- }) {
29613
- const [requestedX, requestedY] = point;
29614
- const [snapX, snapY] = snapStep;
29615
- return [Math.round(requestedX / snapX) * snapX, Math.round(requestedY / snapY) * snapY];
29616
- }
29617
-
29618
29892
  // Returns the closest point to the given `point` that is within the graph
29619
29893
  // bounds given in `state`.
29620
29894
  function bound$1({
@@ -29668,7 +29942,7 @@ function clamp(value, min, max) {
29668
29942
  const coordsOverlap = coords => coords.some((coord, i) => coords.some((c, j) => i !== j && vector$1.equal(coord, c)));
29669
29943
  function updateAtIndex(args) {
29670
29944
  const {
29671
- array = [],
29945
+ array,
29672
29946
  index,
29673
29947
  update
29674
29948
  } = args;
@@ -29681,13 +29955,12 @@ function updateAtIndex(args) {
29681
29955
  }
29682
29956
  function setAtIndex(args) {
29683
29957
  const {
29684
- array = [],
29958
+ array,
29685
29959
  index,
29686
29960
  newValue
29687
29961
  } = args;
29688
29962
  const copy = [...array];
29689
29963
  copy[index] = newValue;
29690
- // restoring readonly to array
29691
29964
  return copy;
29692
29965
  }
29693
29966
 
@@ -29728,19 +30001,32 @@ function initializeGraphState(params) {
29728
30001
  })
29729
30002
  };
29730
30003
  case "polygon":
29731
- return _extends({
30004
+ return {
30005
+ type: "polygon",
29732
30006
  hasBeenInteractedWith: false,
29733
30007
  range,
29734
- snapStep
29735
- }, graph, {
30008
+ snapStep,
30009
+ showAngles: Boolean(graph.showAngles),
30010
+ showSides: Boolean(graph.showSides),
29736
30011
  coords: getPolygonCoords({
29737
30012
  graph,
29738
30013
  range,
29739
30014
  step
29740
30015
  })
29741
- });
29742
- case "angle":
30016
+ };
29743
30017
  case "point":
30018
+ return {
30019
+ type: graph.type,
30020
+ hasBeenInteractedWith: false,
30021
+ range,
30022
+ snapStep,
30023
+ coords: getDefaultPoints({
30024
+ graph,
30025
+ step,
30026
+ range
30027
+ })
30028
+ };
30029
+ case "angle":
29744
30030
  case "circle":
29745
30031
  case "sinusoid":
29746
30032
  case "quadratic":
@@ -29749,6 +30035,46 @@ function initializeGraphState(params) {
29749
30035
  throw new UnreachableCaseError(graph);
29750
30036
  }
29751
30037
  }
30038
+ const getDefaultPoints = ({
30039
+ graph,
30040
+ range,
30041
+ step
30042
+ }) => {
30043
+ var _graph$coords;
30044
+ const numPoints = graph.numPoints || 1;
30045
+ let coords = (_graph$coords = graph.coords) == null ? void 0 : _graph$coords.slice();
30046
+ if (coords) {
30047
+ return coords;
30048
+ }
30049
+ switch (numPoints) {
30050
+ case 1:
30051
+ // Back in the day, one point's coords were in graph.coord
30052
+ coords = [graph.coord || [0, 0]];
30053
+ break;
30054
+ case 2:
30055
+ coords = [[-5, 0], [5, 0]];
30056
+ break;
30057
+ case 3:
30058
+ coords = [[-5, 0], [0, 0], [5, 0]];
30059
+ break;
30060
+ case 4:
30061
+ coords = [[-6, 0], [-2, 0], [2, 0], [6, 0]];
30062
+ break;
30063
+ case 5:
30064
+ coords = [[-6, 0], [-3, 0], [0, 0], [3, 0], [6, 0]];
30065
+ break;
30066
+ case 6:
30067
+ coords = [[-5, 0], [-3, 0], [-1, 0], [1, 0], [3, 0], [5, 0]];
30068
+ break;
30069
+ default:
30070
+ coords = [];
30071
+ break;
30072
+ }
30073
+ // Transform coords from their -10 to 10 space to 0 to 1
30074
+ // because of the old graph.coord, and also it's easier.
30075
+ const newCoords = normalizeCoords(coords, [[-10, 10], [-10, 10]]);
30076
+ return normalizePoints(range, step, newCoords);
30077
+ };
29752
30078
 
29753
30079
  // TS v4 doesn't narrow return types, while v5 does.
29754
30080
  // Instead of updating to v5, using generic type to relate input and output types.
@@ -29769,15 +30095,13 @@ function getGradableGraph(state, initialGraph) {
29769
30095
  });
29770
30096
  }
29771
30097
  if (state.type === "linear" && initialGraph.type === "linear") {
29772
- var _state$coords;
29773
30098
  return _extends({}, initialGraph, {
29774
- coords: (_state$coords = state.coords) == null ? void 0 : _state$coords[0]
30099
+ coords: state.coords[0]
29775
30100
  });
29776
30101
  }
29777
30102
  if (state.type === "ray" && initialGraph.type === "ray") {
29778
- var _state$coords2;
29779
30103
  return _extends({}, initialGraph, {
29780
- coords: (_state$coords2 = state.coords) == null ? void 0 : _state$coords2[0]
30104
+ coords: state.coords[0]
29781
30105
  });
29782
30106
  }
29783
30107
  if (state.type === "polygon" && initialGraph.type === "polygon") {
@@ -29785,6 +30109,11 @@ function getGradableGraph(state, initialGraph) {
29785
30109
  coords: state.coords
29786
30110
  });
29787
30111
  }
30112
+ if (state.type === "point" && initialGraph.type === "point") {
30113
+ return _extends({}, initialGraph, {
30114
+ coords: state.coords
30115
+ });
30116
+ }
29788
30117
  throw new Error("Mafs is not yet implemented for graph type: " + initialGraph.type);
29789
30118
  }
29790
30119
  const getDefaultSegments = ({
@@ -29822,10 +30151,10 @@ const getLineCoords = ({
29822
30151
  range,
29823
30152
  step
29824
30153
  }) => {
29825
- var _graph$coords;
30154
+ var _graph$coords2;
29826
30155
  return (
29827
30156
  // Return two lines for a linear system, one for a ray or linear
29828
- ((_graph$coords = graph.coords) != null ? _graph$coords : graph.type === "linear-system") ? defaultLinearCoords.map(collinear => normalizePoints(range, step, collinear)) : [normalizePoints(range, step, defaultLinearCoords[0])]
30157
+ ((_graph$coords2 = graph.coords) != null ? _graph$coords2 : graph.type === "linear-system") ? defaultLinearCoords.map(collinear => normalizePoints(range, step, collinear)) : [normalizePoints(range, step, defaultLinearCoords[0])]
29829
30158
  );
29830
30159
  };
29831
30160
  const getPolygonCoords = ({
@@ -29833,7 +30162,8 @@ const getPolygonCoords = ({
29833
30162
  range,
29834
30163
  step
29835
30164
  }) => {
29836
- let coords = graph.coords;
30165
+ var _graph$coords3;
30166
+ let coords = (_graph$coords3 = graph.coords) == null ? void 0 : _graph$coords3.slice();
29837
30167
  if (coords) {
29838
30168
  return coords;
29839
30169
  }
@@ -29887,6 +30217,13 @@ const renderGraph = props => {
29887
30217
  graphState: state,
29888
30218
  dispatch: dispatch
29889
30219
  });
30220
+ case "point":
30221
+ return /*#__PURE__*/React.createElement(PointGraph, {
30222
+ graphState: state,
30223
+ dispatch: dispatch
30224
+ });
30225
+ case "circle":
30226
+ throw new Error("the circle graph type is not yet implemented");
29890
30227
  default:
29891
30228
  return new UnreachableCaseError(type);
29892
30229
  }
@@ -29894,10 +30231,36 @@ const renderGraph = props => {
29894
30231
  const MafsGraph = /*#__PURE__*/React.forwardRef((props, ref) => {
29895
30232
  const [width, height] = props.box;
29896
30233
  const [state, dispatch] = React.useReducer(interactiveGraphReducer, props, initializeGraphState);
30234
+ const prevState = useRef(state);
30235
+ useEffect(() => {
30236
+ if (prevState.current !== state) {
30237
+ props.onChange({
30238
+ graph: state
30239
+ });
30240
+ }
30241
+ prevState.current = state;
30242
+ }, [props, state]);
30243
+
30244
+ // Destructuring first to keep useEffect from making excess calls
30245
+ const [xSnap, ySnap] = props.snapStep;
30246
+ useEffect(() => {
30247
+ dispatch(changeSnapStep([xSnap, ySnap]));
30248
+ }, [xSnap, ySnap]);
30249
+
30250
+ // Destructuring first to keep useEffect from making excess calls
30251
+ const [[xMinRange, xMaxRange], [yMinRange, yMaxRange]] = props.range;
30252
+ useEffect(() => {
30253
+ dispatch(changeRange([[xMinRange, xMaxRange], [yMinRange, yMaxRange]]));
30254
+ }, [xMinRange, xMaxRange, yMinRange, yMaxRange]);
29897
30255
  React.useImperativeHandle(ref, () => ({
29898
30256
  getUserInput: () => getGradableGraph(state, props.graph)
29899
30257
  }));
29900
- return /*#__PURE__*/React.createElement(View, {
30258
+ return /*#__PURE__*/React.createElement(GraphStateContext.Provider, {
30259
+ value: {
30260
+ state,
30261
+ dispatch
30262
+ }
30263
+ }, /*#__PURE__*/React.createElement(View, {
29901
30264
  style: {
29902
30265
  width,
29903
30266
  height,
@@ -29923,12 +30286,18 @@ const MafsGraph = /*#__PURE__*/React.forwardRef((props, ref) => {
29923
30286
  zoom: false,
29924
30287
  width: width,
29925
30288
  height: height
29926
- }, /*#__PURE__*/React.createElement(SvgDefs, null), /*#__PURE__*/React.createElement(Grid, props), props.lockedFigures && /*#__PURE__*/React.createElement(GraphLockedLayer, {
30289
+ }, /*#__PURE__*/React.createElement(SvgDefs, null), /*#__PURE__*/React.createElement(Grid, {
30290
+ tickStep: props.step,
30291
+ gridStep: props.gridStep,
30292
+ range: props.range,
30293
+ containerSizeClass: props.containerSizeClass,
30294
+ markings: props.markings
30295
+ }), props.lockedFigures && /*#__PURE__*/React.createElement(GraphLockedLayer, {
29927
30296
  lockedFigures: props.lockedFigures
29928
30297
  }), renderGraph({
29929
30298
  state,
29930
30299
  dispatch
29931
- }))));
30300
+ })))));
29932
30301
  });
29933
30302
 
29934
30303
  const {
@@ -31065,6 +31434,7 @@ class LegacyInteractiveGraph extends React.Component {
31065
31434
  $(this.angle).on("move", () => {
31066
31435
  var _this$angle;
31067
31436
  this.onChange({
31437
+ // @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'.
31068
31438
  graph: _extends({}, graph, {
31069
31439
  coords: (_this$angle = this.angle) == null ? void 0 : _this$angle.getClockwiseCoords()
31070
31440
  })
@@ -31656,9 +32026,8 @@ class InteractiveGraph extends React.Component {
31656
32026
  };
31657
32027
  }
31658
32028
  } else if (userInput.type === "point" && rubric.correct.type === "point" && userInput.coords != null) {
31659
- var _userInput$coords;
31660
32029
  let correct = InteractiveGraph.getPointCoords(rubric.correct, component);
31661
- const guess = (_userInput$coords = userInput.coords) == null ? void 0 : _userInput$coords.slice();
32030
+ const guess = userInput.coords.slice();
31662
32031
  correct = correct.slice();
31663
32032
  // Everything's already rounded so we shouldn't need to do an
31664
32033
  // eq() comparison but _.isEqual(0, -0) is false, so we'll use
@@ -31676,8 +32045,8 @@ class InteractiveGraph extends React.Component {
31676
32045
  };
31677
32046
  }
31678
32047
  } else if (userInput.type === "polygon" && rubric.correct.type === "polygon" && userInput.coords != null) {
31679
- var _userInput$coords2, _rubric$correct$coord;
31680
- const guess = (_userInput$coords2 = userInput.coords) == null ? void 0 : _userInput$coords2.slice();
32048
+ var _userInput$coords, _rubric$correct$coord;
32049
+ const guess = (_userInput$coords = userInput.coords) == null ? void 0 : _userInput$coords.slice();
31681
32050
  // @ts-expect-error - TS2322 - Type 'Coord[] | undefined' is not assignable to type 'Coord[]'.
31682
32051
  const correct = (_rubric$correct$coord = rubric.correct.coords) == null ? void 0 : _rubric$correct$coord.slice();
31683
32052
  let match;
@@ -31702,9 +32071,9 @@ class InteractiveGraph extends React.Component {
31702
32071
  };
31703
32072
  }
31704
32073
  } else if (userInput.type === "segment" && rubric.correct.type === "segment" && userInput.coords != null) {
31705
- var _rubric$correct$coord2;
31706
- let guess = userInput.coords.slice();
31707
- let correct = (_rubric$correct$coord2 = rubric.correct.coords) == null ? void 0 : _rubric$correct$coord2.slice();
32074
+ var _rubric$correct;
32075
+ let guess = Util.deepClone(userInput.coords);
32076
+ let correct = Util.deepClone((_rubric$correct = rubric.correct) == null ? void 0 : _rubric$correct.coords);
31708
32077
  guess = _.invoke(guess, "sort").sort();
31709
32078
  // @ts-expect-error - TS2345 - Argument of type '(readonly Coord[])[] | undefined' is not assignable to parameter of type 'Collection<any>'.
31710
32079
  correct = _.invoke(correct, "sort").sort();
@@ -41972,7 +42341,7 @@ const init = function init(options) {
41972
42341
 
41973
42342
  // This file is processed by a Rollup plugin (replace) to inject the production
41974
42343
  const libName = "@khanacademy/perseus";
41975
- const libVersion = "21.0.0";
42344
+ const libVersion = "21.2.0";
41976
42345
  addLibraryVersionToPerseusDebug(libName, libVersion);
41977
42346
 
41978
42347
  const apiVersion = {
@@ -43096,6 +43465,9 @@ const ItemExtras = [
43096
43465
  // A background image applied to various widgets.
43097
43466
 
43098
43467
  const PerseusExpressionAnswerFormConsidered = ["correct", "wrong", "ungraded"];
43468
+
43469
+ // 2D range: xMin, xMax, yMin, yMax
43470
+
43099
43471
  const plotterPlotTypes = ["bar", "line", "pic", "histogram", "dotplot"];
43100
43472
 
43101
43473
  /**
@@ -44482,7 +44854,7 @@ function getAnswersFromWidgets(widgets) {
44482
44854
  // E.g. 'item 1\nitem 2\nitem 3'
44483
44855
  const orderer = widget;
44484
44856
  if ((_orderer$options = orderer.options) != null && _orderer$options.correctOptions) {
44485
- answers.push(orderer.options.correctOptions.map(option => option.content).join("\n"));
44857
+ answers.push(joinOptionContents(orderer.options.correctOptions));
44486
44858
  }
44487
44859
  break;
44488
44860
  case "sorter":
@@ -44541,6 +44913,28 @@ function getAnswersFromWidgets(widgets) {
44541
44913
  return answers;
44542
44914
  }
44543
44915
 
44916
+ /**
44917
+ * Join the content of the options together.
44918
+ * @param {Array<{content: string}>} options
44919
+ * @returns {string}
44920
+ */
44921
+ const joinOptionContents = options => options.map(({
44922
+ content
44923
+ }) => content).join("\n");
44924
+
44925
+ /**
44926
+ * Convert an index to an option letter.
44927
+ * @param {number} index
44928
+ * @returns {string}
44929
+ * @example
44930
+ * toOptionLetter(0) // 'A'
44931
+ * toOptionLetter(1) // 'B'
44932
+ * toOptionLetter(25) // 'Z'
44933
+ * // Once the index goes past 25, it will start returning special characters
44934
+ * toOptionLetter(26) // '['
44935
+ */
44936
+ const toOptionLetter = index => String.fromCharCode("A".charCodeAt(0) + index);
44937
+
44544
44938
  /**
44545
44939
  * Inject a string equivalent of the widgets into the content.
44546
44940
  *
@@ -44551,7 +44945,7 @@ function getAnswersFromWidgets(widgets) {
44551
44945
  * @param {PerseusRenderer["widgets"]} widgets
44552
44946
  * @returns
44553
44947
  */
44554
- function injectWidgets(content, widgets) {
44948
+ function injectWidgets(content, widgets, widgetProps) {
44555
44949
  // The types for taskProgress.itemData are not well defined,
44556
44950
  // so there is a chance that widgets or content could be undefined.
44557
44951
  // In that scenario, simply exit early and return the content as is.
@@ -44571,12 +44965,20 @@ function injectWidgets(content, widgets) {
44571
44965
  switch (widget.type) {
44572
44966
  case "radio":
44573
44967
  // Replace radio with the radio options
44574
- // '[[☃ Radio 1]]' -> 'option 1\noption 2\noption 3'
44968
+ // '[[☃ Radio 1]]' -> 'choice 1\nchoice 2\nchoice 3'
44969
+ // or if the current widget state is available with proper order,
44970
+ // '[[☃ Radio 1]]' ->
44971
+ // 'Option A: choice 1\nOption B: choice 2\nOption C: choice 3'
44575
44972
  const radio = widget;
44973
+ const radioProps = widgetProps == null ? void 0 : widgetProps[widgetID];
44576
44974
  if ((_radio$options = radio.options) != null && (_radio$options = _radio$options.choices) != null && _radio$options.length) {
44577
44975
  var _radio$options2;
44578
- let radioContext = radio.options.choices.map(choice => choice.content).join("\n");
44579
- if ((_radio$options2 = radio.options) != null && _radio$options2.randomize) {
44976
+ let radioContext = joinOptionContents(radioProps ? radioProps.choices.map(({
44977
+ content
44978
+ }, i) => ({
44979
+ content: `Option ${toOptionLetter(i)}: ${content}`
44980
+ })) : radio.options.choices);
44981
+ if (!radioProps && (_radio$options2 = radio.options) != null && _radio$options2.randomize) {
44580
44982
  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";
44581
44983
  }
44582
44984
  context = context.replace(`[[☃ ${widgetID}]]`, radioContext);
@@ -44685,7 +45087,7 @@ function injectWidgets(content, widgets) {
44685
45087
  // '[[☃ Orderer 1]]' -> 'option 1\noption 2\noption 3'
44686
45088
  const orderer = widget;
44687
45089
  if ((_orderer$options2 = orderer.options) != null && _orderer$options2.options) {
44688
- context = context.replace(`[[☃ ${widgetID}]]`, orderer.options.options.map(option => option.content).join("\n"));
45090
+ context = context.replace(`[[☃ ${widgetID}]]`, joinOptionContents(orderer.options.options));
44689
45091
  }
44690
45092
  break;
44691
45093
  case "sorter":