@khanacademy/perseus 21.1.0 → 21.2.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.
package/dist/es/index.css CHANGED
@@ -4284,6 +4284,7 @@ html body .mq-editable-field .mq-selection .mq-nthroot-container * {
4284
4284
  --movable-point-ring-radius: calc(2px + var(--movable-point-center-radius));
4285
4285
  --movable-point-halo-radius: calc(3px + var(--movable-point-ring-radius));
4286
4286
  --movable-point-hover-expansion: 2px;
4287
+ --movable-point-focus-ring-offset: 2px;
4287
4288
 
4288
4289
  --movable-line-stroke-color: var(--mafs-blue);
4289
4290
  --movable-line-stroke-weight: 2px;
@@ -4305,7 +4306,13 @@ html body .mq-editable-field .mq-selection .mq-nthroot-container * {
4305
4306
  fill: transparent;
4306
4307
  }
4307
4308
 
4308
- .MafsView :is(.movable-point-center, .movable-point-ring, .movable-point-halo) {
4309
+ .MafsView
4310
+ :is(
4311
+ .movable-point-center,
4312
+ .movable-point-ring,
4313
+ .movable-point-halo,
4314
+ .movable-point-focus-outline
4315
+ ) {
4309
4316
  transition: r 0.15s ease-out;
4310
4317
  }
4311
4318
 
@@ -4344,7 +4351,24 @@ html body .mq-editable-field .mq-selection .mq-nthroot-container * {
4344
4351
  );
4345
4352
  }
4346
4353
 
4347
- .MafsView .movable-point:focus-visible .movable-point-halo {
4348
- outline: 2px solid blue;
4349
- border-radius: 99px;
4354
+ .MafsView .movable-point .movable-point-focus-outline {
4355
+ visibility: hidden;
4356
+ r: calc(
4357
+ var(--movable-point-halo-radius) +
4358
+ var(--movable-point-focus-ring-offset)
4359
+ );
4360
+ stroke-width: 2px;
4361
+ fill: none;
4362
+ stroke: var(--mafs-blue);
4363
+ }
4364
+
4365
+ .MafsView .movable-point:hover .movable-point-focus-outline {
4366
+ r: calc(
4367
+ var(--movable-point-hover-expansion) + var(--movable-point-halo-radius) +
4368
+ var(--movable-point-focus-ring-offset)
4369
+ );
4370
+ }
4371
+
4372
+ .MafsView .movable-point:focus-visible .movable-point-focus-outline {
4373
+ visibility: visible;
4350
4374
  }
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", {
@@ -28407,6 +28453,10 @@ const StyledMovablePoint = props => {
28407
28453
  className: "movable-point-ring",
28408
28454
  cx: x,
28409
28455
  cy: y
28456
+ }), /*#__PURE__*/React.createElement("circle", {
28457
+ className: "movable-point-focus-outline",
28458
+ cx: x,
28459
+ cy: y
28410
28460
  }), /*#__PURE__*/React.createElement("circle", {
28411
28461
  className: "movable-point-center",
28412
28462
  cx: x,
@@ -28725,7 +28775,8 @@ const PolygonGraph = props => {
28725
28775
  type,
28726
28776
  showAngles,
28727
28777
  showSides,
28728
- range
28778
+ range,
28779
+ snapStep
28729
28780
  } = props.graphState;
28730
28781
  const points = coords != null ? coords : [[0, 0]];
28731
28782
  const pointsSum = points.reduce((acc, point) => vec.add(acc, point), [0, 0]);
@@ -28740,7 +28791,7 @@ const PolygonGraph = props => {
28740
28791
  const delta = vec.sub(newPoint, midpoint);
28741
28792
  dispatch(moveAll(delta));
28742
28793
  },
28743
- constrain: p => p
28794
+ constrain: p => snap(snapStep, p)
28744
28795
  });
28745
28796
  const active = hovered || focused || dragging;
28746
28797
  const lines = getLines(points);
@@ -28808,14 +28859,6 @@ function PointGraph(props) {
28808
28859
  })));
28809
28860
  }
28810
28861
 
28811
- const GraphStateContext = /*#__PURE__*/createContext({
28812
- state: {},
28813
- dispatch: () => {}
28814
- });
28815
- function useGraphState() {
28816
- return React__default.useContext(GraphStateContext);
28817
- }
28818
-
28819
28862
  function Arrow(props) {
28820
28863
  const {
28821
28864
  userTransform,
@@ -28859,17 +28902,120 @@ function AxisArrows() {
28859
28902
  }));
