@khanacademy/perseus 22.0.2 → 22.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
@@ -32,8 +32,8 @@ var caretUp = require('@phosphor-icons/core/assets/regular/caret-up.svg');
32
32
  var debounce$1 = require('lodash.debounce');
33
33
  var mafs = require('mafs');
34
34
  var wonderBlocksIcon = require('@khanacademy/wonder-blocks-icon');
35
- var questionIcon = require('@phosphor-icons/core/regular/question.svg');
36
35
  var wonderBlocksTypography = require('@khanacademy/wonder-blocks-typography');
36
+ var questionIcon = require('@phosphor-icons/core/regular/question.svg');
37
37
  var createReactClass = require('create-react-class');
38
38
  var Switch = require('@khanacademy/wonder-blocks-switch');
39
39
  var Pill = require('@khanacademy/wonder-blocks-pill');
@@ -28761,6 +28761,93 @@ class Graph extends React__namespace.Component {
28761
28761
  }
28762
28762
  }
28763
28763
 
28764
+ const defaultGraphConfig = {
28765
+ range: [[0, 1], [0, 1]],
28766
+ snapStep: [1, 1],
28767
+ markings: "none",
28768
+ showTooltips: false,
28769
+ graphDimensionsInPixels: [1, 1],
28770
+ width: 0,
28771
+ height: 0,
28772
+ labels: []
28773
+ };
28774
+ const GraphConfigContext = /*#__PURE__*/React.createContext(defaultGraphConfig);
28775
+ function useGraphConfig() {
28776
+ return React__default["default"].useContext(GraphConfigContext);
28777
+ }
28778
+
28779
+ const matrixBuilder = mafs.vec.matrixBuilder;
28780
+ // When converting vectors, we don't need to translate because a vector
28781
+ // is like a pair of x and y distances, which are not bound to specific graph locations.
28782
+ function vectorsToPixels(vectors, graphState) {
28783
+ let translation = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [0, 0];
28784
+ const {
28785
+ range,
28786
+ width,
28787
+ height
28788
+ } = graphState;
28789
+ const [[xMin, xMax], [yMin, yMax]] = range;
28790
+ const transformToPx = matrixBuilder().translate(...translation).scale(width / (xMax - xMin), -height / (yMax - yMin)).get();
28791
+ return vectors.map(p => mafs.vec.transform(p, transformToPx));
28792
+ }
28793
+
28794
+ // When converting points, we translate so the pixel point ends up in the right location.
28795
+ // This is because points are specific locations on graphs, unlike vectors.
28796
+ function pointToPixel(point, graphState) {
28797
+ const [[xMin], [, yMax]] = graphState.range;
28798
+ const [a, b] = [-xMin, -yMax];
28799
+ return vectorsToPixels([point], graphState, [a, b])[0];
28800
+ }
28801
+ const useTransformVectorsToPixels = function () {
28802
+ const graphState = useGraphConfig();
28803
+ for (var _len = arguments.length, vectors = new Array(_len), _key = 0; _key < _len; _key++) {
28804
+ vectors[_key] = arguments[_key];
28805
+ }
28806
+ return vectorsToPixels(vectors, graphState);
28807
+ };
28808
+
28809
+ function AxisLabels() {
28810
+ const {
28811
+ range,
28812
+ labels,
28813
+ width,
28814
+ height
28815
+ } = useGraphConfig();
28816
+ const yAxisLabelLocation = [0, range[1][1]];
28817
+ const xAxisLabelLocation = [range[0][1], 0];
28818
+ const [xAxisLabelText, yAxisLabelText] = labels;
28819
+ const graphInfo = {
28820
+ range,
28821
+ width,
28822
+ height
28823
+ };
28824
+ const [x1, y1] = pointToPixel(xAxisLabelLocation, graphInfo);
28825
+ // The default location for the y-axis-label is at the maximum y point
28826
+ // This is just underneath the tick line for that point. The -24 moves the
28827
+ // label up one grid square, so it sits on top of the graph
28828
+ const [x2, y2] = mafs.vec.add(pointToPixel(yAxisLabelLocation, graphInfo), [0, -24]);
28829
+ const {
28830
+ TeX
28831
+ } = getDependencies();
28832
+ return /*#__PURE__*/React__default["default"].createElement(React__default["default"].Fragment, null, /*#__PURE__*/React__default["default"].createElement("span", {
28833
+ style: {
28834
+ position: "absolute",
28835
+ left: x1,
28836
+ top: y1,
28837
+ fontSize: "14px",
28838
+ transform: "translate(7px, -50%)"
28839
+ }
28840
+ }, /*#__PURE__*/React__default["default"].createElement(TeX, null, xAxisLabelText)), /*#__PURE__*/React__default["default"].createElement("span", {
28841
+ style: {
28842
+ position: "absolute",
28843
+ left: x2,
28844
+ top: y2,
28845
+ fontSize: "14px",
28846
+ transform: "translate(-50%, 0px)"
28847
+ }
28848
+ }, /*#__PURE__*/React__default["default"].createElement(TeX, null, yAxisLabelText)));
28849
+ }
28850
+
28764
28851
  // TODO(FEI-4010): Remove `Perseus` prefix for all types here
