@khanacademy/perseus 21.1.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/index.js CHANGED
@@ -3281,6 +3281,15 @@ const textarea = {
3281
3281
  */
3282
3282
  const unescapeMathMode$1 = label => label.startsWith("$") && label.endsWith("$") ? label.slice(1, -1) : label;
3283
3283
  const random$1 = seededRNG$1(new Date().getTime() & 0xffffffff);
3284
+
3285
+ // TODO(benchristel): in the future, we may want to make deepClone work for
3286
+ // Record<string, Cloneable> as well. Currently, it only does arrays.
3287
+ function deepClone(obj) {
3288
+ if (Array.isArray(obj)) {
3289
+ return obj.map(deepClone);
3290
+ }
3291
+ return obj;
3292
+ }
3284
3293
  const Util = {
3285
3294
  inputPathsEqual,
3286
3295
  nestedMap: nestedMap$2,
@@ -3326,7 +3335,8 @@ const Util = {
3326
3335
  getDataUrl: getDataUrl,
3327
3336
  textarea,
3328
3337
  unescapeMathMode: unescapeMathMode$1,
3329
- random: random$1
3338
+ random: random$1,
3339
+ deepClone
3330
3340
  };
3331
3341
 
3332
3342
  // Derived from the MIT-licensed:
@@ -28364,6 +28374,28 @@ function movePoint(index, destination) {
28364
28374
  destination
28365
28375
  };
28366
28376
  }
28377
+ const CHANGE_SNAP_STEP = "change-snap-step";
28378
+ function changeSnapStep(snapStep) {
28379
+ return {
28380
+ type: CHANGE_SNAP_STEP,
28381
+ snapStep
28382
+ };
28383
+ }
28384
+ const CHANGE_RANGE = "change-range";
28385
+ function changeRange(range) {
28386
+ return {
28387
+ type: CHANGE_RANGE,
28388
+ range
28389
+ };
28390
+ }
28391
+
28392
+ const GraphStateContext = /*#__PURE__*/React.createContext({
28393
+ state: {},
28394
+ dispatch: () => {}
28395
+ });
28396
+ function useGraphState() {
28397
+ return React__default["default"].useContext(GraphStateContext);
28398
+ }
28367
28399
 
28368
28400
  /**
28369
28401
  * 44 is touch best practice and AAA compliant for WCAG
@@ -28390,6 +28422,11 @@ const normalizeCoords = (coordsList, ranges) => coordsList.map(coords => coords.
28390
28422
  const extent = ranges[i][1] - ranges[i][0];
28391
28423
  return (coord + ranges[i][1]) / extent;
28392
28424
  }));
28425
+ function snap(snapStep, point) {
28426
+ const [requestedX, requestedY] = point;
28427
+ const [snapX, snapY] = snapStep;
28428
+ return [Math.round(requestedX / snapX) * snapX, Math.round(requestedY / snapY) * snapY];
28429
+ }
28393
28430
 
28394
28431
  const useTransform = function () {
28395
28432
  const {
@@ -28448,6 +28485,9 @@ const getLines = points => points.map((point, i) => {
28448
28485
 
28449
28486
  const defaultStroke = "var(--movable-line-stroke-color)";
28450
28487
  const MovableLine = props => {
28488
+ const {
28489
+ state
28490
+ } = useGraphState();
28451
28491
  const {
28452
28492
  start,
28453
28493
  end,
@@ -28472,7 +28512,7 @@ const MovableLine = props => {
28472
28512
  onMove: newPoint => {
28473
28513
  onMove(mafs.vec.sub(newPoint, midpoint));
28474
28514
  },
28475
- constrain: p => p
28515
+ constrain: p => snap(state.snapStep, p)
28476
28516
  });
28477
28517
  return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement("g", {
28478
28518
  ref: line,
@@ -28523,7 +28563,13 @@ function SVGLine(props) {
28523
28563
  });
28524
28564
  }
28525
28565
 
28566
+ // The hitbox size of 48px by 48px is preserved from the legacy interactive
28567
+ // graph.
28568
+ const hitboxSizePx = 48;
28526
28569
  const StyledMovablePoint = props => {
28570
+ const {
28571
+ state
28572
+ } = useGraphState();
28527
28573
  const hitboxRef = React.useRef(null);
28528
28574
  const {
28529
28575
  point,
@@ -28536,7 +28582,7 @@ const StyledMovablePoint = props => {
28536
28582
  gestureTarget: hitboxRef,
28537
28583
  point,
28538
28584
  onMove,
28539
- constrain: p => p
28585
+ constrain: p => snap(state.snapStep, p)
28540
28586
  });
28541
28587
  const [[x, y]] = useTransform(point);
28542
28588
  return /*#__PURE__*/React__namespace.createElement("g", {
@@ -28551,7 +28597,7 @@ const StyledMovablePoint = props => {
28551
28597
  }
28552
28598
  }, /*#__PURE__*/React__namespace.createElement("circle", {
28553
28599
  className: "movable-point-hitbox",
28554
- r: TARGET_SIZE / 2,
28600
+ r: hitboxSizePx / 2,
28555
28601
  cx: x,
28556
28602
  cy: y
28557
28603
  }), /*#__PURE__*/React__namespace.createElement("circle", {
@@ -28887,7 +28933,8 @@ const PolygonGraph = props => {
28887
28933
  type,
28888
28934
  showAngles,
28889
28935
  showSides,
28890
- range
28936
+ range,
28937
+ snapStep
28891
28938
  } = props.graphState;
28892
28939
  const points = coords ?? [[0, 0]];
28893
28940
  const pointsSum = points.reduce((acc, point) => mafs.vec.add(acc, point), [0, 0]);
@@ -28902,7 +28949,7 @@ const PolygonGraph = props => {
28902
28949
  const delta = mafs.vec.sub(newPoint, midpoint);
28903
28950
  dispatch(moveAll(delta));
28904
28951
  },
28905
- constrain: p => p
28952
+ constrain: p => snap(snapStep, p)
28906
28953
  });
28907
28954
  const active = hovered || focused || dragging;
28908
28955
  const lines = getLines(points);
@@ -28971,14 +29018,6 @@ function PointGraph(props) {
28971
29018
  })));