28860
28903
  }
28861
28904
 
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));
28905
+ const tickSize = 10;
28906
+ const tickStyle = {
28907
+ stroke: "black",
28908
+ strokeWidth: 1
28909
+ };
28910
+ const YGridTick = ({
28911
+ y
28912
+ }) => {
28913
+ const pointOnAxis = [0, y];
28914
+ const [[xPosition, yPosition]] = useTransform(pointOnAxis);
28915
+ return /*#__PURE__*/React.createElement("g", {
28916
+ className: "y-axis-ticks"
28917
+ }, /*#__PURE__*/React.createElement("line", {
28918
+ x1: xPosition - tickSize / 2,
28919
+ y1: yPosition,
28920
+ x2: xPosition + tickSize / 2,
28921
+ y2: yPosition,
28922
+ style: tickStyle
28923
+ }));
28924
+ };
28925
+ const XGridTick = ({
28926
+ x
28927
+ }) => {
28928
+ const pointOnAxis = [x, 0];
28929
+ const [[xPosition, yPosition]] = useTransform(pointOnAxis);
28930
+ return /*#__PURE__*/React.createElement("g", {
28931
+ className: "x-axis-ticks"
28932
+ }, /*#__PURE__*/React.createElement("line", {
28933
+ x1: xPosition,
28934
+ y1: yPosition + tickSize / 2,
28935
+ x2: xPosition,
28936
+ y2: yPosition - tickSize / 2,
28937
+ style: tickStyle
28938
+ }));
28939
+ };
28940
+ function generateTickLocations(tickStep, min, max) {
28941
+ const ticks = [];
28942
+ for (let i = 0 + tickStep; i < max; i += tickStep) {
28943
+ ticks.push(i);
28944
+ }
28945
+ for (let i = 0 - tickStep; i > min; i -= tickStep) {
28946
+ ticks.push(i);
28947
+ }
28948
+ return ticks;
28949
+ }
28950
+ const AxisTicks = props => {
28951
+ const range = props.range;
28952
+ const [xMin, xMax] = range[0];
28953
+ const [yMin, yMax] = range[1];
28954
+ const yTickStep = props.tickStep[1];
28955
+ const xTickStep = props.tickStep[0];
28956
+ const yGridTicks = generateTickLocations(yTickStep, yMin, yMax);
28957
+ const xGridTicks = generateTickLocations(xTickStep, xMin, xMax);
28958
+ return /*#__PURE__*/React.createElement("g", {
28959
+ className: "axis-ticks"
28960
+ }, yGridTicks.map(y => {
28961
+ return /*#__PURE__*/React.createElement(YGridTick, {
28962
+ y: y,
28963
+ key: `y-grid-tick-${y}`
28964
+ });
28965
+ }), xGridTicks.map(x => {
28966
+ return /*#__PURE__*/React.createElement(XGridTick, {
28967
+ x: x,
28968
+ key: `x-grid-tick-${x}`
28969
+ });
28970
+ }));
28971
+ };
28972
+
28973
+ /**
28974
+ * lineLabelText get the text that should
28975
+ * be displayed for a specific tick place
28976
+ *
28977
+ * @param {number} n the tick number
28978
+ * @param {number} tickStep how frequently a label should appear
28979
+ * @param {vec.Vector2} range the min/max range on the axis
28980
+ */
28981
+ const lineLabelText = (n, tickStep, range) => {
28982
+ const [min, max] = range;
28983
+ const isOnStep = n % tickStep === 0;
28984
+ const isNegativeOne = n === -1;
28985
+ const isMin = n === min;
28986
+ const isMax = n === max;
28987
+ const shouldRender = isOnStep && !isNegativeOne && !isMin && !isMax;
28988
+ return shouldRender ? `${n}` : "";
28989
+ };
28990
+
28991
+ /**
28992
+ * axisOptions determine axis options for Mafs
28993
+ *
28994
+ * axisIndex is for grabbing data in an array that contains
28995
+ * data for multiple axes. For example range: [[-10, 10], [-10, 10]]
28996
+ * range[0] is data for the x axis and range[1] is data for the y axis
28997
+ *
28998
+ * @param {GridProps} props
28999
+ * @param {number} axisIndex which axis we're getting options for
29000
+ */
29001
+ const axisOptions = (props, axisIndex) => {
29002
+ const axisStep = props.tickStep[axisIndex];
29003
+ const axisRange = props.range[axisIndex];
29004
+ return {
29005
+ axis: props.markings === "graph",
29006
+ lines: props.gridStep[axisIndex],
29007
+ labels: n => lineLabelText(n, axisStep, axisRange)
29008
+ };
29009
+ };
29010
+ const Grid = props => {
29011
+ return props.markings === "none" ? null : /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Coordinates.Cartesian, {
29012
+ xAxis: axisOptions(props, 0),
29013
+ yAxis: axisOptions(props, 1)
29014
+ }), /*#__PURE__*/React.createElement(AxisTicks, {
29015
+ range: props.range,
29016
+ tickStep: props.tickStep
29017
+ }), props.markings === "graph" && /*#__PURE__*/React.createElement(AxisArrows, null));
29018
+ };
28873
29019
 