28765
28852
  // TODO(FEI-4011): Use types generated by https://github.com/jaredly/generate-perseus-flowtypes
28766
28853
 
@@ -28878,6 +28965,162 @@ const lockedFigureColors = {
28878
28965
  };
28879
28966
  const plotterPlotTypes = ["bar", "line", "pic", "histogram", "dotplot"];
28880
28967
 
28968
+ function pathBuilder() {
28969
+ return new PathBuilder();
28970
+ }
28971
+ class PathBuilder {
28972
+ path = [];
28973
+ scaleFactor = 1;
28974
+ build() {
28975
+ return this.path.map(scaleCommandBy(this.scaleFactor)).map(commandToString).join("");
28976
+ }
28977
+
28978
+ // add a move (M) command to the path
28979
+ move(x, y) {
28980
+ this.path.push({
28981
+ action: "M",
28982
+ coords: [x, y]
28983
+ });
28984
+ return this;
28985
+ }
28986
+
28987
+ // add a curve (C) command to the path
28988
+ curve(control1X, control1Y, control2X, control2Y, endX, endY) {
28989
+ this.path.push({
28990
+ action: "C",
28991
+ coords: [control1X, control1Y, control2X, control2Y, endX, endY]
28992
+ });
28993
+ return this;
28994
+ }
28995
+
28996
+ // Scale all coordinates of the path by a factor
28997
+ scale(factor) {
28998
+ this.scaleFactor *= factor;
28999
+ return this;
29000
+ }
29001
+ }
29002
+ function commandToString(command) {
29003
+ return `${command.action}${command.coords.join(" ")}`;
29004
+ }
29005
+ function scaleCommandBy(scaleFactor) {
29006
+ return command => ({
29007
+ ...command,
29008
+ coords: command.coords.map(c => c * scaleFactor)
29009
+ });
29010
+ }
29011
+
29012
+ // We use the pathBuilder here to scale up the SVG path coordinates used
29013
+ // elsewhere for arrowheads. We scale in the path itself instead of using a CSS
29014
+ // transform because a transform would also scale up the stroke weight, and we
29015
+ // don't want that.
29016
+ const arrowPath = pathBuilder().move(-3, 4).curve(-2.75, 2.5, 0, 0.25, 0.75, 0).curve(0, -0.25, -2.75, -2.5, -3, -4).scale(1.4).build();
29017
+ function Arrowhead(props) {
29018
+ const [point] = useTransformVectorsToPixels(props.tip);
29019
+ return /*#__PURE__*/React__namespace.createElement("g", {
29020
+ transform: `translate(${point[0]} ${point[1]}) rotate(${-props.angle})`
29021
+ }, /*#__PURE__*/React__namespace.createElement("g", {
29022
+ transform: "translate(-1.5)"
29023
+ }, /*#__PURE__*/React__namespace.createElement("path", {
29024
+ d: arrowPath,
29025
+ fill: "none",
29026
+ style: {
29027
+ stroke: props.color ?? "inherit"
29028
+ },
29029
+ strokeLinejoin: "round",
29030
+ strokeLinecap: "round"
29031
+ })));
29032
+ }
29033
+
29034
+ function SVGLine(props) {
29035
+ const {
29036
+ start,
29037
+ end,
29038
+ style,
29039
+ className,
29040
+ testId
29041
+ } = props;
29042
+ return /*#__PURE__*/React__namespace.createElement("line", {
29043
+ x1: start[0],
29044
+ y1: start[1],
29045
+ x2: end[0],
29046
+ y2: end[1],
29047
+ style: style,
29048
+ className: className,
29049
+ "data-testid": testId
29050
+ });
29051
+ }
29052
+
29053
+ function Vector(props) {
29054
+ const {
29055
+ tail,
29056
+ tip,
29057
+ color,
29058
+ style
29059
+ } = props;
29060
+ const [tailPx, tipPx] = useTransformVectorsToPixels(tail, tip);
29061
+ const direction = mafs.vec.sub(tip, tail);
29062
+ return /*#__PURE__*/React__namespace.createElement("g", {
29063
+ style: {
29064
+ stroke: color,
29065
+ strokeWidth: 2
29066
+ }
29067
+ }, /*#__PURE__*/React__namespace.createElement(SVGLine, {
29068
+ start: tailPx,
29069
+ end: tipPx,
29070
+ style: style
29071
+ }), /*#__PURE__*/React__namespace.createElement(Arrowhead, {
29072
+ tip: tip,
29073
+ angle: angleDegrees(direction)
29074
+ }));
29075
+ }
29076
+ function angleDegrees(_ref) {
29077
+ let [x, y] = _ref;
29078
+ return Math.atan2(y, x) * 180 / Math.PI;
29079
+ }
29080
+
29081
+ /**
29082
+ * Given a ray and a rectangular box, find the point where the ray intersects
29083
+ * the edge of the box. Assumes the `initialPoint` is inside the box.
29084
+ * @param initialPoint - The starting point of the ray.
29085
+ * @param throughPoint - A point that the ray passes through. Must be different from initialPoint.
29086
+ * @param box - The box with which to intersect the ray, in the form [[xMin, xMax], [yMin, yMax]]
29087
+ */
29088
+ const getIntersectionOfRayWithBox = (initialPoint, throughPoint, box) => {
29089
+ const [[xMin, xMax], [yMin, yMax]] = box;
29090
+ const [aX, aY] = throughPoint;
29091
+ const [bX, bY] = initialPoint;
29092
+ const yDiff = bY - aY;
29093
+ const xDiff = bX - aX;
29094
+ const slope = yDiff / xDiff;
29095
+ const yAtXMin = slope * (xMin - aX) + aY;
29096
+ const yAtXMax = slope * (xMax - aX) + aY;
29097
+ const xAtYMin = (yMin - aY) / slope + aX;
29098
+ const xAtYMax = (yMax - aY) / slope + aX;
29099
+
29100
+ // clock analogy to describe quadrants
29101
+ switch (true) {
29102
+ // 12 o'clock to 2:59
29103
+ case yDiff > 0 && xDiff >= 0:
29104
+ return xAtYMax > xMax ? [xMax, yAtXMax] : [xAtYMax, yMax];
29105
+ // 3 o'clock to 5:59
29106
+ case yDiff <= 0 && xDiff > 0:
29107
+ // xAtYMin evaluates to -Infinity here, so we use absolute value
29108
+ return Math.abs(xAtYMin) > xMax ? [xMax, yAtXMax] : [xAtYMin, yMin];
29109
+ // 9 o'clock to 11:59
29110
+ case yDiff >= 0 && xDiff < 0:
29111
+ return xAtYMax < xMin ? [xMin, yAtXMin] : [xAtYMax, yMax];
29112
+ // 6 o'clock to 8:59
29113
+ case yDiff < 0 && xDiff <= 0:
29114
+ return xAtYMin < xMin ? [xMin, yAtXMin] : [xAtYMin, yMin];
29115
+ default:
29116
+ return [xMax, yAtXMax];
29117
+ }
29118
+ };
29119
+ const getLines = points => points.map((point, i) => {
29120
+ const next = points[(i + 1) % points.length];
29121
+ return [point, next];
29122
+ });
29123
+
28881
29124
  const LockedLine = props => {
28882
29125
  const {
28883
29126
  color,
@@ -28885,11 +29128,27 @@ const LockedLine = props => {
28885
29128
  kind,
28886
29129
  points,
28887
29130
  showStartPoint,
28888
- showEndPoint
29131
+ showEndPoint,
29132
+ range
28889
29133
  } = props;
28890
29134
  const [point1, point2] = points;
28891
29135
  let line;
28892
- if (kind === "line" || kind === "segment") {
29136
+ if (kind === "ray") {
29137
+ // Rays extend to the end of the graph in one direction.
29138
+ const endExtend = getIntersectionOfRayWithBox(point2.coord, point1.coord, range);
29139
+ line = /*#__PURE__*/React__namespace.createElement(Vector, {
29140
+ tail: point1.coord,
29141
+ tip: endExtend,
29142
+ color: lockedFigureColors[color],
29143
+ style: {
29144
+ strokeDasharray: lineStyle === "dashed" ?
29145
+ // TODO(lems-1930): Uncomment this line when the
29146
+ // dashed style is updated in Mafs.
29147
+ // ? "var(--mafs-line-stroke-dash-style)"
29148
+ "4, 3" : undefined
29149
+ }
29150
+ });
29151
+ } else {
28893
29152
  const LineType = kind === "segment" ? mafs.Line.Segment : mafs.Line.ThroughPoints;
28894
29153
  line = /*#__PURE__*/React__namespace.createElement(LineType, {
28895
29154
  point1: point1.coord,
@@ -28897,12 +29156,9 @@ const LockedLine = props => {
28897
29156
  color: lockedFigureColors[color],
28898
29157
  style: lineStyle
28899
29158
  });
28900
- } else {
28901
- // TODO(LEMS-1928): Implement rays
28902
- return null;
28903
29159
  }
28904
29160
  return /*#__PURE__*/React__namespace.createElement("g", {
28905
- className: "locked-line"
29161
+ className: kind === "ray" ? "locked-ray" : "locked-line"
28906
29162
  }, line, showStartPoint && /*#__PURE__*/React__namespace.createElement(mafs.Point, {
28907
29163
  x: point1.coord[0],
28908
29164
  y: point1.coord[1],
@@ -28951,14 +29207,16 @@ const GraphLockedLayer = props => {
28951
29207
  lockedFigures
28952
29208
  } = props;
28953
29209
  return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, lockedFigures.map((figure, index) => {
28954
- let Figure;
28955
29210
  switch (figure.type) {
28956
29211
  case "point":
28957
- Figure = LockedPoint;
28958
- break;
29212
+ return /*#__PURE__*/React__namespace.createElement(LockedPoint, _extends({
29213
+ key: `point-${index}`
29214
+ }, figure));
28959
29215
  case "line":
28960
- Figure = LockedLine;
28961
- break;
29216
+ return /*#__PURE__*/React__namespace.createElement(LockedLine, _extends({
29217
+ key: `line-${index}`,
29218
+ range: props.range
29219
+ }, figure));
28962
29220
  default:
28963
29221
  /**
28964
29222
  * Devlopment-time future-proofing: This should
@@ -28968,9 +29226,6 @@ const GraphLockedLayer = props => {
28968
29226
  */
28969
29227
  throw new wonderStuffCore.UnreachableCaseError(figure);
28970
29228
  }
28971
- return /*#__PURE__*/React__namespace.createElement(Figure, _extends({
28972
- key: `${figure.type}-${index}`
28973
- }, figure));
28974
29229
  }));
28975
29230
  };
28976
29231
 
@@ -29052,173 +29307,6 @@ function snap(snapStep, point) {
29052
29307
  return [Math.round(requestedX / snapX) * snapX, Math.round(requestedY / snapY) * snapY];
29053
29308
  }
29054
29309
 
29055
- const useTransform = function () {
29056
- const {
29057
- viewTransform,
29058
- userTransform
29059
- } = mafs.useTransformContext();
29060
- const transformToPx = mafs.vec.matrixMult(viewTransform, userTransform);
29061
- for (var _len = arguments.length, points = new Array(_len), _key = 0; _key < _len; _key++) {
29062
- points[_key] = arguments[_key];
29063
- }
29064
- const transformedPoints = points.map(point => mafs.vec.transform(point, transformToPx));
29065
- return transformedPoints;
29066
- };
29067
-
29068
- /**
29069
- * Given two points, find the tips that extends through the points to the edge of the range.
29070
- * @param collinearPoint - The point that the line passes through. Needed to establish slope.
29071
- * @param extendFrom - The point that the line extends from to the edge of the graph.
29072
- */
29073
- const getRayIntersectionCoords = (collinearPoint, extendFrom, range) => {
29074
- // edges of the graph
29075
- const [[xMin, xMax], [yMin, yMax]] = range;
29076
- const [aX, aY] = collinearPoint;
29077
- const [bX, bY] = extendFrom;
29078
- const yDiff = bY - aY;
29079
- const xDiff = bX - aX;
29080
- const slope = yDiff / xDiff;
29081
- const yAtXMin = slope * (xMin - aX) + aY;
29082
- const yAtXMax = slope * (xMax - aX) + aY;
29083
- const xAtYMin = (yMin - aY) / slope + aX;
29084
- const xAtYMax = (yMax - aY) / slope + aX;
29085
-
29086
- // clock analogy to describe quadrants
29087
- switch (true) {
29088
- // 12 o'clock to 2:59
29089
- case yDiff > 0 && xDiff >= 0:
29090
- return xAtYMax > xMax ? [xMax, yAtXMax] : [xAtYMax, yMax];
29091
- // 3 o'clock to 5:59
29092
- case yDiff <= 0 && xDiff > 0:
29093
- // xAtYMin evaluates to -Infinity here, so we use absolute value
29094
- return Math.abs(xAtYMin) > xMax ? [xMax, yAtXMax] : [xAtYMin, yMin];
29095
- // 9 o'clock to 11:59
29096
- case yDiff >= 0 && xDiff < 0:
29097
- return xAtYMax < xMin ? [xMin, yAtXMin] : [xAtYMax, yMax];
29098
- // 6 o'clock to 8:59
29099
- case yDiff < 0 && xDiff <= 0:
29100
- return xAtYMin < xMin ? [xMin, yAtXMin] : [xAtYMin, yMin];
29101
- default:
29102
- return [xMax, yAtXMax];
29103
- }
29104
- };
29105
- const getLines = points => points.map((point, i) => {
29106
- const next = points[(i + 1) % points.length];
29107
- return [point, next];
29108
- });
29109
-
29110
- function SVGLine(props) {
29111
- const {
29112
- start,
29113
- end,
29114
- style,
29115
- className,
29116
- testId
29117
- } = props;
29118
- return /*#__PURE__*/React__namespace.createElement("line", {
29119
- x1: start[0],
29120
- y1: start[1],
29121
- x2: end[0],
29122
- y2: end[1],
29123
- style: style,
29124
- className: `${className}`,
29125
- "data-testid": testId
29126
- });
29127
- }
29128
-
29129
- function pathBuilder() {
29130
- return new PathBuilder();
29131
- }
29132
- class PathBuilder {
29133
- path = [];
29134
- scaleFactor = 1;
29135
- build() {
29136
- return this.path.map(scaleCommandBy(this.scaleFactor)).map(commandToString).join("");
29137
- }
29138
-
29139
- // add a move (M) command to the path
29140
- move(x, y) {
29141
- this.path.push({
29142
- action: "M",
29143
- coords: [x, y]
29144
- });
29145
- return this;
29146
- }
29147
-
29148
- // add a curve (C) command to the path
29149
- curve(control1X, control1Y, control2X, control2Y, endX, endY) {
29150
- this.path.push({
29151
- action: "C",
29152
- coords: [control1X, control1Y, control2X, control2Y, endX, endY]
29153
- });
29154
- return this;
29155
- }
29156
-
29157
- // Scale all coordinates of the path by a factor
29158
- scale(factor) {
29159
- this.scaleFactor *= factor;
29160
- return this;
29161
- }
29162
- }
29163
- function commandToString(command) {
29164
- return `${command.action}${command.coords.join(" ")}`;
29165
- }
29166
- function scaleCommandBy(scaleFactor) {
29167
- return command => ({
29168
- ...command,
29169
- coords: command.coords.map(c => c * scaleFactor)
29170
- });
29171
- }
29172
-
29173
- // We use the pathBuilder here to scale up the SVG path coordinates used
29174
- // elsewhere for arrowheads. We scale in the path itself instead of using a CSS
29175
- // transform because a transform would also scale up the stroke weight, and we
29176
- // don't want that.
29177
- const arrowPath = pathBuilder().move(-3, 4).curve(-2.75, 2.5, 0, 0.25, 0.75, 0).curve(0, -0.25, -2.75, -2.5, -3, -4).scale(1.4).build();
29178
- function Arrowhead(props) {
29179
- const [point] = useTransform([props.x, props.y]);
29180
- return /*#__PURE__*/React__namespace.createElement("g", {
29181
- transform: `translate(${point[0]} ${point[1]}) rotate(${-props.angle})`
29182
- }, /*#__PURE__*/React__namespace.createElement("g", {
29183
- transform: "translate(-1.5)"
29184
- }, /*#__PURE__*/React__namespace.createElement("path", {
29185
- d: arrowPath,
29186
- fill: "none",
29187
- style: {
29188
- stroke: props.color ?? "inherit"
29189
- },
29190
- strokeLinejoin: "round",
29191
- strokeLinecap: "round"
29192
- })));
29193
- }
29194
-
29195
- function Vector(props) {
29196
- const {
29197
- tail,
29198
- tip,
29199
- color
29200
- } = props;
29201
- const [tailPx, tipPx] = useTransform(tail, tip);
29202
- const direction = mafs.vec.sub(tip, tail);
29203
- return /*#__PURE__*/React__namespace.createElement("g", {
29204
- style: {
29205
- stroke: color,
29206
- strokeWidth: 2
29207
- }
29208
- }, /*#__PURE__*/React__namespace.createElement(SVGLine, {
29209
- start: tailPx,
29210
- end: tipPx
29211
- }), /*#__PURE__*/React__namespace.createElement(Arrowhead, {
29212
- x: tip[0],
29213
- y: tip[1],
29214
- angle: angleDegrees(direction)
29215
- }));
29216
- }
29217
- function angleDegrees(_ref) {
29218
- let [x, y] = _ref;
29219
- return Math.atan2(y, x) * 180 / Math.PI;
29220
- }
29221
-
29222
29310
  const defaultStroke = "var(--movable-line-stroke-color)";
29223
29311
  const MovableLine = props => {
29224
29312
  const {
@@ -29229,12 +29317,16 @@ const MovableLine = props => {
29229
29317
  stroke = defaultStroke
29230
29318
  } = props;
29231
29319
  const midpoint = mafs.vec.midpoint(start, end);
29232
- const [startPtPx, endPtPx] = useTransform(start, end);
29320
+ const [startPtPx, endPtPx] = useTransformVectorsToPixels(start, end);
29321
+ const {
29322
+ graphDimensionsInPixels
29323
+ } = useGraphConfig();
29233
29324
  let startExtend = undefined;
29234
29325
  let endExtend = undefined;
29235
29326
  if (extend) {
29236
- startExtend = extend.start ? getRayIntersectionCoords(end, start, extend.range) : undefined;
29237
- endExtend = extend.end ? getRayIntersectionCoords(start, end, extend.range) : undefined;
29327
+ const trimmedRange = trimRange(extend.range, graphDimensionsInPixels);
29328
+ startExtend = extend.start ? getIntersectionOfRayWithBox(start, end, trimmedRange) : undefined;
29329
+ endExtend = extend.end ? getIntersectionOfRayWithBox(end, start, trimmedRange) : undefined;
29238
29330
  }
29239
29331
  const line = React.useRef(null);
29240
29332
  const {
@@ -29291,16 +29383,24 @@ const MovableLine = props => {
29291
29383
  color: stroke
29292
29384
  }));
29293
29385
  };
29294
-
29295
- const defaultGraphConfig = {
29296
- range: [[0, 1], [0, 1]],
29297
- snapStep: [1, 1],
29298
- markings: "none",
29299
- showTooltips: false
29300
- };
29301
- const GraphConfigContext = /*#__PURE__*/React.createContext(defaultGraphConfig);
29302
- function useGraphConfig() {
29303
- return React__default["default"].useContext(GraphConfigContext);
29386
+ function trimRange(range, graphDimensionsInPixels) {
29387
+ const pixelsToTrim = 4;
29388
+ const [xRange, yRange] = range;
29389
+ const [pixelsWide, pixelsTall] = graphDimensionsInPixels;
29390
+ const graphUnitsPerPixelX = size(xRange) / pixelsWide;
29391
+ const graphUnitsPerPixelY = size(yRange) / pixelsTall;
29392
+ const graphUnitsToTrimX = pixelsToTrim * graphUnitsPerPixelX;
29393
+ const graphUnitsToTrimY = pixelsToTrim * graphUnitsPerPixelY;
29394
+ return [trim(xRange, graphUnitsToTrimX), trim(yRange, graphUnitsToTrimY)];
29395
+ }
29396
+ function trim(interval, amount) {
29397
+ if (size(interval) < amount * 2) {
29398
+ return [0, 0];
29399
+ }
29400
+ return [interval[0] + amount, interval[1] - amount];
29401
+ }
29402
+ function size(interval) {
29403
+ return interval[1] - interval[0];
29304
29404
  }
29305
29405
 
29306
29406
  // The hitbox size of 48px by 48px is preserved from the legacy interactive
@@ -29335,13 +29435,13 @@ const StyledMovablePoint = props => {
29335
29435
  constrain: p => snap(snapStep, p)
29336
29436
  });
29337
29437
  const pointClasses = `movable-point ${dragging ? "movable-point--dragging" : ""}`;
29338
- const [[x, y]] = useTransform(point);
29438
+ const [[x, y]] = useTransformVectorsToPixels(point);
29339
29439
  const [xMin, xMax] = range[0];
29340
29440
  const [yMin, yMax] = range[1];
29341
- const [[verticalStartX]] = useTransform([xMin, 0]);
29342
- const [[verticalEndX]] = useTransform([xMax, 0]);
29343
- const [[_, horizontalStartY]] = useTransform([0, yMin]);
29344
- const [[__, horizontalEndY]] = useTransform([0, yMax]);
29441
+ const [[verticalStartX]] = useTransformVectorsToPixels([xMin, 0]);
29442
+ const [[verticalEndX]] = useTransformVectorsToPixels([xMax, 0]);
29443
+ const [[_, horizontalStartY]] = useTransformVectorsToPixels([0, yMin]);
29444
+ const [[__, horizontalEndY]] = useTransformVectorsToPixels([0, yMax]);
29345
29445
  const showHairlines = dragging && markings !== "none";
29346
29446
  const hairlines = /*#__PURE__*/React__namespace.createElement("g", null, /*#__PURE__*/React__namespace.createElement("line", {
29347
29447
  x1: verticalStartX,
@@ -29669,7 +29769,7 @@ const isRightAngle = angle => Math.abs(angle - Math.PI / 2) < 0.01;
29669
29769
  */
29670
29770
  const shouldDrawArcOutside = (midpoint, vertex, range, polygonLines) => {
29671
29771
  // Create a ray from the midpoint (inside angle) to the edge of the range
29672
- const rangeIntersectionPoint = getRayIntersectionCoords(midpoint, vertex, range);
29772
+ const rangeIntersectionPoint = getIntersectionOfRayWithBox(vertex, midpoint, range);
29673
29773
  let lineIntersectionCount = 0;
29674
29774
  polygonLines.forEach(line => linesIntersect([vertex, rangeIntersectionPoint], line) && lineIntersectionCount++);
29675
29775
 
@@ -29794,23 +29894,19 @@ function AxisArrows() {
29794
29894
  const axisColor = "var(--mafs-fg)";
29795
29895
  return /*#__PURE__*/React__default["default"].createElement(React__default["default"].Fragment, null, /*#__PURE__*/React__default["default"].createElement(Arrowhead, {
29796
29896
  color: axisColor,
29797
- x: xMin,
29798
- y: 0,
29897
+ tip: [xMin, 0],
29799
29898
  angle: 180
29800
29899
  }), /*#__PURE__*/React__default["default"].createElement(Arrowhead, {
29801
29900
  color: axisColor,
29802
- x: xMax,
29803
- y: 0,
29901
+ tip: [xMax, 0],
29804
29902
  angle: 0
29805
29903
  }), /*#__PURE__*/React__default["default"].createElement(Arrowhead, {
29806
29904
  color: axisColor,
29807
- x: 0,
29808
- y: yMin,
29905
+ tip: [0, yMin],
29809
29906
  angle: 270
29810
29907
  }), /*#__PURE__*/React__default["default"].createElement(Arrowhead, {
29811
29908
  color: axisColor,
29812
- x: 0,
29813
- y: yMax,
29909
+ tip: [0, yMax],
29814
29910
  angle: 90
29815
29911
  }));
29816
29912
  }
@@ -29836,7 +29932,7 @@ const YGridTick = _ref => {
29836
29932
  tickStep
29837
29933
  } = _ref;
29838
29934
  const pointOnAxis = [0, y];
29839
- const [[xPosition, yPosition]] = useTransform(pointOnAxis);
29935
+ const [[xPosition, yPosition]] = useTransformVectorsToPixels(pointOnAxis);
29840
29936
  return /*#__PURE__*/React__namespace.createElement("g", {
29841
29937
  className: "y-axis-ticks"
29842
29938
  }, /*#__PURE__*/React__namespace.createElement("line", {
@@ -29860,7 +29956,7 @@ const XGridTick = _ref2 => {
29860
29956
  tickStep
29861
29957
  } = _ref2;
29862
29958
  const pointOnAxis = [x, 0];
29863
- const [[xPosition, yPosition]] = useTransform(pointOnAxis);
29959
+ const [[xPosition, yPosition]] = useTransformVectorsToPixels(pointOnAxis);
29864
29960
  return /*#__PURE__*/React__namespace.createElement("g", {
29865
29961
  className: "x-axis-ticks"
29866
29962
  }, /*#__PURE__*/React__namespace.createElement("line", {
@@ -30082,96 +30178,26 @@ const styles$k = aphrodite.StyleSheet.create({
30082
30178
  }
30083
30179
  });
30084
30180
 
30085
- const colors$1 = {
30086
- grayLight: "#aaa",
30087
- basicBorderColor: "#ccc",
30088
- white: "#fff"
30089
- };
30090
- const triangleBeforeAfter = {
30091
- borderBottom: "9px solid transparent",
30092
- borderTop: "9px solid transparent",
30093
- content: '" "',
30094
- height: "0",
30095
- position: "absolute",
30096
- top: "0",
30097
- width: "0"
30181
+ const InfoTip$1 = props => {
30182
+ return /*#__PURE__*/React__namespace.createElement(Tooltip__default["default"], {
30183
+ content: /*#__PURE__*/React__namespace.createElement(wonderBlocksTypography.Body, {
30184
+ style: styles$j.body
30185
+ }, props.children),
30186
+ placement: "right"
30187
+ }, /*#__PURE__*/React__namespace.createElement(wonderBlocksIcon.PhosphorIcon, {
30188
+ size: "small",
30189
+ icon: questionIcon__default["default"],
30190
+ style: styles$j.spacingLeft
30191
+ }));
30098
30192
  };
30099
30193
  const styles$j = aphrodite.StyleSheet.create({
30100
- infoTip: {
30101
- display: "inline-block",
30102
- marginLeft: "5px",
30103
- position: "relative"
30194
+ spacingLeft: {
30195
+ marginInlineStart: wonderBlocksTokens.spacing.xxxSmall_4
30104
30196
  },
30105
- infoTipContainer: {
30106
- position: "absolute",
30107
- top: "-12px",
30108
- left: "22px",
30109
- zIndex: 1000
30110
- },
30111
- infoTipTriangle: {
30112
- height: "10px",
30113
- left: "0",
30114
- position: "absolute",
30115
- top: "8px",
30116
- width: "0",
30117
- zIndex: 1,
30118
- ":before": {
30119
- ...triangleBeforeAfter,
30120
- borderRight: "9px solid #bbb",
30121
- right: "0"
30122
- },
30123
- ":after": {
30124
- ...triangleBeforeAfter,
30125
- borderRight: `9px solid ${colors$1.white}`,
30126
- right: "-1px"
30127
- }
30128
- },
30129
- verticalShadow: {
30130
- border: `1px solid ${colors$1.basicBorderColor}`,
30131
- boxShadow: `0 1px 3px ${colors$1.basicBorderColor}`,
30132
- borderBottom: `1px solid ${colors$1.grayLight}`
30133
- },
30134
- infoTipContentContainer: {
30135
- background: colors$1.white,
30136
- padding: "5px 10px",
30137
- width: "240px"
30197
+ body: {
30198
+ padding: wonderBlocksTokens.spacing.medium_16
30138
30199
  }
30139
30200
  });
30140
- class InfoTip$1 extends React__namespace.Component {
30141
- state = {
30142
- hover: false
30143
- };
30144
- handleMouseEnter = () => {
30145
- this.setState({
30146
- hover: true
30147
- });
30148
- };
30149
- handleMouseLeave = () => {
30150
- this.setState({
30151
- hover: false
30152
- });
30153
- };
30154
- render() {
30155
- return /*#__PURE__*/React__namespace.createElement("div", {
30156
- className: aphrodite.css(styles$j.infoTip)
30157
- }, /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
30158
- onMouseEnter: this.handleMouseEnter,
30159
- onMouseLeave: this.handleMouseLeave
30160
- }, /*#__PURE__*/React__namespace.createElement(wonderBlocksIcon.PhosphorIcon, {
30161
- size: "small",
30162
- icon: questionIcon__default["default"]
30163
- })), /*#__PURE__*/React__namespace.createElement("div", {
30164
- className: aphrodite.css(styles$j.infoTipContainer),
30165
- style: {
30166
- display: this.state.hover ? "block" : "none"
30167
- }
30168
- }, /*#__PURE__*/React__namespace.createElement("div", {
30169
- className: aphrodite.css(styles$j.infoTipTriangle)
30170
- }), /*#__PURE__*/React__namespace.createElement("div", {
30171
- className: aphrodite.css(styles$j.verticalShadow, styles$j.infoTipContentContainer)
30172
- }, this.props.children)));
30173
- }
30174
- }
30175
30201
 
30176
30202
  /**
30177
30203
  * A wrapper around info-tip/info-tip.jsx that can be rendered on the
@@ -31028,7 +31054,7 @@ const getDefaultPoints = _ref => {
31028
31054
  function getGradableGraph(state, initialGraph) {
31029
31055
  if (!state.hasBeenInteractedWith) {
31030
31056
  return {
31031
- type: initialGraph.type
31057
+ ...initialGraph
31032
31058
  };
31033
31059
  }
31034
31060
  if (initialGraph.type === "linear-system" && state.type === "linear-system") {
@@ -31195,7 +31221,8 @@ const StatefulMafsGraph = /*#__PURE__*/React__namespace.forwardRef((props, ref)
31195
31221
  const MafsGraph = props => {
31196
31222
  const {
31197
31223
  state,
31198
- dispatch
31224
+ dispatch,
31225
+ labels
31199
31226
  } = props;
31200
31227
  const [width, height] = props.box;
31201
31228
  const prevState = React.useRef(state);
@@ -31224,7 +31251,11 @@ const MafsGraph = props => {
31224
31251
  range: state.range,
31225
31252
  snapStep: state.snapStep,
31226
31253
  markings: props.markings,
31227
- showTooltips: !!props.showTooltips
31254
+ showTooltips: !!props.showTooltips,
31255
+ graphDimensionsInPixels: props.box,
31256
+ width,
31257
+ height,
31258
+ labels
31228
31259
  }
31229
31260
  }, /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
31230
31261
  style: {
@@ -31243,7 +31274,7 @@ const MafsGraph = props => {
31243
31274
  bottom: 0,
31244
31275
  left: 0
31245
31276
  }
31246
- }, /*#__PURE__*/React__namespace.createElement(mafs.Mafs, {
31277
+ }, props.markings === "graph" && /*#__PURE__*/React__namespace.createElement(AxisLabels, null), /*#__PURE__*/React__namespace.createElement(mafs.Mafs, {
31247
31278
  preserveAspectRatio: false,
31248
31279
  viewBox: {
31249
31280
  x: props.range[0],
@@ -31261,7 +31292,8 @@ const MafsGraph = props => {
31261
31292
  containerSizeClass: props.containerSizeClass,
31262
31293
  markings: props.markings
31263
31294
  }), props.lockedFigures && /*#__PURE__*/React__namespace.createElement(GraphLockedLayer, {
31264
- lockedFigures: props.lockedFigures
31295
+ lockedFigures: props.lockedFigures,
31296
+ range: props.range
31265
31297
  }), renderGraph({
31266
31298
  state,
31267
31299
  dispatch
@@ -32470,8 +32502,12 @@ class LegacyInteractiveGraph extends React__namespace.Component {
32470
32502
  graph: graph
32471
32503
  });
32472
32504
  };
32473
- getUserInput = () => InteractiveGraph.getUserInputFromProps(this.props);
32474
- simpleValidate = rubric => InteractiveGraph.validate(this.getUserInput(), rubric, this);
32505
+ getUserInput() {
32506
+ return InteractiveGraph.getUserInputFromProps(this.props);
32507
+ }
32508
+ simpleValidate(rubric) {
32509
+ return InteractiveGraph.validate(this.getUserInput(), rubric, this);
32510
+ }
32475
32511
  focus = $__default["default"].noop;
32476
32512
  render() {
32477
32513
  const box = getInteractiveBoxFromSizeClass(this.props.containerSizeClass);
@@ -32550,7 +32586,9 @@ class InteractiveGraph extends React__namespace.Component {
32550
32586
  }
32551
32587
  throw new PerseusError("Cannot getUserInput from a graph that has never rendered", Errors.NotAllowed);
32552
32588
  };
32553
- simpleValidate = rubric => InteractiveGraph.validate(this.getUserInput?.(), rubric, this);
32589
+ simpleValidate(rubric) {
32590
+ return InteractiveGraph.validate(this.getUserInput?.(), rubric, this);
32591
+ }
32554
32592
  render() {
32555
32593
  // Mafs shim
32556
32594
  if (this.props.apiOptions?.flags?.["mafs"]?.[this.props.graph.type]) {
@@ -43351,7 +43389,7 @@ const init = function (options) {
43351
43389
 
43352
43390
  // This file is processed by a Rollup plugin (replace) to inject the production
43353
43391
  const libName = "@khanacademy/perseus";
43354
- const libVersion = "22.0.2";
43392
+ const libVersion = "22.2.0";
43355
43393
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
43356
43394
 
43357
43395
  const apiVersion = {