@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/es/index.js CHANGED
@@ -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:
@@ -28212,6 +28222,28 @@ function movePoint(index, destination) {
28212
28222
  destination
28213
28223
  };
28214
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
+ }
28215
28247
 
28216
28248
  /**
28217
28249
  * 44 is touch best practice and AAA compliant for WCAG
@@ -28238,6 +28270,11 @@ const normalizeCoords = (coordsList, ranges) => coordsList.map(coords => coords.
28238
28270
  const extent = ranges[i][1] - ranges[i][0];
28239
28271
  return (coord + ranges[i][1]) / extent;
28240
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
+ }
28241
28278
 
28242
28279
  const useTransform = (...points) => {
28243
28280
  const {
@@ -28293,6 +28330,9 @@ const getLines = points => points.map((point, i) => {
28293
28330
 
28294
28331
  const defaultStroke = "var(--movable-line-stroke-color)";
28295
28332
  const MovableLine = props => {
28333
+ const {
28334
+ state
28335
+ } = useGraphState();
28296
28336
  const {
28297
28337
  start,
28298
28338
  end,
@@ -28317,7 +28357,7 @@ const MovableLine = props => {
28317
28357
  onMove: newPoint => {
28318
28358
  onMove(vec.sub(newPoint, midpoint));
28319
28359
  },
28320
- constrain: p => p
28360
+ constrain: p => snap(state.snapStep, p)
28321
28361
  });
28322
28362
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("g", {
28323
28363
  ref: line,
@@ -28368,7 +28408,13 @@ function SVGLine(props) {
28368
28408
  });
28369
28409
  }
28370
28410
 
28411
+ // The hitbox size of 48px by 48px is preserved from the legacy interactive
28412
+ // graph.
28413
+ const hitboxSizePx = 48;
28371
28414
  const StyledMovablePoint = props => {
28415
+ const {
28416
+ state
28417
+ } = useGraphState();
28372
28418
  const hitboxRef = useRef(null);
28373
28419
  const {
28374
28420
  point,
@@ -28381,7 +28427,7 @@ const StyledMovablePoint = props => {
28381
28427
  gestureTarget: hitboxRef,
28382
28428
  point,
28383
28429
  onMove,
28384
- constrain: p => p
28430
+ constrain: p => snap(state.snapStep, p)
28385
28431
  });
28386
28432
  const [[x, y]] = useTransform(point);
28387
28433
  return /*#__PURE__*/React.createElement("g", {
@@ -28396,7 +28442,7 @@ const StyledMovablePoint = props => {
28396
28442
  }
28397
28443
  }, /*#__PURE__*/React.createElement("circle", {
28398
28444
  className: "movable-point-hitbox",
28399
- r: TARGET_SIZE / 2,
28445
+ r: hitboxSizePx / 2,
28400
28446
  cx: x,
28401
28447
  cy: y
28402
28448
  }), /*#__PURE__*/React.createElement("circle", {
@@ -28725,7 +28771,8 @@ const PolygonGraph = props => {
28725
28771
  type,
28726
28772
  showAngles,
28727
28773
  showSides,
28728
- range
28774
+ range,
28775
+ snapStep
28729
28776
  } = props.graphState;
28730
28777
  const points = coords != null ? coords : [[0, 0]];
28731
28778
  const pointsSum = points.reduce((acc, point) => vec.add(acc, point), [0, 0]);
@@ -28740,7 +28787,7 @@ const PolygonGraph = props => {
28740
28787
  const delta = vec.sub(newPoint, midpoint);
28741
28788
  dispatch(moveAll(delta));
28742
28789
  },
28743
- constrain: p => p
28790
+ constrain: p => snap(snapStep, p)
28744
28791
  });
28745
28792
  const active = hovered || focused || dragging;
28746
28793
  const lines = getLines(points);
@@ -28808,14 +28855,6 @@ function PointGraph(props) {
28808
28855
  })));
28809
28856
  }
28810
28857
 