28874
29020
  // Displays a stylized open eye: lint warnings are visible
28875
29021
  const VisibleIcon = () => /*#__PURE__*/React.createElement("svg", {
@@ -29576,6 +29722,10 @@ function interactiveGraphReducer(state, action) {
29576
29722
  return doMoveAll(state, action);
29577
29723
  case MOVE_POINT:
29578
29724
  return doMovePoint(state, action);
29725
+ case CHANGE_SNAP_STEP:
29726
+ return doChangeSnapStep(state, action);
29727
+ case CHANGE_RANGE:
29728
+ return doChangeRange(state, action);
29579
29729
  default:
29580
29730
  throw new UnreachableCaseError(action);
29581
29731
  }
@@ -29597,14 +29747,11 @@ function doMoveControlPoint(state, action) {
29597
29747
  update: tuple => setAtIndex({
29598
29748
  array: tuple,
29599
29749
  index: action.pointIndex,
29600
- newValue: snap({
29750
+ newValue: snap(snapStep, bound$1({
29601
29751
  snapStep,
29602
- point: bound$1({
29603
- snapStep,
29604
- range,
29605
- point: action.destination
29606
- })
29607
- })
29752
+ range,
29753
+ point: action.destination
29754
+ }))
29608
29755
  })
29609
29756
  });
29610
29757
  const coordsToCheck = newCoords[action.itemIndex];
@@ -29647,13 +29794,7 @@ function doMoveLine(state, action) {
29647
29794
  snapStep,
29648
29795
  range
29649
29796
  });
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
- })];
29797
+ const newLine = [snap(snapStep, vec.add(currentLine[0], change)), snap(snapStep, vec.add(currentLine[1], change))];
29657
29798
  const newCoords = setAtIndex({
29658
29799
  array: state.coords,
29659
29800
  index: action.itemIndex,
@@ -29683,10 +29824,7 @@ function doMoveAll(state, action) {
29683
29824
  snapStep,
29684
29825
  range
29685
29826
  });
29686
- const newCoords = state.coords.map(point => snap({
29687
- snapStep,
29688
- point: vec.add(point, change)
29689
- }));
29827
+ const newCoords = state.coords.map(point => snap(snapStep, vec.add(point, change)));
29690
29828
  return _extends({}, state, {
29691
29829
  hasBeenInteractedWith: true,
29692
29830
  coords: newCoords
@@ -29707,14 +29845,11 @@ function doMovePoint(state, action) {
29707
29845
  coords: setAtIndex({
29708
29846
  array: state.coords,
29709
29847
  index: action.index,
29710
- newValue: snap({
29848
+ newValue: snap(state.snapStep, bound$1({
29711
29849
  snapStep: state.snapStep,
29712
- point: bound$1({
29713
- snapStep: state.snapStep,
29714
- range: state.range,
29715
- point: action.destination
29716
- })
29717
- })
29850
+ range: state.range,
29851
+ point: action.destination
29852
+ }))
29718
29853
  })
29719
29854
  });
29720
29855
  }
@@ -29722,6 +29857,26 @@ function doMovePoint(state, action) {
29722
29857
  throw new Error("The movePoint action is only for point and polygon graphs");
29723
29858
  }
29724
29859
  }