28972
29019
  }
28973
29020
 
28974
- const GraphStateContext = /*#__PURE__*/React.createContext({
28975
- state: {},
28976
- dispatch: () => {}
28977
- });
28978
- function useGraphState() {
28979
- return React__default["default"].useContext(GraphStateContext);
28980
- }
28981
-
28982
29021
  function Arrow(props) {
28983
29022
  const {
28984
29023
  userTransform,
@@ -29022,20 +29061,122 @@ function AxisArrows() {
29022
29061
  }));
29023
29062
  }
29024
29063
 
29025
- const renderLineLabel = (n, _ref) => {
29026
- let [min, max] = _ref;
29027
- return n !== -1 && n !== min && n !== max;
29064
+ const tickSize = 10;
29065
+ const tickStyle = {
29066
+ stroke: "black",
29067
+ strokeWidth: 1
29068
+ };
29069
+ const YGridTick = _ref => {
29070
+ let {
29071
+ y
29072
+ } = _ref;
29073
+ const pointOnAxis = [0, y];
29074
+ const [[xPosition, yPosition]] = useTransform(pointOnAxis);
29075
+ return /*#__PURE__*/React__namespace.createElement("g", {
29076
+ className: "y-axis-ticks"
29077
+ }, /*#__PURE__*/React__namespace.createElement("line", {
29078
+ x1: xPosition - tickSize / 2,
29079
+ y1: yPosition,
29080
+ x2: xPosition + tickSize / 2,
29081
+ y2: yPosition,
29082
+ style: tickStyle
29083
+ }));
29084
+ };
29085
+ const XGridTick = _ref2 => {
29086
+ let {
29087
+ x
29088
+ } = _ref2;
29089
+ const pointOnAxis = [x, 0];
29090
+ const [[xPosition, yPosition]] = useTransform(pointOnAxis);
29091
+ return /*#__PURE__*/React__namespace.createElement("g", {
29092
+ className: "x-axis-ticks"
29093
+ }, /*#__PURE__*/React__namespace.createElement("line", {
29094
+ x1: xPosition,
29095
+ y1: yPosition + tickSize / 2,
29096
+ x2: xPosition,
29097
+ y2: yPosition - tickSize / 2,
29098
+ style: tickStyle
29099
+ }));
29100
+ };
29101
+ function generateTickLocations(tickStep, min, max) {
29102
+ const ticks = [];
29103
+ for (let i = 0 + tickStep; i < max; i += tickStep) {
29104
+ ticks.push(i);
29105
+ }
29106
+ for (let i = 0 - tickStep; i > min; i -= tickStep) {
29107
+ ticks.push(i);
29108
+ }
29109
+ return ticks;
29110
+ }
29111
+ const AxisTicks = props => {
29112
+ const range = props.range;
29113
+ const [xMin, xMax] = range[0];
29114
+ const [yMin, yMax] = range[1];
29115
+ const yTickStep = props.tickStep[1];
29116
+ const xTickStep = props.tickStep[0];
29117
+ const yGridTicks = generateTickLocations(yTickStep, yMin, yMax);
29118
+ const xGridTicks = generateTickLocations(xTickStep, xMin, xMax);
29119
+ return /*#__PURE__*/React__namespace.createElement("g", {
29120
+ className: "axis-ticks"
29121
+ }, yGridTicks.map(y => {
29122
+ return /*#__PURE__*/React__namespace.createElement(YGridTick, {
29123
+ y: y,
29124
+ key: `y-grid-tick-${y}`
29125
+ });
29126
+ }), xGridTicks.map(x => {
29127
+ return /*#__PURE__*/React__namespace.createElement(XGridTick, {
29128
+ x: x,
29129
+ key: `x-grid-tick-${x}`
29130
+ });
29131
+ }));
29132
+ };
29133
+
29134
+ /**
29135
+ * lineLabelText get the text that should
29136
+ * be displayed for a specific tick place
29137
+ *
29138
+ * @param {number} n the tick number
29139
+ * @param {number} tickStep how frequently a label should appear
29140
+ * @param {vec.Vector2} range the min/max range on the axis
29141
+ */
29142
+ const lineLabelText = (n, tickStep, range) => {
29143
+ const [min, max] = range;
29144
+ const isOnStep = n % tickStep === 0;
29145
+ const isNegativeOne = n === -1;
29146
+ const isMin = n === min;
29147
+ const isMax = n === max;
29148
+ const shouldRender = isOnStep && !isNegativeOne && !isMin && !isMax;
29149
+ return shouldRender ? `${n}` : "";
29150
+ };
29151
+
29152
+ /**
29153
+ * axisOptions determine axis options for Mafs
29154
+ *
29155
+ * axisIndex is for grabbing data in an array that contains
29156
+ * data for multiple axes. For example range: [[-10, 10], [-10, 10]]
29157
+ * range[0] is data for the x axis and range[1] is data for the y axis
29158
+ *
29159
+ * @param {GridProps} props
29160
+ * @param {number} axisIndex which axis we're getting options for
29161
+ */
29162
+ const axisOptions = (props, axisIndex) => {
29163
+ const axisStep = props.tickStep[axisIndex];
29164
+ const axisRange = props.range[axisIndex];
29165
+ return {
29166
+ axis: props.markings === "graph",
29167
+ lines: props.gridStep[axisIndex],
29168
+ labels: n => lineLabelText(n, axisStep, axisRange)
29169
+ };
29170
+ };
29171
+ const Grid = props => {
29172
+ return props.markings === "none" ? null : /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(mafs.Coordinates.Cartesian, {
29173
+ xAxis: axisOptions(props, 0),
29174
+ yAxis: axisOptions(props, 1)
29175
+ }), /*#__PURE__*/React__namespace.createElement(AxisTicks, {
29176
+ range: props.range,
29177
+ tickStep: props.tickStep
29178
+ }), props.markings === "graph" && /*#__PURE__*/React__namespace.createElement(AxisArrows, null));
29028
29179
  };
