@khanacademy/perseus 33.0.0 → 33.1.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
@@ -28320,6 +28320,94 @@ Graph.defaultProps = {
28320
28320
  isMobile: false
28321
28321
  };
28322
28322
 
28323
+ function pathBuilder() {
28324
+ return new PathBuilder();
28325
+ }
28326
+ class PathBuilder {
28327
+ constructor() {
28328
+ this.path = [];
28329
+ this.scaleFactor = 1;
28330
+ }
28331
+ build() {
28332
+ return this.path.map(scaleCommandBy(this.scaleFactor)).map(commandToString).join("");
28333
+ }
28334
+
28335
+ // add a move (M) command to the path
28336
+ move(x, y) {
28337
+ this.path.push({
28338
+ action: "M",
28339
+ args: [x, y]
28340
+ });
28341
+ return this;
28342
+ }
28343
+ line(x, y) {
28344
+ this.path.push({
28345
+ action: "L",
28346
+ args: [x, y]
28347
+ });
28348
+ return this;
28349
+ }
28350
+ circularArc(radius, toX, toY, {
28351
+ sweep = false,
28352
+ largeArc = false
28353
+ } = {}) {
28354
+ this.path.push({
28355
+ action: "A",
28356
+ args: [radius, radius, 0 /* rotation */, largeArc ? 1 : 0, sweep ? 1 : 0, toX, toY]
28357
+ });
28358
+ return this;
28359
+ }
28360
+
28361
+ // add a curve (C) command to the path
28362
+ curve(control1X, control1Y, control2X, control2Y, endX, endY) {
28363
+ this.path.push({
28364
+ action: "C",
28365
+ args: [control1X, control1Y, control2X, control2Y, endX, endY]
28366
+ });
28367
+ return this;
28368
+ }
28369
+
28370
+ // Scale all coordinates of the path by a factor
28371
+ scale(factor) {
28372
+ this.scaleFactor *= factor;
28373
+ return this;
28374
+ }
28375
+ }
28376
+ function commandToString(command) {
28377
+ return `${command.action}${command.args.join(" ")}`;
28378
+ }
28379
+ function scaleCommandBy(scaleFactor) {
28380
+ return command => {
28381
+ switch (command.action) {
28382
+ case "A":
28383
+ // TODO(benchristel): do we want to refactor this switch to
28384
+ // use polymorphism?
28385
+ // Arc command
28386
+ return _extends({}, command, {
28387
+ args: [command.args[0] * scaleFactor,
28388
+ // x radius
28389
+ command.args[1] * scaleFactor,
28390
+ // y radius
28391
+ command.args[2],
28392
+ // rotation
28393
+ command.args[3],
28394
+ // largeArc flag
28395
+ command.args[4],
28396
+ // sweep flag
28397
+ command.args[5] * scaleFactor,
28398
+ // end x
28399
+ command.args[6] * scaleFactor // end y
28400
+ ]
28401
+ });
28402
+
28403
+ default:
28404
+ return _extends({}, command, {
28405
+ args: command.args.map(c => c * scaleFactor)
28406
+ });
28407
+ }
28408
+ };
28409
+ }
28410
+
28323
28411
  // Restricts `value` to be between `min` and `max`.
28324
28412
  function clamp(value, min, max) {
28325
28413
  if (value < min) {
@@ -28536,377 +28624,6 @@ function pixelsToVectors(pixels, graphState) {
28536
28624
  });
28537
28625
  }
28538
28626
 