29860
+ function doChangeSnapStep(state, action) {
29861
+ if (
29862
+ // Deep equality check since these are arrays
29863
+ _.isEqual(state.snapStep, action.snapStep)) {
29864
+ return state;
29865
+ }
29866
+ return _extends({}, state, {
29867
+ snapStep: action.snapStep
29868
+ });
29869
+ }
29870
+ function doChangeRange(state, action) {
29871
+ if (
29872
+ // Deep equality check since these are arrays
29873
+ _.isEqual(state.range, action.range)) {
29874
+ return state;
29875
+ }
29876
+ return _extends({}, state, {
29877
+ range: action.range
29878
+ });
29879
+ }
29725
29880
  const getChange = (coords, delta, constraintOpts) => {
29726
29881
  const [deltaX, deltaY] = delta;
29727
29882
  const maxMoves = coords.map(point => maxMove(_extends({}, constraintOpts, {
@@ -29738,15 +29893,6 @@ const getChange = (coords, delta, constraintOpts) => {
29738
29893
  const dy = clamp(deltaY, minYMove, maxYMove);
29739
29894
  return [dx, dy];
29740
29895
  };
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
29896
  // Returns the closest point to the given `point` that is within the graph
29751
29897
  // bounds given in `state`.
29752
29898
  function bound$1({
@@ -30098,6 +30244,18 @@ const MafsGraph = /*#__PURE__*/React.forwardRef((props, ref) => {
30098
30244
  }
30099
30245
  prevState.current = state;
30100
30246
  }, [props, state]);
30247
+
30248
+ // Destructuring first to keep useEffect from making excess calls
30249
+ const [xSnap, ySnap] = props.snapStep;
30250
+ useEffect(() => {
30251
+ dispatch(changeSnapStep([xSnap, ySnap]));
30252
+ }, [xSnap, ySnap]);
30253
+
30254
+ // Destructuring first to keep useEffect from making excess calls
30255
+ const [[xMinRange, xMaxRange], [yMinRange, yMaxRange]] = props.range;
30256
+ useEffect(() => {
30257
+ dispatch(changeRange([[xMinRange, xMaxRange], [yMinRange, yMaxRange]]));
30258
+ }, [xMinRange, xMaxRange, yMinRange, yMaxRange]);
30101
30259
  React.useImperativeHandle(ref, () => ({
30102
30260
  getUserInput: () => getGradableGraph(state, props.graph)
30103
30261
  }));
@@ -30132,7 +30290,13 @@ const MafsGraph = /*#__PURE__*/React.forwardRef((props, ref) => {
30132
30290
  zoom: false,
30133
30291
  width: width,
30134
30292
  height: height
30135
- }, /*#__PURE__*/React.createElement(SvgDefs, null), /*#__PURE__*/React.createElement(Grid, props), props.lockedFigures && /*#__PURE__*/React.createElement(GraphLockedLayer, {
30293
+ }, /*#__PURE__*/React.createElement(SvgDefs, null), /*#__PURE__*/React.createElement(Grid, {
30294
+ tickStep: props.step,
30295
+ gridStep: props.gridStep,
30296
+ range: props.range,
30297
+ containerSizeClass: props.containerSizeClass,
30298
+ markings: props.markings
30299
+ }), props.lockedFigures && /*#__PURE__*/React.createElement(GraphLockedLayer, {
30136
30300
  lockedFigures: props.lockedFigures
30137
30301
  }), renderGraph({
30138
30302
  state,
@@ -31911,9 +32075,9 @@ class InteractiveGraph extends React.Component {
31911
32075
  };
31912
32076
  }
31913
32077
  } 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();
32078
+ var _rubric$correct;
32079
+ let guess = Util.deepClone(userInput.coords);
32080
+ let correct = Util.deepClone((_rubric$correct = rubric.correct) == null ? void 0 : _rubric$correct.coords);
31917
32081
  guess = _.invoke(guess, "sort").sort();
31918
32082
  // @ts-expect-error - TS2345 - Argument of type '(readonly Coord[])[] | undefined' is not assignable to parameter of type 'Collection<any>'.
31919
32083
  correct = _.invoke(correct, "sort").sort();
@@ -42181,7 +42345,7 @@ const init = function init(options) {
42181
42345
 
42182
42346
  // This file is processed by a Rollup plugin (replace) to inject the production
42183
42347
  const libName = "@khanacademy/perseus";
42184
- const libVersion = "21.1.0";
42348
+ const libVersion = "21.2.1";
42185
42349
  addLibraryVersionToPerseusDebug(libName, libVersion);
42186
42350
 
42187
42351
  const apiVersion = {
@@ -43305,6 +43469,9 @@ const ItemExtras = [
43305
43469
  // A background image applied to various widgets.
43306
43470
 
43307
43471
  const PerseusExpressionAnswerFormConsidered = ["correct", "wrong", "ungraded"];
43472
+
43473
+ // 2D range: xMin, xMax, yMin, yMax
43474
+
43308
43475
  const plotterPlotTypes = ["bar", "line", "pic", "histogram", "dotplot"];
43309
43476
 
43310
43477
  /**