@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/es/index.js CHANGED
@@ -29,10 +29,10 @@ import { OptionItem, SingleSelect, MultiSelect } from '@khanacademy/wonder-block
29
29
  import caretDown from '@phosphor-icons/core/assets/regular/caret-down.svg';
30
30
  import caretUp from '@phosphor-icons/core/assets/regular/caret-up.svg';
31
31
  import debounce$2 from 'lodash.debounce';
32
- import { Line as Line$3, Point as Point$2, useTransformContext, vec, useMovable, Text as Text$1, Polygon, Coordinates, Mafs } from 'mafs';
32
+ import { vec, Line as Line$3, Point as Point$2, useMovable, Text as Text$1, Polygon, Coordinates, Mafs } from 'mafs';
33
33
  import { PhosphorIcon } from '@khanacademy/wonder-blocks-icon';
34
+ import { Body, LabelSmall, LabelMedium } from '@khanacademy/wonder-blocks-typography';
34
35
  import questionIcon from '@phosphor-icons/core/regular/question.svg';
35
- import { LabelSmall, LabelMedium } from '@khanacademy/wonder-blocks-typography';
36
36
  import createReactClass from 'create-react-class';
37
37
  import Switch from '@khanacademy/wonder-blocks-switch';
38
38
  import Pill from '@khanacademy/wonder-blocks-pill';
@@ -28477,6 +28477,89 @@ Graph.defaultProps = {
28477
28477
  isMobile: false
28478
28478
  };
28479
28479
 
28480
+ const defaultGraphConfig = {
28481
+ range: [[0, 1], [0, 1]],
28482
+ snapStep: [1, 1],
28483
+ markings: "none",
28484
+ showTooltips: false,
28485
+ graphDimensionsInPixels: [1, 1],
28486
+ width: 0,
28487
+ height: 0,
28488
+ labels: []
28489
+ };
28490
+ const GraphConfigContext = /*#__PURE__*/createContext(defaultGraphConfig);
28491
+ function useGraphConfig() {
28492
+ return React__default.useContext(GraphConfigContext);
28493
+ }
28494
+
28495
+ const matrixBuilder = vec.matrixBuilder;
28496
+ // When converting vectors, we don't need to translate because a vector
28497
+ // is like a pair of x and y distances, which are not bound to specific graph locations.
28498
+ function vectorsToPixels(vectors, graphState, translation = [0, 0]) {
28499
+ const {
28500
+ range,
28501
+ width,
28502
+ height
28503
+ } = graphState;
28504
+ const [[xMin, xMax], [yMin, yMax]] = range;
28505
+ const transformToPx = matrixBuilder().translate(...translation).scale(width / (xMax - xMin), -height / (yMax - yMin)).get();
28506
+ return vectors.map(p => vec.transform(p, transformToPx));
28507
+ }
28508
+
28509
+ // When converting points, we translate so the pixel point ends up in the right location.
28510
+ // This is because points are specific locations on graphs, unlike vectors.
28511
+ function pointToPixel(point, graphState) {
28512
+ const [[xMin], [, yMax]] = graphState.range;
28513
+ const [a, b] = [-xMin, -yMax];
28514
+ return vectorsToPixels([point], graphState, [a, b])[0];
28515
+ }
28516
+ const useTransformVectorsToPixels = (...vectors) => {
28517
+ const graphState = useGraphConfig();
28518
+ return vectorsToPixels(vectors, graphState);
28519
+ };
28520
+
28521
+ function AxisLabels() {
28522
+ const {
28523
+ range,
28524
+ labels,
28525
+ width,
28526
+ height
28527
+ } = useGraphConfig();
28528
+ const yAxisLabelLocation = [0, range[1][1]];
28529
+ const xAxisLabelLocation = [range[0][1], 0];
28530
+ const [xAxisLabelText, yAxisLabelText] = labels;
28531
+ const graphInfo = {
28532
+ range,
28533
+ width,
28534
+ height
28535
+ };
28536
+ const [x1, y1] = pointToPixel(xAxisLabelLocation, graphInfo);
28537
+ // The default location for the y-axis-label is at the maximum y point
28538
+ // This is just underneath the tick line for that point. The -24 moves the
28539
+ // label up one grid square, so it sits on top of the graph
28540
+ const [x2, y2] = vec.add(pointToPixel(yAxisLabelLocation, graphInfo), [0, -24]);
28541
+ const {
28542
+ TeX
28543
+ } = getDependencies();
28544
+ return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement("span", {
28545
+ style: {
28546
+ position: "absolute",
28547
+ left: x1,
28548
+ top: y1,
28549
+ fontSize: "14px",
28550
+ transform: "translate(7px, -50%)"
28551
+ }
28552
+ }, /*#__PURE__*/React__default.createElement(TeX, null, xAxisLabelText)), /*#__PURE__*/React__default.createElement("span", {
28553
+ style: {
28554
+ position: "absolute",
28555
+ left: x2,
28556
+ top: y2,
28557
+ fontSize: "14px",
28558
+ transform: "translate(-50%, 0px)"
28559
+ }
28560
+ }, /*#__PURE__*/React__default.createElement(TeX, null, yAxisLabelText)));
28561
+ }
28562
+
28480
28563
  // TODO(FEI-4010): Remove `Perseus` prefix for all types here