28539
- function AxisLabels() {
28540
- const {
28541
- range,
28542
- labels,
28543
- width,
28544
- height
28545
- } = useGraphConfig();
28546
- const yAxisLabelLocation = [0, range[Y][MAX]];
28547
- const xAxisLabelLocation = [range[X][MAX], 0];
28548
- const [xAxisLabelText, yAxisLabelText] = labels;
28549
- const graphInfo = {
28550
- range,
28551
- width,
28552
- height
28553
- };
28554
- const [x1, y1] = pointToPixel(xAxisLabelLocation, graphInfo);
28555
- // The default location for the y-axis-label is at the maximum y point
28556
- // This is just underneath the tick line for that point. The -24 moves the
28557
- // label up one grid square, so it sits on top of the graph
28558
- const [x2, y2] = vec.add(pointToPixel(yAxisLabelLocation, graphInfo), [0, -24]);
28559
- const {
28560
- TeX
28561
- } = getDependencies();
28562
- return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement("span", {
28563
- style: {
28564
- position: "absolute",
28565
- left: x1,
28566
- top: y1,
28567
- fontSize: "14px",
28568
- transform: "translate(7px, -50%)"
28569
- }
28570
- }, /*#__PURE__*/React__default.createElement(TeX, null, xAxisLabelText)), /*#__PURE__*/React__default.createElement("span", {
28571
- style: {
28572
- position: "absolute",
28573
- left: x2,
28574
- top: y2,
28575
- fontSize: "14px",
28576
- transform: "translate(-50%, 0px)"
28577
- }
28578
- }, /*#__PURE__*/React__default.createElement(TeX, null, yAxisLabelText)));
28579
- }
28580
-
28581
- // Universal padding for the axis ticks to ensure that the labels
28582
- // have enough space to render without overlapping the tick lines
28583
- const tickPadding = 5;
28584
- const AxisTickLabels = () => {
28585
- const graphConfig = useGraphConfig();
28586
- const {
28587
- tickStep,
28588
- range
28589
- } = graphConfig;
28590
- const [[xMin, xMax], [yMin, yMax]] = range;
28591
- const [xTickStep, yTickStep] = tickStep;
28592
- const yGridTicks = generateTickLocations$1(yTickStep, yMin, yMax);
28593
- const xGridTicks = generateTickLocations$1(xTickStep, xMin, xMax);
28594
- return /*#__PURE__*/React.createElement("div", {
28595
- className: "axis-tick-labels"
28596
- }, /*#__PURE__*/React.createElement(YGridAxis, {
28597
- axisTicks: yGridTicks,
28598
- graphConfig: graphConfig
28599
- }), /*#__PURE__*/React.createElement(XGridAxis, {
28600
- axisTicks: xGridTicks,
28601
- graphConfig: graphConfig
28602
- }));
28603
- };
28604
-
28605
- // Generate the necessary styles and ticks for the y-axis tick labels container
28606
- const YGridAxis = props => {
28607
- const {
28608
- axisTicks,
28609
- graphConfig
28610
- } = props;
28611
- const {
28612
- width,
28613
- height
28614
- } = graphConfig;
28615
-
28616
- // First get the label height by calculating the difference between the first two y-axis labels
28617
- const yTopPoint = pointToPixel([0, axisTicks[0]], graphConfig);
28618
- const ySecondPoint = pointToPixel([0, axisTicks[1]], graphConfig);
28619
- const yLabelHeight = ySecondPoint[1] - yTopPoint[1];
28620
-
28621
- // When rendering purely positive y-axis labels, we want to adjust the left position
28622
- // from the lowest calculated tick, as yMin may be negative
28623
- const yLowestTick = axisTicks[axisTicks.length - 1];
28624
- const yLeftAdjustment = Math.min(Math.max(yTopPoint[0] - tickPadding, 0), width);
28625
-
28626
- // We only want to render the 0 label on the y-axis if it is left of the grid
28627
- const yAxisIsLeftOfGrid = yLeftAdjustment === 0;
28628
-
28629
- // If the y-axis is right of the graph's viewbox/range,
28630
- // we want to adjust both the positioning and text alignment
28631
- const yAxisRightOfGrid = yLeftAdjustment === width;
28632
-
28633
- // Determine the default spacing for the labels based on whether the y-axis is right of the grid
28634
- const defaultSpacing = yAxisRightOfGrid ? 0.5 : 0;
28635
-
28636
- // We also want to adjust the spacing if there are no negative y-axis labels as the extra space is not needed
28637
- const marginSpacing = yLowestTick >= 0 ? defaultSpacing * 2 : defaultSpacing;
28638
-
28639
- // Determine the text alignment based on whether the y-axis is right of the grid
28640
- // We need to cast this as a const as it is used in the inline styles
28641
- const textAlign = yAxisRightOfGrid ? "left" : "right";
28642
- const yAxisStyles = {
28643
- left: `calc(${yLeftAdjustment}px - ${marginSpacing}em)`,
28644
- top: `calc(${yTopPoint[1]}px - 0.5em)`,
28645
- height,
28646
- textAlign,
28647
- "--y-axis-label-height": yLabelHeight + "px"
28648
- };
28649
- const classNames = yAxisRightOfGrid ? "y-axis-tick-labels y-axis-right-of-grid" : "y-axis-tick-labels ";
28650
- return /*#__PURE__*/React.createElement("div", {
28651
- className: classNames,
28652
- "data-testid": "y-axis-tick-labels",
28653
- style: yAxisStyles
28654
- }, axisTicks.map(label => {
28655
- return /*#__PURE__*/React.createElement(AxisTickLabel, {
28656
- label: label,
28657
- axis: "y",
28658
- key: `y-grid-tick-${label}`,
28659
- graphConfig: graphConfig,
28660
- axisOutOfBounds: yAxisIsLeftOfGrid
28661
- });
28662
- }));
28663
- };
28664
-
28665
- // Generate the necessary styles and ticks for the x-axis tick labels container
28666
- const XGridAxis = props => {
28667
- const {
28668
- axisTicks,
28669
- graphConfig
28670
- } = props;
28671
- const {
28672
- width,
28673
- height
28674
- } = graphConfig;
28675
-
28676
- // Determine label width by calculating the difference between the first two x-axis labels
28677
- const xFirstPoint = pointToPixel([axisTicks[0], 0], graphConfig);
28678
- const xSecondPoint = pointToPixel([axisTicks[1], 0], graphConfig);
28679
- const xLabelWidth = Math.abs(xSecondPoint[0] - xFirstPoint[0]);
28680
-
28681
- // Determine the top position of the x-axis labels based on
28682
- // whether the x-axis is above, within, or below the graph
28683
- const xAxisAboveGraph = xFirstPoint[1] < 0;
28684
- const xTopAdjustment = xAxisAboveGraph ? 0 : Math.min(xFirstPoint[1] + tickPadding, height);
28685
-
28686
- // Determine whether the x-axis is outside of the graph's viewbox/range
28687
- // This is used to conditionally render the 0 label on the x-axis
28688
- const xAxisOutOfBounds = xTopAdjustment === height || xAxisAboveGraph;
28689
-
28690
- // Determine the available space to the right of the x-axis labels
28691
- const availableSpaceRight = width - xFirstPoint[0];
28692
-
28693
- // Calculate the total combined width of ALL the x-axis labels
28694
- const axisTicksWidth = axisTicks.length * xLabelWidth;
28695
-
28696
- // Determine if we need to increase the width of the axis past the graph width
28697
- const calculatedWidth = axisTicksWidth > width ? axisTicksWidth : width;
28698
-
28699
- // Determine the right adjustment for the x-axis labels based on the available space
28700
- // We're adjusting the right as the flex-direction is row-reverse
28701
- const xRightAdjustment = availableSpaceRight - xLabelWidth / 2;
28702
- const xAxisStyles = {
28703
- top: xTopAdjustment,
28704
- right: xRightAdjustment,
28705
- width: calculatedWidth,
28706
- "--x-axis-label-width": xLabelWidth + "px"
28707
- };
28708
- return /*#__PURE__*/React.createElement("div", {
28709
- className: `x-axis-tick-labels ${xAxisAboveGraph && "x-axis-top-of-grid"}`,
28710
- "data-testid": "x-axis-tick-labels",
28711
- style: xAxisStyles
28712
- }, axisTicks.map(label => {
28713
- return /*#__PURE__*/React.createElement(AxisTickLabel, {
28714
- axis: "x",
28715
- label: label,
28716
- key: `x-grid-tick-${label}`,
28717
- graphConfig: graphConfig,
28718
- axisOutOfBounds: xAxisOutOfBounds
28719
- });
28720
- }));
28721
- };
28722
-
28723
- // Generate the individual tick label elements
28724
- const AxisTickLabel = ({
28725
- label,
28726
- graphConfig,
28727
- axisOutOfBounds,
28728
- axis
28729
- }) => {
28730
- const {
28731
- gridStep,
28732
- tickStep
28733
- } = graphConfig;
28734
- // Determine the point on the axis based on the axis type
28735
- const pointOnAxis = axis === "x" ? [label, 0] : [0, label];
28736
- const pixelPoint = pointToPixel(pointOnAxis, graphConfig);
28737
-
28738
- // Determine the relevant edge of the graph based on the axis type
28739
- const graphEdge = axis === "x" ? graphConfig.width : graphConfig.height;
28740
-
28741
- // Determine the vector index based on the axis type
28742
- const vectorIndex = axis === "x" ? 0 : 1;
28743
-
28744
- // Determine whether or not to render the tick label text
28745
- const shouldShowLabel = showTickLabel({
28746
- label,
28747
- axisOutOfBounds,
28748
- graphEdge,
28749
- position: pixelPoint[vectorIndex],
28750
- gridStep: gridStep[vectorIndex],
28751
- tickStep: tickStep[vectorIndex]
28752
- });
28753
- return /*#__PURE__*/React.createElement("span", {
28754
- className: `${axis}-axis-tick-label`
28755
- }, shouldShowLabel && label);
28756
- };
28757
-
28758
- // Determine whether or not to show the tick label text
28759
- const showTickLabel = ({
28760
- gridStep,
28761
- tickStep,
28762
- label,
28763
- graphEdge,
28764
- position,
28765
- axisOutOfBounds
28766
- }) => {
28767
- let showLabel = true;
28768
-
28769
- // We only want to show the initial negative tick labels (e.g. -1) on each
28770
- // axis if the tickStep > gridStep, to ensure that these labels do not overlap.
28771
- // e.g. If gridStep = 1 and tickStep = 2, there are 2 grid lines for every 1 tick,
28772
- // which allows enough room for both these tick labels to render.
28773
- showLabel = tickStep > gridStep ? true : label !== -tickStep;
28774
-
28775
- // If the tick label position is greater than the relevant edge of the graph, don't render it
28776
- if (position <= -graphEdge || position >= graphEdge) {
28777
- showLabel = false;
28778
- }
28779
-
28780
- // If the label is 0, we only want to show it if the axis is to the left of the graph's viewbox/range
28781
- // otherwise it will overlap with graph axis labels
28782
- if (label === 0 && !axisOutOfBounds) {
28783
- showLabel = false;
28784
- }
28785
- return showLabel;
28786
- };
28787
-
28788
- // Generate the tick labels for an axis based on the tick step
28789
- function generateTickLocations$1(tickStep, min, max) {
28790
- const positiveTicks = [];
28791
- const negativeTicks = [];
28792
- const ticks = [];
28793
-
28794
- // Add 0 if it is applicable.
28795
- // This will be conditionally rendered using showTickLabel.
28796
- if (min <= 0 && max >= 0) {
28797
- positiveTicks.push(0);
28798
- }
28799
-
28800
- // Add ticks on the positive axis
28801
- // Start at the first tick after 0 or the minimum value,
28802
- // taking into account that the graph may be zoomed in on the positive axis
28803
- let i = Math.max(min, 0) + tickStep;
28804
- for (i; min < i && i < max; i += tickStep) {
28805
- positiveTicks.push(i);
28806
- }
28807
-
28808
- // Add ticks on the negative axis
28809
- // Start at the first tick before 0 or the maximum value,
28810
- // taking into account that the graph may be zoomed in on the negative axis
28811
- i = Math.min(max, 0) - tickStep;
28812
- for (i; i > min; i -= tickStep) {
28813
- negativeTicks.push(i);
28814
- }
28815
-
28816
- // Reverse the ticks so that they are in the correct render order
28817
- positiveTicks.reverse();
28818
- ticks.push(...positiveTicks, ...negativeTicks);
28819
- return ticks;
28820
- }
28821
-
28822
- function pathBuilder() {
28823
- return new PathBuilder();
28824
- }
28825
- class PathBuilder {
28826
- constructor() {
28827
- this.path = [];
28828
- this.scaleFactor = 1;
28829
- }
28830
- build() {
28831
- return this.path.map(scaleCommandBy(this.scaleFactor)).map(commandToString).join("");
28832
- }
28833
-
28834
- // add a move (M) command to the path
28835
- move(x, y) {
28836
- this.path.push({
28837
- action: "M",
28838
- args: [x, y]
28839
- });
28840
- return this;
28841
- }
28842
- line(x, y) {
28843
- this.path.push({
28844
- action: "L",
28845
- args: [x, y]
28846
- });
28847
- return this;
28848
- }
28849
- circularArc(radius, toX, toY, {
28850
- sweep = false,
28851
- largeArc = false
28852
- } = {}) {
28853
- this.path.push({
28854
- action: "A",
28855
- args: [radius, radius, 0 /* rotation */, largeArc ? 1 : 0, sweep ? 1 : 0, toX, toY]
28856
- });
28857
- return this;
28858
- }
28859
-
28860
- // add a curve (C) command to the path
28861
- curve(control1X, control1Y, control2X, control2Y, endX, endY) {
28862
- this.path.push({
28863
- action: "C",
28864
- args: [control1X, control1Y, control2X, control2Y, endX, endY]
28865
- });
28866
- return this;
28867
- }
28868
-
28869
- // Scale all coordinates of the path by a factor
28870
- scale(factor) {
28871
- this.scaleFactor *= factor;
28872
- return this;
28873
- }
28874
- }
28875
- function commandToString(command) {
28876
- return `${command.action}${command.args.join(" ")}`;
28877
- }
28878
- function scaleCommandBy(scaleFactor) {
28879
- return command => {
28880
- switch (command.action) {
28881
- case "A":
28882
- // TODO(benchristel): do we want to refactor this switch to
28883
- // use polymorphism?
28884
- // Arc command
28885
- return _extends({}, command, {
28886
- args: [command.args[0] * scaleFactor,
28887
- // x radius
28888
- command.args[1] * scaleFactor,
28889
- // y radius
28890
- command.args[2],
28891
- // rotation
28892
- command.args[3],
28893
- // largeArc flag
28894
- command.args[4],
28895
- // sweep flag
28896
- command.args[5] * scaleFactor,
28897
- // end x
28898
- command.args[6] * scaleFactor // end y
28899
- ]
28900
- });
28901
-
28902
- default:
28903
- return _extends({}, command, {
28904
- args: command.args.map(c => c * scaleFactor)
28905
- });
28906
- }
28907
- };
28908
- }
28909
-
28910
28627
  // We use the pathBuilder here to scale up the SVG path coordinates used