28811
- const GraphStateContext = /*#__PURE__*/createContext({
28812
- state: {},
28813
- dispatch: () => {}
28814
- });
28815
- function useGraphState() {
28816
- return React__default.useContext(GraphStateContext);
28817
- }
28818
-
28819
28858
  function Arrow(props) {
28820
28859
  const {
28821
28860
  userTransform,
@@ -28859,17 +28898,120 @@ function AxisArrows() {
28859
28898
  }));
28860
28899
  }
28861
28900
 
28862
- const renderLineLabel = (n, [min, max]) => n !== -1 && n !== min && n !== max;
28863
- const axisOptions = (props, index) => ({
28864
- axis: props.markings === "graph",
28865
- lines: props.step[index],
28866
- subdivisions: props.step[index] / props.gridStep[index],
28867
- labels: n => renderLineLabel(n, props.range[index]) ? n : ""
28868
- });
28869
- const Grid = props => props.markings === "none" ? null : /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Coordinates.Cartesian, {
28870
- xAxis: axisOptions(props, 0),
28871
- yAxis: axisOptions(props, 1)
28872
- }), props.markings === "graph" && /*#__PURE__*/React.createElement(AxisArrows, null));
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
+ };
28873
29015
 
28874
29016
  // Displays a stylized open eye: lint warnings are visible
28875
29017
  const VisibleIcon = () => /*#__PURE__*/React.createElement("svg", {
@@ -29576,6 +29718,10 @@ function interactiveGraphReducer(state, action) {
29576
29718
  return doMoveAll(state, action);
29577
29719
  case MOVE_POINT:
29578
29720
  return doMovePoint(state, action);
29721
+ case CHANGE_SNAP_STEP:
29722
+ return doChangeSnapStep(state, action);
29723
+ case CHANGE_RANGE:
29724
+ return doChangeRange(state, action);
29579
29725
  default:
29580
29726
  throw new UnreachableCaseError(action);
29581
29727
  }
@@ -29597,14 +29743,11 @@ function doMoveControlPoint(state, action) {
29597
29743
  update: tuple => setAtIndex({
29598
29744
  array: tuple,
29599
29745
  index: action.pointIndex,
29600
- newValue: snap({
29746
+ newValue: snap(snapStep, bound$1({
29601
29747
  snapStep,
29602
- point: bound$1({
29603
- snapStep,
29604
- range,
29605
- point: action.destination
29606
- })
29607
- })
29748
+ range,
29749
+ point: action.destination
29750
+ }))
29608
29751
  })
29609
29752
  });
29610
29753
  const coordsToCheck = newCoords[action.itemIndex];
@@ -29647,13 +29790,7 @@ function doMoveLine(state, action) {
29647
29790
  snapStep,
29648
29791
  range
29649
29792
  });