28481
28564
  // TODO(FEI-4011): Use types generated by https://github.com/jaredly/generate-perseus-flowtypes
28482
28565
 
@@ -28594,6 +28677,163 @@ const lockedFigureColors = {
28594
28677
  };
28595
28678
  const plotterPlotTypes = ["bar", "line", "pic", "histogram", "dotplot"];
28596
28679
 
28680
+ function pathBuilder() {
28681
+ return new PathBuilder();
28682
+ }
28683
+ class PathBuilder {
28684
+ constructor() {
28685
+ this.path = [];
28686
+ this.scaleFactor = 1;
28687
+ }
28688
+ build() {
28689
+ return this.path.map(scaleCommandBy(this.scaleFactor)).map(commandToString).join("");
28690
+ }
28691
+
28692
+ // add a move (M) command to the path
28693
+ move(x, y) {
28694
+ this.path.push({
28695
+ action: "M",
28696
+ coords: [x, y]
28697
+ });
28698
+ return this;
28699
+ }
28700
+
28701
+ // add a curve (C) command to the path
28702
+ curve(control1X, control1Y, control2X, control2Y, endX, endY) {
28703
+ this.path.push({
28704
+ action: "C",
28705
+ coords: [control1X, control1Y, control2X, control2Y, endX, endY]
28706
+ });
28707
+ return this;
28708
+ }
28709
+
28710
+ // Scale all coordinates of the path by a factor
28711
+ scale(factor) {
28712
+ this.scaleFactor *= factor;
28713
+ return this;
28714
+ }
28715
+ }
28716
+ function commandToString(command) {
28717
+ return `${command.action}${command.coords.join(" ")}`;
28718
+ }
28719
+ function scaleCommandBy(scaleFactor) {
28720
+ return command => _extends({}, command, {
28721
+ coords: command.coords.map(c => c * scaleFactor)
28722
+ });
28723
+ }
28724
+
28725
+ // We use the pathBuilder here to scale up the SVG path coordinates used
28726
+ // elsewhere for arrowheads. We scale in the path itself instead of using a CSS
28727
+ // transform because a transform would also scale up the stroke weight, and we
28728
+ // don't want that.
28729
+ 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();
28730
+ function Arrowhead(props) {
28731
+ var _props$color;
28732
+ const [point] = useTransformVectorsToPixels(props.tip);
28733
+ return /*#__PURE__*/React.createElement("g", {
28734
+ transform: `translate(${point[0]} ${point[1]}) rotate(${-props.angle})`
28735
+ }, /*#__PURE__*/React.createElement("g", {
28736
+ transform: "translate(-1.5)"
28737
+ }, /*#__PURE__*/React.createElement("path", {
28738
+ d: arrowPath,
28739
+ fill: "none",
28740
+ style: {
28741
+ stroke: (_props$color = props.color) != null ? _props$color : "inherit"
28742
+ },
28743
+ strokeLinejoin: "round",
28744
+ strokeLinecap: "round"
28745
+ })));
28746
+ }
28747
+
28748
+ function SVGLine(props) {
28749
+ const {
28750
+ start,
28751
+ end,
28752
+ style,
28753
+ className,
28754
+ testId
28755
+ } = props;
28756
+ return /*#__PURE__*/React.createElement("line", {
28757
+ x1: start[0],
28758
+ y1: start[1],
28759
+ x2: end[0],
28760
+ y2: end[1],
28761
+ style: style,
28762
+ className: className,
28763
+ "data-testid": testId
28764
+ });
28765
+ }
28766
+
28767
+ function Vector(props) {
28768
+ const {
28769
+ tail,
28770
+ tip,
28771
+ color,
28772
+ style
28773
+ } = props;
28774
+ const [tailPx, tipPx] = useTransformVectorsToPixels(tail, tip);
28775
+ const direction = vec.sub(tip, tail);
28776
+ return /*#__PURE__*/React.createElement("g", {
28777
+ style: {
28778
+ stroke: color,
28779
+ strokeWidth: 2
28780
+ }
28781
+ }, /*#__PURE__*/React.createElement(SVGLine, {
28782
+ start: tailPx,
28783
+ end: tipPx,
28784
+ style: style
28785
+ }), /*#__PURE__*/React.createElement(Arrowhead, {
28786
+ tip: tip,
28787
+ angle: angleDegrees(direction)
28788
+ }));
28789
+ }
28790
+ function angleDegrees([x, y]) {
28791
+ return Math.atan2(y, x) * 180 / Math.PI;
28792
+ }
28793
+
28794
+ /**
28795
+ * Given a ray and a rectangular box, find the point where the ray intersects
28796
+ * the edge of the box. Assumes the `initialPoint` is inside the box.
28797
+ * @param initialPoint - The starting point of the ray.
28798
+ * @param throughPoint - A point that the ray passes through. Must be different from initialPoint.
28799
+ * @param box - The box with which to intersect the ray, in the form [[xMin, xMax], [yMin, yMax]]
28800
+ */
28801
+ const getIntersectionOfRayWithBox = (initialPoint, throughPoint, box) => {
28802
+ const [[xMin, xMax], [yMin, yMax]] = box;
28803
+ const [aX, aY] = throughPoint;
28804
+ const [bX, bY] = initialPoint;
28805
+ const yDiff = bY - aY;
28806
+ const xDiff = bX - aX;
28807
+ const slope = yDiff / xDiff;
28808
+ const yAtXMin = slope * (xMin - aX) + aY;
28809
+ const yAtXMax = slope * (xMax - aX) + aY;
28810
+ const xAtYMin = (yMin - aY) / slope + aX;
28811
+ const xAtYMax = (yMax - aY) / slope + aX;
28812
+
28813
+ // clock analogy to describe quadrants
28814
+ switch (true) {
28815
+ // 12 o'clock to 2:59
28816
+ case yDiff > 0 && xDiff >= 0:
28817
+ return xAtYMax > xMax ? [xMax, yAtXMax] : [xAtYMax, yMax];
28818
+ // 3 o'clock to 5:59
28819
+ case yDiff <= 0 && xDiff > 0:
28820
+ // xAtYMin evaluates to -Infinity here, so we use absolute value
28821
+ return Math.abs(xAtYMin) > xMax ? [xMax, yAtXMax] : [xAtYMin, yMin];
28822
+ // 9 o'clock to 11:59
28823
+ case yDiff >= 0 && xDiff < 0:
28824
+ return xAtYMax < xMin ? [xMin, yAtXMin] : [xAtYMax, yMax];
28825
+ // 6 o'clock to 8:59
28826
+ case yDiff < 0 && xDiff <= 0:
28827
+ return xAtYMin < xMin ? [xMin, yAtXMin] : [xAtYMin, yMin];
28828
+ default:
28829
+ return [xMax, yAtXMax];
28830
+ }
28831
+ };
28832
+ const getLines = points => points.map((point, i) => {
28833
+ const next = points[(i + 1) % points.length];
28834
+ return [point, next];
28835
+ });
28836
+
28597
28837
  const LockedLine = props => {
28598
28838
  const {
28599
28839
  color: color$1,
@@ -28601,11 +28841,27 @@ const LockedLine = props => {
28601
28841
  kind,
28602
28842
  points,
28603
28843
  showStartPoint,
28604
- showEndPoint
28844
+ showEndPoint,
28845
+ range
28605
28846
  } = props;
28606
28847
  const [point1, point2] = points;
28607
28848
  let line;
28608
- if (kind === "line" || kind === "segment") {
28849
+ if (kind === "ray") {
28850
+ // Rays extend to the end of the graph in one direction.
28851
+ const endExtend = getIntersectionOfRayWithBox(point2.coord, point1.coord, range);
28852
+ line = /*#__PURE__*/React.createElement(Vector, {
28853
+ tail: point1.coord,
28854
+ tip: endExtend,
28855
+ color: lockedFigureColors[color$1],
28856
+ style: {
28857
+ strokeDasharray: lineStyle === "dashed" ?
28858
+ // TODO(lems-1930): Uncomment this line when the
28859
+ // dashed style is updated in Mafs.
28860
+ // ? "var(--mafs-line-stroke-dash-style)"
28861
+ "4, 3" : undefined
28862
+ }
28863
+ });
28864
+ } else {
28609
28865
  const LineType = kind === "segment" ? Line$3.Segment : Line$3.ThroughPoints;
28610
28866
  line = /*#__PURE__*/React.createElement(LineType, {
28611
28867
  point1: point1.coord,
@@ -28613,12 +28869,9 @@ const LockedLine = props => {
28613
28869
  color: lockedFigureColors[color$1],
28614
28870
  style: lineStyle
28615
28871
  });
28616
- } else {
28617
- // TODO(LEMS-1928): Implement rays
28618
- return null;
28619
28872
  }
28620
28873
  return /*#__PURE__*/React.createElement("g", {
28621
- className: "locked-line"
28874
+ className: kind === "ray" ? "locked-ray" : "locked-line"
28622
28875
  }, line, showStartPoint && /*#__PURE__*/React.createElement(Point$2, {
28623
28876
  x: point1.coord[0],
28624
28877
  y: point1.coord[1],
@@ -28667,14 +28920,16 @@ const GraphLockedLayer = props => {
28667
28920
  lockedFigures
28668
28921
  } = props;
28669
28922
  return /*#__PURE__*/React.createElement(React.Fragment, null, lockedFigures.map((figure, index) => {
28670
- let Figure;
28671
28923
  switch (figure.type) {
28672
28924
  case "point":
28673
- Figure = LockedPoint;
28674
- break;
28925
+ return /*#__PURE__*/React.createElement(LockedPoint, _extends({
28926
+ key: `point-${index}`
28927
+ }, figure));
28675
28928
  case "line":
28676
- Figure = LockedLine;
28677
- break;
28929
+ return /*#__PURE__*/React.createElement(LockedLine, _extends({
28930
+ key: `line-${index}`,
28931
+ range: props.range
28932
+ }, figure));
28678
28933
  default:
28679
28934
  /**
28680
28935
  * Devlopment-time future-proofing: This should
@@ -28684,9 +28939,6 @@ const GraphLockedLayer = props => {
28684
28939
  */
28685
28940
  throw new UnreachableCaseError(figure);
28686
28941
  }
28687
- return /*#__PURE__*/React.createElement(Figure, _extends({
28688
- key: `${figure.type}-${index}`
28689
- }, figure));
28690
28942
  }));
28691
28943
  };
28692
28944
 
@@ -28768,171 +29020,6 @@ function snap(snapStep, point) {
28768
29020
  return [Math.round(requestedX / snapX) * snapX, Math.round(requestedY / snapY) * snapY];
28769
29021
  }
28770
29022
 
28771
- const useTransform = (...points) => {
28772
- const {
28773
- viewTransform,
28774
- userTransform
28775
- } = useTransformContext();
28776
- const transformToPx = vec.matrixMult(viewTransform, userTransform);
28777
- const transformedPoints = points.map(point => vec.transform(point, transformToPx));
28778
- return transformedPoints;
28779
- };
28780
-
28781
- /**
28782
- * Given two points, find the tips that extends through the points to the edge of the range.
28783
- * @param collinearPoint - The point that the line passes through. Needed to establish slope.
28784
- * @param extendFrom - The point that the line extends from to the edge of the graph.
28785
- */
28786
- const getRayIntersectionCoords = (collinearPoint, extendFrom, range) => {
28787
- // edges of the graph
28788
- const [[xMin, xMax], [yMin, yMax]] = range;
28789
- const [aX, aY] = collinearPoint;
28790
- const [bX, bY] = extendFrom;
28791
- const yDiff = bY - aY;
28792
- const xDiff = bX - aX;
28793
- const slope = yDiff / xDiff;
28794
- const yAtXMin = slope * (xMin - aX) + aY;
28795
- const yAtXMax = slope * (xMax - aX) + aY;
28796
- const xAtYMin = (yMin - aY) / slope + aX;
28797
- const xAtYMax = (yMax - aY) / slope + aX;
28798
-
28799
- // clock analogy to describe quadrants
28800
- switch (true) {
28801
- // 12 o'clock to 2:59
28802
- case yDiff > 0 && xDiff >= 0:
28803
- return xAtYMax > xMax ? [xMax, yAtXMax] : [xAtYMax, yMax];
28804
- // 3 o'clock to 5:59
28805
- case yDiff <= 0 && xDiff > 0:
28806
- // xAtYMin evaluates to -Infinity here, so we use absolute value
28807
- return Math.abs(xAtYMin) > xMax ? [xMax, yAtXMax] : [xAtYMin, yMin];
28808
- // 9 o'clock to 11:59
28809
- case yDiff >= 0 && xDiff < 0:
28810
- return xAtYMax < xMin ? [xMin, yAtXMin] : [xAtYMax, yMax];
28811
- // 6 o'clock to 8:59
28812
- case yDiff < 0 && xDiff <= 0:
28813
- return xAtYMin < xMin ? [xMin, yAtXMin] : [xAtYMin, yMin];
28814
- default:
28815
- return [xMax, yAtXMax];
28816
- }
28817
- };
28818
- const getLines = points => points.map((point, i) => {
28819
- const next = points[(i + 1) % points.length];
28820
- return [point, next];
28821
- });
28822
-
28823
- function SVGLine(props) {
28824
- const {
28825
- start,
28826
- end,
28827
- style,
28828
- className,
28829
- testId
28830
- } = props;
28831
- return /*#__PURE__*/React.createElement("line", {
28832
- x1: start[0],
28833
- y1: start[1],
28834
- x2: end[0],
28835
- y2: end[1],
28836
- style: style,
28837
- className: `${className}`,
28838
- "data-testid": testId
28839
- });
28840
- }
28841
-
28842
- function pathBuilder() {
28843
- return new PathBuilder();
28844
- }
28845
- class PathBuilder {
28846
- constructor() {
28847
- this.path = [];
28848
- this.scaleFactor = 1;
28849
- }
28850
- build() {
28851
- return this.path.map(scaleCommandBy(this.scaleFactor)).map(commandToString).join("");
28852
- }
28853
-
28854
- // add a move (M) command to the path
28855
- move(x, y) {
28856
- this.path.push({
28857
- action: "M",
28858
- coords: [x, y]
28859
- });
28860
- return this;
28861
- }
28862
-
28863
- // add a curve (C) command to the path
28864
- curve(control1X, control1Y, control2X, control2Y, endX, endY) {
28865
- this.path.push({
28866
- action: "C",
28867
- coords: [control1X, control1Y, control2X, control2Y, endX, endY]
28868
- });
28869
- return this;
28870
- }
28871
-
28872
- // Scale all coordinates of the path by a factor
28873
- scale(factor) {
28874
- this.scaleFactor *= factor;
28875
- return this;
28876
- }
28877
- }
28878
- function commandToString(command) {
28879
- return `${command.action}${command.coords.join(" ")}`;
28880
- }
28881
- function scaleCommandBy(scaleFactor) {
28882
- return command => _extends({}, command, {
28883
- coords: command.coords.map(c => c * scaleFactor)
28884
- });
28885
- }
28886
-
28887
- // We use the pathBuilder here to scale up the SVG path coordinates used
28888
- // elsewhere for arrowheads. We scale in the path itself instead of using a CSS
28889
- // transform because a transform would also scale up the stroke weight, and we
28890
- // don't want that.
28891
- 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();
28892
- function Arrowhead(props) {
28893
- var _props$color;
28894
- const [point] = useTransform([props.x, props.y]);
28895
- return /*#__PURE__*/React.createElement("g", {
28896
- transform: `translate(${point[0]} ${point[1]}) rotate(${-props.angle})`
28897
- }, /*#__PURE__*/React.createElement("g", {
28898
- transform: "translate(-1.5)"
28899
- }, /*#__PURE__*/React.createElement("path", {
28900
- d: arrowPath,
28901
- fill: "none",
28902
- style: {
28903
- stroke: (_props$color = props.color) != null ? _props$color : "inherit"
28904
- },
28905
- strokeLinejoin: "round",
28906
- strokeLinecap: "round"
28907
- })));
28908
- }
28909
-
28910
- function Vector(props) {
28911
- const {
28912
- tail,
28913
- tip,
28914
- color
28915
- } = props;
28916
- const [tailPx, tipPx] = useTransform(tail, tip);
28917
- const direction = vec.sub(tip, tail);
28918
- return /*#__PURE__*/React.createElement("g", {
28919
- style: {
28920
- stroke: color,
28921
- strokeWidth: 2
28922
- }
28923
- }, /*#__PURE__*/React.createElement(SVGLine, {
28924
- start: tailPx,
28925
- end: tipPx
28926
- }), /*#__PURE__*/React.createElement(Arrowhead, {
28927
- x: tip[0],
28928
- y: tip[1],
28929
- angle: angleDegrees(direction)
28930
- }));
28931
- }
28932
- function angleDegrees([x, y]) {
28933
- return Math.atan2(y, x) * 180 / Math.PI;
28934
- }
28935
-
28936
29023
  const defaultStroke = "var(--movable-line-stroke-color)";
28937
29024
  const MovableLine = props => {
28938
29025
  const {
@@ -28943,12 +29030,16 @@ const MovableLine = props => {
28943
29030
  stroke = defaultStroke
28944
29031
  } = props;
28945
29032
  const midpoint = vec.midpoint(start, end);
28946
- const [startPtPx, endPtPx] = useTransform(start, end);
29033
+ const [startPtPx, endPtPx] = useTransformVectorsToPixels(start, end);
29034
+ const {
29035
+ graphDimensionsInPixels
29036
+ } = useGraphConfig();
28947
29037
  let startExtend = undefined;
28948
29038
  let endExtend = undefined;
28949
29039
  if (extend) {
28950
- startExtend = extend.start ? getRayIntersectionCoords(end, start, extend.range) : undefined;
28951
- endExtend = extend.end ? getRayIntersectionCoords(start, end, extend.range) : undefined;
29040
+ const trimmedRange = trimRange(extend.range, graphDimensionsInPixels);
29041
+ startExtend = extend.start ? getIntersectionOfRayWithBox(start, end, trimmedRange) : undefined;
29042
+ endExtend = extend.end ? getIntersectionOfRayWithBox(end, start, trimmedRange) : undefined;
28952
29043
  }
28953
29044
  const line = useRef(null);
28954
29045
  const {
@@ -29005,16 +29096,24 @@ const MovableLine = props => {
29005
29096
  color: stroke
29006
29097
  }));
29007
29098
  };
29008
-
29009
- const defaultGraphConfig = {
29010
- range: [[0, 1], [0, 1]],
29011
- snapStep: [1, 1],
29012
- markings: "none",
29013
- showTooltips: false
29014
- };
29015
- const GraphConfigContext = /*#__PURE__*/createContext(defaultGraphConfig);
29016
- function useGraphConfig() {
29017
- return React__default.useContext(GraphConfigContext);
29099
+ function trimRange(range, graphDimensionsInPixels) {
29100
+ const pixelsToTrim = 4;
29101
+ const [xRange, yRange] = range;
29102
+ const [pixelsWide, pixelsTall] = graphDimensionsInPixels;
29103
+ const graphUnitsPerPixelX = size(xRange) / pixelsWide;
29104
+ const graphUnitsPerPixelY = size(yRange) / pixelsTall;
29105
+ const graphUnitsToTrimX = pixelsToTrim * graphUnitsPerPixelX;
29106
+ const graphUnitsToTrimY = pixelsToTrim * graphUnitsPerPixelY;
29107
+ return [trim(xRange, graphUnitsToTrimX), trim(yRange, graphUnitsToTrimY)];
29108
+ }
29109
+ function trim(interval, amount) {
29110
+ if (size(interval) < amount * 2) {
29111
+ return [0, 0];
29112
+ }
29113
+ return [interval[0] + amount, interval[1] - amount];
29114
+ }
29115
+ function size(interval) {
29116
+ return interval[1] - interval[0];
29018
29117
  }
29019
29118
 
29020
29119
  // The hitbox size of 48px by 48px is preserved from the legacy interactive
@@ -29047,13 +29146,13 @@ const StyledMovablePoint = props => {
29047
29146
  constrain: p => snap(snapStep, p)
29048
29147
  });
29049
29148
  const pointClasses = `movable-point ${dragging ? "movable-point--dragging" : ""}`;
29050
- const [[x, y]] = useTransform(point);
29149
+ const [[x, y]] = useTransformVectorsToPixels(point);
29051
29150
  const [xMin, xMax] = range[0];
29052
29151
  const [yMin, yMax] = range[1];
29053
- const [[verticalStartX]] = useTransform([xMin, 0]);
29054
- const [[verticalEndX]] = useTransform([xMax, 0]);
29055
- const [[_, horizontalStartY]] = useTransform([0, yMin]);
29056
- const [[__, horizontalEndY]] = useTransform([0, yMax]);
29152
+ const [[verticalStartX]] = useTransformVectorsToPixels([xMin, 0]);
29153
+ const [[verticalEndX]] = useTransformVectorsToPixels([xMax, 0]);
29154
+ const [[_, horizontalStartY]] = useTransformVectorsToPixels([0, yMin]);
29155
+ const [[__, horizontalEndY]] = useTransformVectorsToPixels([0, yMax]);
29057
29156
  const showHairlines = dragging && markings !== "none";
29058
29157
  const hairlines = /*#__PURE__*/React.createElement("g", null, /*#__PURE__*/React.createElement("line", {
29059
29158
  x1: verticalStartX,
@@ -29375,7 +29474,7 @@ const isRightAngle = angle => Math.abs(angle - Math.PI / 2) < 0.01;
29375
29474
  */
29376
29475
  const shouldDrawArcOutside = (midpoint, vertex, range, polygonLines) => {
29377
29476
  // Create a ray from the midpoint (inside angle) to the edge of the range
29378
- const rangeIntersectionPoint = getRayIntersectionCoords(midpoint, vertex, range);
29477
+ const rangeIntersectionPoint = getIntersectionOfRayWithBox(vertex, midpoint, range);
29379
29478
  let lineIntersectionCount = 0;
29380
29479
  polygonLines.forEach(line => linesIntersect([vertex, rangeIntersectionPoint], line) && lineIntersectionCount++);
29381
29480
 
@@ -29498,23 +29597,19 @@ function AxisArrows() {
29498
29597
  const axisColor = "var(--mafs-fg)";
29499
29598
  return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(Arrowhead, {
29500
29599
  color: axisColor,
29501
- x: xMin,
29502
- y: 0,
29600
+ tip: [xMin, 0],
29503
29601
  angle: 180
29504
29602
  }), /*#__PURE__*/React__default.createElement(Arrowhead, {
29505
29603
  color: axisColor,
29506
- x: xMax,
29507
- y: 0,
29604
+ tip: [xMax, 0],
29508
29605
  angle: 0
29509
29606
  }), /*#__PURE__*/React__default.createElement(Arrowhead, {
29510
29607
  color: axisColor,
29511
- x: 0,
29512
- y: yMin,
29608
+ tip: [0, yMin],
29513
29609
  angle: 270
29514
29610
  }), /*#__PURE__*/React__default.createElement(Arrowhead, {
29515
29611
  color: axisColor,
29516
- x: 0,
29517
- y: yMax,
29612
+ tip: [0, yMax],
29518
29613
  angle: 90
29519
29614
  }));
29520
29615
  }
@@ -29539,7 +29634,7 @@ const YGridTick = ({
29539
29634
  tickStep
29540
29635
  }) => {
29541
29636
  const pointOnAxis = [0, y];
29542
- const [[xPosition, yPosition]] = useTransform(pointOnAxis);
29637
+ const [[xPosition, yPosition]] = useTransformVectorsToPixels(pointOnAxis);
29543
29638
  return /*#__PURE__*/React.createElement("g", {
29544
29639
  className: "y-axis-ticks"
29545
29640
  }, /*#__PURE__*/React.createElement("line", {
@@ -29562,7 +29657,7 @@ const XGridTick = ({
29562
29657
  tickStep
29563
29658
  }) => {
29564
29659
  const pointOnAxis = [x, 0];
29565
- const [[xPosition, yPosition]] = useTransform(pointOnAxis);
29660
+ const [[xPosition, yPosition]] = useTransformVectorsToPixels(pointOnAxis);
29566
29661
  return /*#__PURE__*/React.createElement("g", {
29567
29662
  className: "x-axis-ticks"
29568
29663
  }, /*#__PURE__*/React.createElement("line", {
@@ -29783,97 +29878,26 @@ const styles$k = StyleSheet.create({
29783
29878
  }
29784
29879
  });
29785
29880
 
29786
- const colors$1 = {
29787
- grayLight: "#aaa",
29788
- basicBorderColor: "#ccc",
29789
- white: "#fff"
29790
- };
29791
- const triangleBeforeAfter = {
29792
- borderBottom: "9px solid transparent",
29793
- borderTop: "9px solid transparent",
29794
- content: '" "',
29795
- height: "0",
29796
- position: "absolute",
29797
- top: "0",
29798
- width: "0"
29881
+ const InfoTip$1 = props => {
29882
+ return /*#__PURE__*/React.createElement(Tooltip$1, {
29883
+ content: /*#__PURE__*/React.createElement(Body, {
29884
+ style: styles$j.body
29885
+ }, props.children),
29886
+ placement: "right"
29887
+ }, /*#__PURE__*/React.createElement(PhosphorIcon, {
29888
+ size: "small",
29889
+ icon: questionIcon,
29890
+ style: styles$j.spacingLeft
29891
+ }));
29799
29892
  };
29800
29893
  const styles$j = StyleSheet.create({
29801
- infoTip: {
29802
- display: "inline-block",
29803
- marginLeft: "5px",
29804
- position: "relative"
29805
- },
29806
- infoTipContainer: {
29807
- position: "absolute",
29808
- top: "-12px",
29809
- left: "22px",
29810
- zIndex: 1000
29811
- },
29812
- infoTipTriangle: {
29813
- height: "10px",
29814
- left: "0",
29815
- position: "absolute",
29816
- top: "8px",
29817
- width: "0",
29818
- zIndex: 1,
29819
- ":before": _extends({}, triangleBeforeAfter, {
29820
- borderRight: "9px solid #bbb",
29821
- right: "0"
29822
- }),
29823
- ":after": _extends({}, triangleBeforeAfter, {
29824
- borderRight: `9px solid ${colors$1.white}`,
29825
- right: "-1px"
29826
- })
29827
- },
29828
- verticalShadow: {
29829
- border: `1px solid ${colors$1.basicBorderColor}`,
29830
- boxShadow: `0 1px 3px ${colors$1.basicBorderColor}`,
29831
- borderBottom: `1px solid ${colors$1.grayLight}`
29894
+ spacingLeft: {
29895
+ marginInlineStart: spacing.xxxSmall_4
29832
29896
  },
29833
- infoTipContentContainer: {
29834
- background: colors$1.white,
29835
- padding: "5px 10px",
29836
- width: "240px"
29897
+ body: {
29898
+ padding: spacing.medium_16
29837
29899
  }
29838
29900
  });
29839
- class InfoTip$1 extends React.Component {
29840
- constructor(...args) {
29841
- super(...args);
29842
- this.state = {
29843
- hover: false
29844
- };
29845
- this.handleMouseEnter = () => {
29846
- this.setState({
29847
- hover: true
29848
- });
29849
- };
29850
- this.handleMouseLeave = () => {
29851
- this.setState({
29852
- hover: false
29853
- });
29854
- };
29855
- }
29856
- render() {
29857
- return /*#__PURE__*/React.createElement("div", {
29858
- className: css(styles$j.infoTip)
29859
- }, /*#__PURE__*/React.createElement(View, {
29860
- onMouseEnter: this.handleMouseEnter,
29861
- onMouseLeave: this.handleMouseLeave
29862
- }, /*#__PURE__*/React.createElement(PhosphorIcon, {
29863
- size: "small",
29864
- icon: questionIcon
29865
- })), /*#__PURE__*/React.createElement("div", {
29866
- className: css(styles$j.infoTipContainer),
29867
- style: {
29868
- display: this.state.hover ? "block" : "none"
29869
- }
29870
- }, /*#__PURE__*/React.createElement("div", {
29871
- className: css(styles$j.infoTipTriangle)
29872
- }), /*#__PURE__*/React.createElement("div", {
29873
- className: css(styles$j.verticalShadow, styles$j.infoTipContentContainer)
29874
- }, this.props.children)));
29875
- }
29876
- }
29877
29901
 
29878
29902
  /**
29879
29903
  * A wrapper around info-tip/info-tip.jsx that can be rendered on the
@@ -30713,9 +30737,7 @@ const getDefaultPoints = ({
30713
30737
  // Instead of updating to v5, using generic type to relate input and output types.
30714
30738
  function getGradableGraph(state, initialGraph) {
30715
30739
  if (!state.hasBeenInteractedWith) {
30716
- return {
30717
- type: initialGraph.type
30718
- };
30740
+ return _extends({}, initialGraph);
30719
30741
  }
30720
30742
  if (initialGraph.type === "linear-system" && state.type === "linear-system") {
30721
30743
  return _extends({}, initialGraph, {
@@ -30874,7 +30896,8 @@ const StatefulMafsGraph = /*#__PURE__*/React.forwardRef((props, ref) => {
30874
30896
  const MafsGraph = props => {
30875
30897
  const {
30876
30898
  state,
30877
- dispatch
30899
+ dispatch,
30900
+ labels
30878
30901
  } = props;
30879
30902
  const [width, height] = props.box;
30880
30903
  const prevState = useRef(state);
@@ -30903,7 +30926,11 @@ const MafsGraph = props => {
30903
30926
  range: state.range,
30904
30927
  snapStep: state.snapStep,
30905
30928
  markings: props.markings,
30906
- showTooltips: !!props.showTooltips
30929
+ showTooltips: !!props.showTooltips,
30930
+ graphDimensionsInPixels: props.box,
30931
+ width,
30932
+ height,
30933
+ labels
30907
30934
  }
30908
30935
  }, /*#__PURE__*/React.createElement(View, {
30909
30936
  style: {
@@ -30922,7 +30949,7 @@ const MafsGraph = props => {
30922
30949
  bottom: 0,
30923
30950
  left: 0
30924
30951
  }
30925
- }, /*#__PURE__*/React.createElement(Mafs, {
30952
+ }, props.markings === "graph" && /*#__PURE__*/React.createElement(AxisLabels, null), /*#__PURE__*/React.createElement(Mafs, {
30926
30953
  preserveAspectRatio: false,
30927
30954
  viewBox: {
30928
30955
  x: props.range[0],
@@ -30940,7 +30967,8 @@ const MafsGraph = props => {
30940
30967
  containerSizeClass: props.containerSizeClass,
30941
30968
  markings: props.markings
30942
30969
  }), props.lockedFigures && /*#__PURE__*/React.createElement(GraphLockedLayer, {
30943
- lockedFigures: props.lockedFigures
30970
+ lockedFigures: props.lockedFigures,
30971
+ range: props.range
30944
30972
  }), renderGraph({
30945
30973
  state,
30946
30974
  dispatch
@@ -32104,8 +32132,6 @@ class LegacyInteractiveGraph extends React.Component {
32104
32132
  graph: graph
32105
32133
  });
32106
32134
  };
32107
- this.getUserInput = () => InteractiveGraph.getUserInputFromProps(this.props);
32108
- this.simpleValidate = rubric => InteractiveGraph.validate(this.getUserInput(), rubric, this);
32109
32135
  this.focus = $.noop;
32110
32136
  }
32111
32137
  // TODO(jangmi, CP-3288): Remove usage of `UNSAFE_componentWillMount`
@@ -32157,6 +32183,12 @@ class LegacyInteractiveGraph extends React.Component {
32157
32183
  areSegmentGraphsEqual(prevGraph, currentGraph) {
32158
32184
  return prevGraph.numSegments !== currentGraph.numSegments;
32159
32185
  }
32186
+ getUserInput() {
32187
+ return InteractiveGraph.getUserInputFromProps(this.props);
32188
+ }
32189
+ simpleValidate(rubric) {
32190
+ return InteractiveGraph.validate(this.getUserInput(), rubric, this);
32191
+ }
32160
32192
  render() {
32161
32193
  const box = getInteractiveBoxFromSizeClass(this.props.containerSizeClass);
32162
32194
  const gridStep = this.props.gridStep || Util.getGridStep(this.props.range, this.props.step, box[0]);
@@ -32238,10 +32270,10 @@ class InteractiveGraph extends React.Component {
32238
32270
  }
32239
32271
  throw new PerseusError("Cannot getUserInput from a graph that has never rendered", Errors.NotAllowed);
32240
32272
  };
32241
- this.simpleValidate = rubric => {
32242
- var _this$getUserInput;
32243
- return InteractiveGraph.validate((_this$getUserInput = this.getUserInput) == null ? void 0 : _this$getUserInput.call(this), rubric, this);
32244
- };
32273
+ }
32274
+ simpleValidate(rubric) {
32275
+ var _this$getUserInput;
32276
+ return InteractiveGraph.validate((_this$getUserInput = this.getUserInput) == null ? void 0 : _this$getUserInput.call(this), rubric, this);
32245
32277
  }
32246
32278
  render() {
32247
32279
  var _this$props$apiOption;
@@ -43037,7 +43069,7 @@ const init = function init(options) {
43037
43069
 
43038
43070
  // This file is processed by a Rollup plugin (replace) to inject the production
43039
43071
  const libName = "@khanacademy/perseus";
43040
- const libVersion = "22.0.2";
43072
+ const libVersion = "22.2.0";
43041
43073
  addLibraryVersionToPerseusDebug(libName, libVersion);
43042
43074
 
43043
43075
  const apiVersion = {