28911
28628
  // elsewhere for arrowheads. We scale in the path itself instead of using a CSS
28912
28629
  // transform because a transform would also scale up the stroke weight, and we
@@ -28937,7 +28654,10 @@ function AxisArrows() {
28937
28654
  range: [[xMin, xMax], [yMin, yMax]]
28938
28655
  } = useGraphConfig();
28939
28656
  const axisColor = "var(--mafs-fg)";
28940
- return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(Arrowhead, {
28657
+
28658
+ // Only render the arrows if the axis is within the visible range
28659
+ // as otherwise the arrows will be rendered outside the graph
28660
+ return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, !(yMin > 0 || yMax < 0) && /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(Arrowhead, {
28941
28661
  color: axisColor,
28942
28662
  tip: [xMin, 0],
28943
28663
  angle: 180
@@ -28945,7 +28665,7 @@ function AxisArrows() {
28945
28665
  color: axisColor,
28946
28666
  tip: [xMax, 0],
28947
28667
  angle: 0
28948
- }), /*#__PURE__*/React__default.createElement(Arrowhead, {
28668
+ })), !(xMin > 0 || xMax < 0) && /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(Arrowhead, {
28949
28669
  color: axisColor,
28950
28670
  tip: [0, yMin],
28951
28671
  angle: 90
@@ -28953,38 +28673,75 @@ function AxisArrows() {
28953
28673
  color: axisColor,
28954
28674
  tip: [0, yMax],
28955
28675
  angle: 270
28956
- }));
28676
+ })));
28677
+ }
28678
+
28679
+ function AxisLabels() {
28680
+ const {
28681
+ range,
28682
+ labels,
28683
+ width,
28684
+ height
28685
+ } = useGraphConfig();
28686
+ const yAxisLabelLocation = [0, range[Y][MAX]];
28687
+ const xAxisLabelLocation = [range[X][MAX], 0];
28688
+ const [xAxisLabelText, yAxisLabelText] = labels;
28689
+ const graphInfo = {
28690
+ range,
28691
+ width,
28692
+ height
28693
+ };
28694
+ const [x1, y1] = pointToPixel(xAxisLabelLocation, graphInfo);
28695
+ // The default location for the y-axis-label is at the maximum y point
28696
+ // This is just underneath the tick line for that point. The -24 moves the
28697
+ // label up one grid square, so it sits on top of the graph
28698
+ const [x2, y2] = vec.add(pointToPixel(yAxisLabelLocation, graphInfo), [0, -24]);
28699
+ const {
28700
+ TeX
28701
+ } = getDependencies();
28702
+ return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement("span", {
28703
+ style: {
28704
+ position: "absolute",
28705
+ left: x1,
28706
+ top: y1,
28707
+ fontSize: "14px",
28708
+ transform: "translate(7px, -50%)"
28709
+ }
28710
+ }, /*#__PURE__*/React__default.createElement(TeX, null, xAxisLabelText)), /*#__PURE__*/React__default.createElement("span", {
28711
+ style: {
28712
+ position: "absolute",
28713
+ left: x2,
28714
+ top: y2,
28715
+ fontSize: "14px",
28716
+ transform: "translate(-50%, 0px)"
28717
+ }
28718
+ }, /*#__PURE__*/React__default.createElement(TeX, null, yAxisLabelText)));
28957
28719
  }