29650
- const newLine = [snap({
29651
- snapStep,
29652
- point: vec.add(currentLine[0], change)
29653
- }), snap({
29654
- snapStep,
29655
- point: vec.add(currentLine[1], change)
29656
- })];
29793
+ const newLine = [snap(snapStep, vec.add(currentLine[0], change)), snap(snapStep, vec.add(currentLine[1], change))];
29657
29794
  const newCoords = setAtIndex({
29658
29795
  array: state.coords,
29659
29796
  index: action.itemIndex,
@@ -29683,10 +29820,7 @@ function doMoveAll(state, action) {
29683
29820
  snapStep,
29684
29821
  range
29685
29822
  });
29686
- const newCoords = state.coords.map(point => snap({
29687
- snapStep,
29688
- point: vec.add(point, change)
29689
- }));
29823
+ const newCoords = state.coords.map(point => snap(snapStep, vec.add(point, change)));
29690
29824
  return _extends({}, state, {
29691
29825
  hasBeenInteractedWith: true,
29692
29826
  coords: newCoords
@@ -29707,14 +29841,11 @@ function doMovePoint(state, action) {
29707
29841
  coords: setAtIndex({
29708
29842
  array: state.coords,
29709
29843
  index: action.index,
29710
- newValue: snap({
29844
+ newValue: snap(state.snapStep, bound$1({
29711
29845
  snapStep: state.snapStep,
29712
- point: bound$1({
29713
- snapStep: state.snapStep,
29714
- range: state.range,
29715
- point: action.destination
29716
- })
29717
- })
29846
+ range: state.range,
29847
+ point: action.destination
29848
+ }))
29718
29849
  })
29719
29850
  });
29720
29851
  }
@@ -29722,6 +29853,26 @@ function doMovePoint(state, action) {
29722
29853
  throw new Error("The movePoint action is only for point and polygon graphs");
29723
29854
  }
29724
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
+ }
29725
29876
  const getChange = (coords, delta, constraintOpts) => {
29726
29877
  const [deltaX, deltaY] = delta;
29727
29878
  const maxMoves = coords.map(point => maxMove(_extends({}, constraintOpts, {
@@ -29738,15 +29889,6 @@ const getChange = (coords, delta, constraintOpts) => {
29738
29889
  const dy = clamp(deltaY, minYMove, maxYMove);
29739
29890
  return [dx, dy];
29740
29891
  };
29741
- function snap({
29742
- snapStep,
29743
- point
29744
- }) {
29745
- const [requestedX, requestedY] = point;
29746
- const [snapX, snapY] = snapStep;
29747
- return [Math.round(requestedX / snapX) * snapX, Math.round(requestedY / snapY) * snapY];
29748
- }
29749
-
29750
29892
  // Returns the closest point to the given `point` that is within the graph
29751
29893
  // bounds given in `state`.
29752
29894
  function bound$1({
@@ -30098,6 +30240,18 @@ const MafsGraph = /*#__PURE__*/React.forwardRef((props, ref) => {
30098
30240
  }
30099
30241
  prevState.current = state;
30100
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]);
30101
30255
  React.useImperativeHandle(ref, () => ({
30102
30256
  getUserInput: () => getGradableGraph(state, props.graph)
30103
30257
  }));
@@ -30132,7 +30286,13 @@ const MafsGraph = /*#__PURE__*/React.forwardRef((props, ref) => {
30132
30286
  zoom: false,
30133
30287
  width: width,
30134
30288
  height: height
30135
- }, /*#__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, {
30136
30296
  lockedFigures: props.lockedFigures
30137
30297
  }), renderGraph({
30138
30298
  state,
@@ -31911,9 +32071,9 @@ class InteractiveGraph extends React.Component {
31911
32071
  };
31912
32072
  }
31913
32073
  } else if (userInput.type === "segment" && rubric.correct.type === "segment" && userInput.coords != null) {
31914
- var _rubric$correct$coord2;
31915
- let guess = userInput.coords.slice();
31916
- 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);
31917
32077
  guess = _.invoke(guess, "sort").sort();
31918
32078
  // @ts-expect-error - TS2345 - Argument of type '(readonly Coord[])[] | undefined' is not assignable to parameter of type 'Collection<any>'.
31919
32079
  correct = _.invoke(correct, "sort").sort();
@@ -42181,7 +42341,7 @@ const init = function init(options) {
42181
42341
 
42182
42342
  // This file is processed by a Rollup plugin (replace) to inject the production
42183
42343
  const libName = "@khanacademy/perseus";
42184
- const libVersion = "21.1.0";
42344
+ const libVersion = "21.2.0";
42185
42345
  addLibraryVersionToPerseusDebug(libName, libVersion);
42186
42346
 
42187
42347
  const apiVersion = {
@@ -43305,6 +43465,9 @@ const ItemExtras = [
43305
43465
  // A background image applied to various widgets.
43306
43466
 
43307
43467
  const PerseusExpressionAnswerFormConsidered = ["correct", "wrong", "ungraded"];
43468
+
43469
+ // 2D range: xMin, xMax, yMin, yMax
43470
+
43308
43471
  const plotterPlotTypes = ["bar", "line", "pic", "histogram", "dotplot"];
43309
43472
 
43310
43473
  /**