29029
- const axisOptions = (props, index) => ({
29030
- axis: props.markings === "graph",
29031
- lines: props.step[index],
29032
- subdivisions: props.step[index] / props.gridStep[index],
29033
- labels: n => renderLineLabel(n, props.range[index]) ? n : ""
29034
- });
29035
- const Grid = props => props.markings === "none" ? null : /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(mafs.Coordinates.Cartesian, {
29036
- xAxis: axisOptions(props, 0),
29037
- yAxis: axisOptions(props, 1)
29038
- }), props.markings === "graph" && /*#__PURE__*/React__namespace.createElement(AxisArrows, null));
29039
29180
 
29040
29181
  // Displays a stylized open eye: lint warnings are visible
29041
29182
  const VisibleIcon = () => /*#__PURE__*/React__namespace.createElement("svg", {
@@ -29743,6 +29884,10 @@ function interactiveGraphReducer(state, action) {
29743
29884
  return doMoveAll(state, action);
29744
29885
  case MOVE_POINT:
29745
29886
  return doMovePoint(state, action);
29887
+ case CHANGE_SNAP_STEP:
29888
+ return doChangeSnapStep(state, action);
29889
+ case CHANGE_RANGE:
29890
+ return doChangeRange(state, action);
29746
29891
  default:
29747
29892
  throw new wonderStuffCore.UnreachableCaseError(action);
29748
29893
  }
@@ -29764,14 +29909,11 @@ function doMoveControlPoint(state, action) {
29764
29909
  update: tuple => setAtIndex({
29765
29910
  array: tuple,
29766
29911
  index: action.pointIndex,
29767
- newValue: snap({
29912
+ newValue: snap(snapStep, bound$1({
29768
29913
  snapStep,
29769
- point: bound$1({
29770
- snapStep,
29771
- range,
29772
- point: action.destination
29773
- })
29774
- })
29914
+ range,
29915
+ point: action.destination
29916
+ }))
29775
29917
  })
29776
29918
  });
29777
29919
  const coordsToCheck = newCoords[action.itemIndex];
@@ -29815,13 +29957,7 @@ function doMoveLine(state, action) {
29815
29957
  snapStep,
29816
29958
  range
29817
29959
  });