28958
28720
 
28721
+ // The size of the ticks and labels in pixels
28959
28722
  const tickSize = 10;
28960
- const tickStyle = {
28961
- stroke: "black",
28962
- strokeWidth: 1
28963
- };
28723
+ const tickLabelSize = 14;
28964
28724
  const YGridTick = ({
28965
28725
  y,
28966
- graphInfo
28726
+ range
28967
28727
  }) => {
28728
+ // If the graph requires out-of-bounds labels, we want to make sure to set the
28729
+ // coordinates to the edge of the visible range of the graph. Otherwise,
28730
+ // the ticks and labels would render outside of the clipping-mask.
28968
28731
  let xPointOnAxis = 0;
28969
-
28970
- // If the graph is zoomed in, we want to make sure the ticks are still visible
28971
- // even if they are outside the graph's range.
28972
- if (graphInfo.range[X][MIN] > 0) {
28732
+ if (range[X][MIN] > 0) {
28973
28733
  // If the graph is on the positive side of the x-axis, lock the ticks to the left side of the graph
28974
- xPointOnAxis = graphInfo.range[X][MIN];
28734
+ xPointOnAxis = range[X][MIN];
28975
28735
  }
28976
- if (graphInfo.range[X][MAX] < 0) {
28736
+ if (range[X][MAX] < 0) {
28977
28737
  // If the graph is on the negative side of the x-axis, lock the ticks to the right side of the graph
28978
- xPointOnAxis = graphInfo.range[X][MAX];
28738
+ xPointOnAxis = range[X][MAX];
28979
28739
  }
28740
+
28741
+ // Convert the Vector2 coordinates to pixel coordinates
28980
28742
  const pointOnAxis = [xPointOnAxis, y];
28981
28743
  const [[xPosition, yPosition]] = useTransformVectorsToPixels(pointOnAxis);
28982
28744
 
28983
- // If the tick is on the edge of the graph's range, don't render it
28984
- if (yPosition === -graphInfo.height || yPosition === graphInfo.height + 20) {
28985
- return null;
28986
- }
28987
-
28988
28745
  // Position of the start of the tick
28989
28746
  const x1 = xPosition - tickSize / 2;
28990
28747
  const y1 = yPosition;
@@ -28992,39 +28749,55 @@ const YGridTick = ({
28992
28749
  // Position of the end of the tick
28993
28750
  const x2 = xPosition + tickSize / 2;
28994
28751
  const y2 = yPosition;
28752
+
28753
+ // Adjust the y position of the x-axis labels based on
28754
+ // whether the x-axis is above, within, or below the graph
28755
+ const xAdjustment = range[X][MAX] <= 0 ? tickLabelSize * 1.5 : -tickLabelSize * 1.1;
28756
+ const xPositionText = xPosition + xAdjustment;
28757
+ const yPositionText = yPosition + tickLabelSize * 0.25; // Center the text vertically on the tick
28758
+
28759
+ // If the graph displays both the y and x axis lines within the graph, we want
28760
+ // to hide the label at -1 on the y-axis to prevent overlap with the x-axis label
28761
+ const showLabel = shouldShowLabel(y, range);
28995
28762
  return /*#__PURE__*/React.createElement("g", {
28996
- className: "y-axis-ticks"
28763
+ className: "tick"
28997
28764
  }, /*#__PURE__*/React.createElement("line", {
28998
28765
  x1: x1,
28999
28766
  y1: y1,
29000
28767
  x2: x2,
29001
28768
  y2: y2,
29002
- style: tickStyle
29003
- }));
28769
+ className: "axis-tick"
28770
+ }), showLabel && /*#__PURE__*/React.createElement("text", {
28771
+ className: "axis-tick-label",
28772
+ style: {
28773
+ fontSize: tickLabelSize
28774
+ },
28775
+ textAnchor: "end",
28776
+ x: xPositionText,
28777
+ y: yPositionText
28778
+ }, y.toString()));
29004
28779
  };
29005
28780
  const XGridTick = ({
29006
28781
  x,
29007
- graphInfo
28782
+ range
29008
28783
  }) => {
28784
+ // If the graph requires out-of-bounds labels, we want to make sure to set the
28785
+ // coordinates to the edge of the visible range of the graph. Otherwise,
28786
+ // the ticks and labels would render outside of the clipping-mask.
29009
28787
  let yPointOnAxis = 0;
29010
- // If the graph is zoomed in, we want to make sure the ticks are still visible
29011
- // even if they are outside the graph's range.
29012
- if (graphInfo.range[Y][MIN] > 0) {
28788
+ if (range[Y][MIN] > 0) {
29013
28789
  // If the graph is on the positive side of the y-axis, lock the ticks to the top of the graph
29014
- yPointOnAxis = graphInfo.range[Y][MIN];
28790
+ yPointOnAxis = range[Y][MIN];
29015
28791
  }
29016
- if (graphInfo.range[Y][MAX] < 0) {
28792
+ if (range[Y][MAX] < 0) {
29017
28793
  // If the graph is on the negative side of the x-axis, lock the ticks to the bottom of the graph
29018
- yPointOnAxis = graphInfo.range[Y][MAX];
28794
+ yPointOnAxis = range[Y][MAX];
29019
28795
  }
28796
+
28797
+ // Convert the Vector2 coordinates to pixel coordinates
29020
28798
  const pointOnAxis = [x, yPointOnAxis];
29021
28799
  const [[xPosition, yPosition]] = useTransformVectorsToPixels(pointOnAxis);
29022
28800
 
29023
- // If the tick is on the edge of the graph's range, don't render it
29024
- if (xPosition === -graphInfo.width / 2 || xPosition === graphInfo.width) {
29025
- return null;
29026
- }
29027
-
29028
28801
  // Position of the start of the tick
29029
28802
  const x1 = xPosition;
29030
28803
  const y1 = yPosition + tickSize / 2;
@@ -29032,21 +28805,56 @@ const XGridTick = ({
29032
28805
  // Position of the end of the tick
29033
28806
  const x2 = xPosition;
29034
28807
  const y2 = yPosition - tickSize / 2;
28808
+
28809
+ // Adjust the Y position of the x-axis labels based on
28810
+ // whether the x-axis is above, within, or below the graph
28811
+ const yAdjustment = range[Y][MAX] < 0 ? -tickLabelSize : tickLabelSize * 1.75;
28812
+
28813
+ // Adjust the X position of the x-axis labels based on
28814
+ // whether the label is positive or negative, in order to
28815
+ // account for the width of the negative sign
28816
+ const xAdjustment = x < 0 ? -2 : 0;
28817
+
28818
+ // Apply the adjustments to the x and y positions for the text
28819
+ const xPositionText = xPosition + xAdjustment;
28820
+ const yPositionText = yPosition + yAdjustment;
29035
28821
  return /*#__PURE__*/React.createElement("g", {
29036
- className: "x-axis-ticks"
28822
+ className: "tick"
29037
28823
  }, /*#__PURE__*/React.createElement("line", {
29038
28824
  x1: x1,
29039
28825
  y1: y1,
29040
28826
  x2: x2,
29041
28827
  y2: y2,
29042
- style: tickStyle
29043
- }));
28828
+ className: "axis-tick"
28829
+ }), /*#__PURE__*/React.createElement("text", {
28830
+ className: "axis-tick-label",
28831
+ style: {
28832
+ fontSize: tickLabelSize
28833
+ },
28834
+ textAnchor: "middle",
28835
+ x: xPositionText,
28836
+ y: yPositionText
28837
+ }, x.toString()));
28838
+ };
28839
+
28840
+ // Determines whether to show the label for the given tick
28841
+ // Currently, the only condition is to hide the label at -1
28842
+ // on the y-axis when the x-axis is within the graph
28843
+ const shouldShowLabel = (number, range) => {
28844
+ let showLabel = true;
28845
+
28846
+ // If the x-axis is within the graph and the y-axis is at -1, hide the label
28847
+ if (range[X][MIN] < -1 && range[X][MAX] > 0 && number === -1) {
28848
+ showLabel = false;
28849
+ }
28850
+ return showLabel;
29044
28851
  };
29045
28852
  function generateTickLocations(tickStep, min, max) {
29046
28853
  const ticks = [];
29047
28854
 
29048
28855
  // Add ticks in the positive direction
29049
- for (let _i = 0 + tickStep; _i < max; _i += tickStep) {
28856
+ const start = Math.max(min, 0);
28857
+ for (let _i = start + tickStep; _i < max; _i += tickStep) {
29050
28858
  ticks.push(_i);
29051
28859
  }
29052
28860
 
@@ -29061,34 +28869,34 @@ function generateTickLocations(tickStep, min, max) {
29061
28869
  const AxisTicks = () => {
29062
28870
  const {
29063
28871
  tickStep,
29064
- range,
29065
- width,
29066
- height
28872
+ range
29067
28873
  } = useGraphConfig();
29068
- const graphInfo = {
29069
- range,
29070
- width,
29071
- height
29072
- };
29073
28874
  const [[xMin, xMax], [yMin, yMax]] = range;
29074
28875
  const [xTickStep, yTickStep] = tickStep;
28876
+
28877
+ // Generate the tick locations & labels for the x and y axes
29075
28878
  const yGridTicks = generateTickLocations(yTickStep, yMin, yMax);
29076
28879
  const xGridTicks = generateTickLocations(xTickStep, xMin, xMax);
29077
28880
  return /*#__PURE__*/React.createElement("g", {
29078
- className: "axis-ticks"
28881
+ className: "axis-ticks",
28882
+ role: "presentation"
28883
+ }, /*#__PURE__*/React.createElement("g", {
28884
+ className: "y-axis-ticks"
29079
28885
  }, yGridTicks.map(y => {
29080
28886
  return /*#__PURE__*/React.createElement(YGridTick, {
29081
28887
  y: y,
29082
28888
  key: `y-grid-tick-${y}`,
29083
- graphInfo: graphInfo
28889
+ range: range
29084
28890
  });
29085
- }), xGridTicks.map(x => {
28891
+ })), /*#__PURE__*/React.createElement("g", {
28892
+ className: "x-axis-ticks"
28893
+ }, xGridTicks.map(x => {
29086
28894
  return /*#__PURE__*/React.createElement(XGridTick, {
29087
28895
  x: x,
29088
28896
  key: `x-grid-tick-${x}`,
29089
- graphInfo: graphInfo
28897
+ range: range
29090
28898
  });
29091
- }));
28899
+ })));
29092
28900
  };
29093
28901
 
29094
28902
  /**
@@ -29109,12 +28917,10 @@ const axisOptions = (props, axisIndex) => {
29109
28917
  };
29110
28918
  };
29111
28919
  const Grid = props => {
29112
- return props.markings === "none" ? null : /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Coordinates.Cartesian, {
28920
+ return props.markings === "none" ? null : /*#__PURE__*/React.createElement(Coordinates.Cartesian, {
29113
28921
  xAxis: axisOptions(props, X),
29114
28922
  yAxis: axisOptions(props, Y)
29115
- }),
29116
- // Only render the axis ticks and arrows if the markings are set to a full "graph"
29117
- props.markings === "graph" && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(AxisTicks, null), /*#__PURE__*/React.createElement(AxisArrows, null)));
28923
+ });
29118
28924
  };
29119
28925
 
29120
28926
  // Displays a stylized open eye: lint warnings are visible
@@ -32035,6 +31841,22 @@ const MafsGraph = props => {
32035
31841
  const uniqueId = React.useId();
32036
31842
  const descriptionId = `interactive-graph-description-${uniqueId}`;
32037
31843
  const graphRef = React.useRef(null);
31844
+
31845
+ // Set up the SVG attributes for the nested SVGs that help lock
31846
+ // the grid and graph elements to the bounds of the graph.
31847
+ const {
31848
+ viewboxX,
31849
+ viewboxY
31850
+ } = calculateNestedSVGCoords(state.range, width, height);
31851
+ const viewBox = `${viewboxX} ${viewboxY} ${width} ${height}`;
31852
+ const nestedSVGAttributes = {
31853
+ width,
31854
+ height,
31855
+ viewBox,
31856
+ preserveAspectRatio: "xMidYMin",
31857
+ x: viewboxX,
31858
+ y: viewboxY
31859
+ };
32038
31860
  return /*#__PURE__*/React.createElement(GraphConfigContext.Provider, {
32039
31861
  value: {
32040
31862
  range: state.range,
@@ -32056,7 +31878,7 @@ const MafsGraph = props => {
32056
31878
  padding: "25px 25px 0 0",
32057
31879
  boxSizing: "content-box",
32058
31880
  marginLeft: "20px",
32059
- marginBottom: "20px",
31881
+ marginBottom: "30px",
32060
31882
  pointerEvents: props.static ? "none" : "auto",
32061
31883
  userSelect: "none",
32062
31884
  width,
@@ -32092,7 +31914,7 @@ const MafsGraph = props => {
32092
31914
  bottom: 0,
32093
31915
  left: 0
32094
31916
  }
32095
- }, props.markings === "graph" && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(AxisLabels, null), /*#__PURE__*/React.createElement(AxisTickLabels, null)), /*#__PURE__*/React.createElement(Mafs, {
31917
+ }, props.markings === "graph" && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(AxisLabels, null)), /*#__PURE__*/React.createElement(Mafs, {
32096
31918
  preserveAspectRatio: false,
32097
31919
  viewBox: {
32098
31920
  x: state.range[X],
@@ -32103,19 +31925,22 @@ const MafsGraph = props => {
32103
31925
  zoom: false,
32104
31926
  width: width,
32105
31927
  height: height
32106
- }, /*#__PURE__*/React.createElement(SvgDefs, null), /*#__PURE__*/React.createElement(Grid, {
32107
- tickStep: props.step,
31928
+ }, /*#__PURE__*/React.createElement(SvgDefs, null), /*#__PURE__*/React.createElement("svg", nestedSVGAttributes, /*#__PURE__*/React.createElement(Grid, {
32108
31929
  gridStep: props.gridStep,
32109
31930
  range: state.range,
32110
31931
  containerSizeClass: props.containerSizeClass,
32111
- markings: props.markings
32112
- }), props.lockedFigures && /*#__PURE__*/React.createElement(GraphLockedLayer, {
31932
+ markings: props.markings,
31933
+ width: width,
31934
+ height: height
31935
+ })),
31936
+ // Only render the axis ticks and arrows if the markings are set to a full "graph"
31937
+ props.markings === "graph" && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(AxisTicks, null), /*#__PURE__*/React.createElement(AxisArrows, null)), /*#__PURE__*/React.createElement("svg", nestedSVGAttributes, props.lockedFigures && /*#__PURE__*/React.createElement(GraphLockedLayer, {
32113
31938
  lockedFigures: props.lockedFigures,
32114
31939
  range: state.range
32115
31940
  }), props.showProtractor && /*#__PURE__*/React.createElement(Protractor, null), renderGraph({
32116
31941
  state,
32117
31942
  dispatch
32118
- })))), renderGraphControls({
31943
+ }))))), renderGraphControls({
32119
31944
  state,
32120
31945
  dispatch
32121
31946
  })));