29818
- const newLine = [snap({
29819
- snapStep,
29820
- point: mafs.vec.add(currentLine[0], change)
29821
- }), snap({
29822
- snapStep,
29823
- point: mafs.vec.add(currentLine[1], change)
29824
- })];
29960
+ const newLine = [snap(snapStep, mafs.vec.add(currentLine[0], change)), snap(snapStep, mafs.vec.add(currentLine[1], change))];
29825
29961
  const newCoords = setAtIndex({
29826
29962
  array: state.coords,
29827
29963
  index: action.itemIndex,
@@ -29852,10 +29988,7 @@ function doMoveAll(state, action) {
29852
29988
  snapStep,
29853
29989
  range
29854
29990
  });
29855
- const newCoords = state.coords.map(point => snap({
29856
- snapStep,
29857
- point: mafs.vec.add(point, change)
29858
- }));
29991
+ const newCoords = state.coords.map(point => snap(snapStep, mafs.vec.add(point, change)));
29859
29992
  return {
29860
29993
  ...state,
29861
29994
  hasBeenInteractedWith: true,
@@ -29878,14 +30011,11 @@ function doMovePoint(state, action) {
29878
30011
  coords: setAtIndex({
29879
30012
  array: state.coords,
29880
30013
  index: action.index,
29881
- newValue: snap({
30014
+ newValue: snap(state.snapStep, bound$1({
29882
30015
  snapStep: state.snapStep,
29883
- point: bound$1({
29884
- snapStep: state.snapStep,
29885
- range: state.range,
29886
- point: action.destination
29887
- })
29888
- })
30016
+ range: state.range,
30017
+ point: action.destination
30018
+ }))
29889
30019
  })
29890
30020
  };
29891
30021
  }
@@ -29893,6 +30023,28 @@ function doMovePoint(state, action) {
29893
30023
  throw new Error("The movePoint action is only for point and polygon graphs");
29894
30024
  }
29895
30025
  }