@@ -32151,6 +31976,56 @@ const renderGraphControls = props => {
32151
31976
  return null;
32152
31977
  }
32153
31978
  };
31979
+
31980
+ // Calculate the difference between the min and max values of a range
31981
+ const getRangeDiff = range => {
31982
+ const [min, max] = range;
31983
+ return Math.abs(max - min);
31984
+ };
31985
+
31986
+ // We need to adjust the nested SVG viewbox x and Y values based on the range of the graph in order
31987
+ // to ensure that the graph is sized and positioned correctly within the Mafs SVG and the clipping mask.
31988
+ // Exported for testing.
31989
+ const calculateNestedSVGCoords = (range, width, height) => {
31990
+ // X RANGE
31991
+ let viewboxX = 0; // When xMin is 0, we want to use 0 as the viewboxX value
31992
+ const totalXRange = getRangeDiff(range[X]);
31993
+ const gridCellWidth = width / totalXRange;
31994
+ const minX = range[X][MIN];
31995
+
31996
+ // If xMin is entirely positive, we need to adjust the
31997
+ // viewboxX to be the grid cell width multiplied by xMin
31998
+ if (minX > 0) {
31999
+ viewboxX = gridCellWidth * Math.abs(minX);
32000
+ }
32001
+ // If xMin is negative, we need to adjust the viewboxX to be
32002
+ // the negative value of the grid cell width multiplied by xMin
32003
+ if (minX < 0) {
32004
+ viewboxX = -gridCellWidth * Math.abs(minX);
32005
+ }
32006
+
32007
+ // Y RANGE
32008
+ let viewboxY = -height; // When yMin is 0, we want to use the negative value of the graph height
32009
+ const totalYRange = getRangeDiff(range[Y]);
32010
+ const gridCellHeight = height / totalYRange;
32011
+ const minY = range[Y][MIN];
32012
+
32013
+ // If the y range is entirely positive, we want a negative sum of the
32014
+ // height and the gridcell height multiplied by the absolute value of yMin
32015
+ if (minY > 0) {
32016
+ viewboxY = -height - gridCellHeight * Math.abs(minY);
32017
+ }
32018
+
32019
+ // If the yMin is negative, we want to multiply the gridcell height
32020
+ // by the absolute value of yMin, and subtract the full height of the graph
32021
+ if (minY < 0) {
32022
+ viewboxY = gridCellHeight * Math.abs(minY) - height;
32023
+ }
32024
+ return {
32025
+ viewboxX,
32026
+ viewboxY
32027
+ };
32028
+ };
32154
32029
  const renderGraph = props => {
32155
32030
  const {
32156
32031
  state,
@@ -44741,7 +44616,7 @@ const init = function init(options) {
44741
44616
 
44742
44617
  // This file is processed by a Rollup plugin (replace) to inject the production
44743
44618
  const libName = "@khanacademy/perseus";
44744
- const libVersion = "33.0.0";
44619
+ const libVersion = "33.1.0";
44745
44620
  addLibraryVersionToPerseusDebug(libName, libVersion);
44746
44621
 
44747
44622
  const apiVersion = {