30026
+ function doChangeSnapStep(state, action) {
30027
+ if (
30028
+ // Deep equality check since these are arrays
30029
+ ___default["default"].isEqual(state.snapStep, action.snapStep)) {
30030
+ return state;
30031
+ }
30032
+ return {
30033
+ ...state,
30034
+ snapStep: action.snapStep
30035
+ };
30036
+ }
30037
+ function doChangeRange(state, action) {
30038
+ if (
30039
+ // Deep equality check since these are arrays
30040
+ ___default["default"].isEqual(state.range, action.range)) {
30041
+ return state;
30042
+ }
30043
+ return {
30044
+ ...state,
30045
+ range: action.range
30046
+ };
30047
+ }
29896
30048
  const getChange = (coords, delta, constraintOpts) => {
29897
30049
  const [deltaX, deltaY] = delta;
29898
30050
  const maxMoves = coords.map(point => maxMove({
@@ -29911,24 +30063,14 @@ const getChange = (coords, delta, constraintOpts) => {
29911
30063
  const dy = clamp(deltaY, minYMove, maxYMove);
29912
30064
  return [dx, dy];
29913
30065
  };
29914
- function snap(_ref) {
29915
- let {
29916
- snapStep,
29917
- point
29918
- } = _ref;
29919
- const [requestedX, requestedY] = point;
29920
- const [snapX, snapY] = snapStep;
29921
- return [Math.round(requestedX / snapX) * snapX, Math.round(requestedY / snapY) * snapY];
29922
- }
29923
-
29924
30066
  // Returns the closest point to the given `point` that is within the graph
29925
30067
  // bounds given in `state`.
29926
- function bound$1(_ref2) {
30068
+ function bound$1(_ref) {
29927
30069
  let {
29928
30070
  snapStep,
29929
30071
  range,
29930
30072
  point
29931
- } = _ref2;
30073
+ } = _ref;
29932
30074
  const [requestedX, requestedY] = point;
29933
30075
  const [snapX, snapY] = snapStep;
29934
30076
  const [[minX, maxX], [minY, maxY]] = range;
@@ -29936,12 +30078,12 @@ function bound$1(_ref2) {
29936
30078
  }
29937
30079
 
29938
30080
  // Returns the vector from the given point to the top-right corner of the graph
29939
- function maxMove(_ref3) {
30081
+ function maxMove(_ref2) {
29940
30082
  let {
29941
30083
  snapStep,
29942
30084
  range,
29943
30085
  point
29944
- } = _ref3;
30086
+ } = _ref2;
29945
30087
  const topRight = bound$1({
29946
30088
  snapStep,
29947
30089
  range,
@@ -29952,12 +30094,12 @@ function maxMove(_ref3) {
29952
30094
 
29953
30095
  // Returns the vector from the given point to the bottom-left corner of the
29954
30096
  // graph
29955
- function minMove(_ref4) {
30097
+ function minMove(_ref3) {
29956
30098
  let {
29957
30099
  snapStep,
29958
30100
  range,
29959
30101
  point
29960
- } = _ref4;
30102
+ } = _ref3;
29961
30103
  const bottomLeft = bound$1({
29962
30104
  snapStep,
29963
30105
  range,
@@ -30282,6 +30424,18 @@ const MafsGraph = /*#__PURE__*/React__namespace.forwardRef((props, ref) => {
30282
30424
  }
30283
30425
  prevState.current = state;
30284
30426
  }, [props, state]);
30427
+
30428
+ // Destructuring first to keep useEffect from making excess calls
30429
+ const [xSnap, ySnap] = props.snapStep;
30430
+ React.useEffect(() => {
30431
+ dispatch(changeSnapStep([xSnap, ySnap]));
30432
+ }, [xSnap, ySnap]);
30433
+
30434
+ // Destructuring first to keep useEffect from making excess calls
30435
+ const [[xMinRange, xMaxRange], [yMinRange, yMaxRange]] = props.range;
30436
+ React.useEffect(() => {
30437
+ dispatch(changeRange([[xMinRange, xMaxRange], [yMinRange, yMaxRange]]));
30438
+ }, [xMinRange, xMaxRange, yMinRange, yMaxRange]);
30285
30439
  React__namespace.useImperativeHandle(ref, () => ({
30286
30440
  getUserInput: () => getGradableGraph(state, props.graph)
30287
30441
  }));
@@ -30316,7 +30470,13 @@ const MafsGraph = /*#__PURE__*/React__namespace.forwardRef((props, ref) => {
30316
30470
  zoom: false,
30317
30471
  width: width,
30318
30472
  height: height
30319
- }, /*#__PURE__*/React__namespace.createElement(SvgDefs, null), /*#__PURE__*/React__namespace.createElement(Grid, props), props.lockedFigures && /*#__PURE__*/React__namespace.createElement(GraphLockedLayer, {
30473
+ }, /*#__PURE__*/React__namespace.createElement(SvgDefs, null), /*#__PURE__*/React__namespace.createElement(Grid, {
30474
+ tickStep: props.step,
30475
+ gridStep: props.gridStep,
30476
+ range: props.range,
30477
+ containerSizeClass: props.containerSizeClass,
30478
+ markings: props.markings
30479
+ }), props.lockedFigures && /*#__PURE__*/React__namespace.createElement(GraphLockedLayer, {
30320
30480
  lockedFigures: props.lockedFigures
30321
30481
  }), renderGraph({
30322
30482
  state,
@@ -32066,8 +32226,8 @@ class InteractiveGraph extends React__namespace.Component {
32066
32226
  };
32067
32227
  }
32068
32228
  } else if (userInput.type === "segment" && rubric.correct.type === "segment" && userInput.coords != null) {
32069
- let guess = userInput.coords.slice();
32070
- let correct = rubric.correct.coords?.slice();
32229
+ let guess = Util.deepClone(userInput.coords);
32230
+ let correct = Util.deepClone(rubric.correct?.coords);
32071
32231
  guess = ___default["default"].invoke(guess, "sort").sort();
32072
32232
  // @ts-expect-error - TS2345 - Argument of type '(readonly Coord[])[] | undefined' is not assignable to parameter of type 'Collection<any>'.
32073
32233
  correct = ___default["default"].invoke(correct, "sort").sort();
@@ -42329,7 +42489,7 @@ const init = function (options) {
42329
42489
 
42330
42490
  // This file is processed by a Rollup plugin (replace) to inject the production
42331
42491
  const libName = "@khanacademy/perseus";
42332
- const libVersion = "21.1.0";
42492
+ const libVersion = "21.2.0";
42333
42493
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
42334
42494
 
42335
42495
  const apiVersion = {
@@ -43458,6 +43618,9 @@ const ItemExtras = [
43458
43618
  // A background image applied to various widgets.
43459
43619
 
43460
43620
  const PerseusExpressionAnswerFormConsidered = ["correct", "wrong", "ungraded"];
43621
+
43622
+ // 2D range: xMin, xMax, yMin, yMax
43623
+
43461
43624
  const plotterPlotTypes = ["bar", "line", "pic", "histogram", "dotplot"];
43462
43625
 
43463
43626
  /**