@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/index.js CHANGED
@@ -28617,6 +28617,95 @@ class Graph extends React__namespace.Component {
28617
28617
  }
28618
28618
  }
28619
28619
 
28620
+ function pathBuilder() {
28621
+ return new PathBuilder();
28622
+ }
28623
+ class PathBuilder {
28624
+ path = [];
28625
+ scaleFactor = 1;
28626
+ build() {
28627
+ return this.path.map(scaleCommandBy(this.scaleFactor)).map(commandToString).join("");
28628
+ }
28629
+
28630
+ // add a move (M) command to the path
28631
+ move(x, y) {
28632
+ this.path.push({
28633
+ action: "M",
28634
+ args: [x, y]
28635
+ });
28636
+ return this;
28637
+ }
28638
+ line(x, y) {
28639
+ this.path.push({
28640
+ action: "L",
28641
+ args: [x, y]
28642
+ });
28643
+ return this;
28644
+ }
28645
+ circularArc(radius, toX, toY) {
28646
+ let {
28647
+ sweep = false,
28648
+ largeArc = false
28649
+ } = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
28650
+ this.path.push({
28651
+ action: "A",
28652
+ args: [radius, radius, 0 /* rotation */, largeArc ? 1 : 0, sweep ? 1 : 0, toX, toY]
28653
+ });
28654
+ return this;
28655
+ }
28656
+
28657
+ // add a curve (C) command to the path
28658
+ curve(control1X, control1Y, control2X, control2Y, endX, endY) {
28659
+ this.path.push({
28660
+ action: "C",
28661
+ args: [control1X, control1Y, control2X, control2Y, endX, endY]
28662
+ });
28663
+ return this;
28664
+ }
28665
+
28666
+ // Scale all coordinates of the path by a factor
28667
+ scale(factor) {
28668
+ this.scaleFactor *= factor;
28669
+ return this;
28670
+ }
28671
+ }
28672
+ function commandToString(command) {
28673
+ return `${command.action}${command.args.join(" ")}`;
28674
+ }
28675
+ function scaleCommandBy(scaleFactor) {
28676
+ return command => {
28677
+ switch (command.action) {
28678
+ case "A":
28679
+ // TODO(benchristel): do we want to refactor this switch to
28680
+ // use polymorphism?
28681
+ // Arc command
28682
+ return {
28683
+ ...command,
28684
+ args: [command.args[0] * scaleFactor,
28685
+ // x radius
28686
+ command.args[1] * scaleFactor,
28687
+ // y radius
28688
+ command.args[2],
28689
+ // rotation
28690
+ command.args[3],
28691
+ // largeArc flag
28692
+ command.args[4],
28693
+ // sweep flag
28694
+ command.args[5] * scaleFactor,
28695
+ // end x
28696
+ command.args[6] * scaleFactor // end y
28697
+ ]
28698
+ };
28699
+
28700
+ default:
28701
+ return {
28702
+ ...command,
28703
+ args: command.args.map(c => c * scaleFactor)
28704
+ };
28705
+ }
28706
+ };
28707
+ }
28708
+
28620
28709
  // Restricts `value` to be between `min` and `max`.
28621
28710
  function clamp(value, min, max) {
28622
28711
  if (value < min) {
@@ -28846,380 +28935,6 @@ function pixelsToVectors(pixels, graphState) {
28846
28935
  });
28847
28936
  }
28848
28937
 
28849
- function AxisLabels() {
28850
- const {
28851
- range,
28852
- labels,
28853
- width,
28854
- height
28855
- } = useGraphConfig();
28856
- const yAxisLabelLocation = [0, range[Y][MAX]];
28857
- const xAxisLabelLocation = [range[X][MAX], 0];
28858
- const [xAxisLabelText, yAxisLabelText] = labels;
28859
- const graphInfo = {
28860
- range,
28861
- width,
28862
- height
28863
- };
28864
- const [x1, y1] = pointToPixel(xAxisLabelLocation, graphInfo);
28865
- // The default location for the y-axis-label is at the maximum y point
28866
- // This is just underneath the tick line for that point. The -24 moves the
28867
- // label up one grid square, so it sits on top of the graph
28868
- const [x2, y2] = mafs.vec.add(pointToPixel(yAxisLabelLocation, graphInfo), [0, -24]);
28869
- const {
28870
- TeX
28871
- } = getDependencies();
28872
- return /*#__PURE__*/React__default["default"].createElement(React__default["default"].Fragment, null, /*#__PURE__*/React__default["default"].createElement("span", {
28873
- style: {
28874
- position: "absolute",
28875
- left: x1,
28876
- top: y1,
28877
- fontSize: "14px",
28878
- transform: "translate(7px, -50%)"
28879
- }
28880
- }, /*#__PURE__*/React__default["default"].createElement(TeX, null, xAxisLabelText)), /*#__PURE__*/React__default["default"].createElement("span", {
28881
- style: {
28882
- position: "absolute",
28883
- left: x2,
28884
- top: y2,
28885
- fontSize: "14px",
28886
- transform: "translate(-50%, 0px)"
28887
- }
28888
- }, /*#__PURE__*/React__default["default"].createElement(TeX, null, yAxisLabelText)));
28889
- }
28890
-
28891
- // Universal padding for the axis ticks to ensure that the labels
28892
- // have enough space to render without overlapping the tick lines
28893
- const tickPadding = 5;
28894
- const AxisTickLabels = () => {
28895
- const graphConfig = useGraphConfig();
28896
- const {
28897
- tickStep,
28898
- range
28899
- } = graphConfig;
28900
- const [[xMin, xMax], [yMin, yMax]] = range;
28901
- const [xTickStep, yTickStep] = tickStep;
28902
- const yGridTicks = generateTickLocations$1(yTickStep, yMin, yMax);
28903
- const xGridTicks = generateTickLocations$1(xTickStep, xMin, xMax);
28904
- return /*#__PURE__*/React__namespace.createElement("div", {
28905
- className: "axis-tick-labels"
28906
- }, /*#__PURE__*/React__namespace.createElement(YGridAxis, {
28907
- axisTicks: yGridTicks,
28908
- graphConfig: graphConfig
28909
- }), /*#__PURE__*/React__namespace.createElement(XGridAxis, {
28910
- axisTicks: xGridTicks,
28911
- graphConfig: graphConfig
28912
- }));
28913
- };
28914
-
28915
- // Generate the necessary styles and ticks for the y-axis tick labels container
28916
- const YGridAxis = props => {
28917
- const {
28918
- axisTicks,
28919
- graphConfig
28920
- } = props;
28921
- const {
28922
- width,
28923
- height
28924
- } = graphConfig;
28925
-
28926
- // First get the label height by calculating the difference between the first two y-axis labels
28927
- const yTopPoint = pointToPixel([0, axisTicks[0]], graphConfig);
28928
- const ySecondPoint = pointToPixel([0, axisTicks[1]], graphConfig);
28929
- const yLabelHeight = ySecondPoint[1] - yTopPoint[1];
28930
-
28931
- // When rendering purely positive y-axis labels, we want to adjust the left position
28932
- // from the lowest calculated tick, as yMin may be negative
28933
- const yLowestTick = axisTicks[axisTicks.length - 1];
28934
- const yLeftAdjustment = Math.min(Math.max(yTopPoint[0] - tickPadding, 0), width);
28935
-
28936
- // We only want to render the 0 label on the y-axis if it is left of the grid
28937
- const yAxisIsLeftOfGrid = yLeftAdjustment === 0;
28938
-
28939
- // If the y-axis is right of the graph's viewbox/range,
28940
- // we want to adjust both the positioning and text alignment
28941
- const yAxisRightOfGrid = yLeftAdjustment === width;
28942
-
28943
- // Determine the default spacing for the labels based on whether the y-axis is right of the grid
28944
- const defaultSpacing = yAxisRightOfGrid ? 0.5 : 0;
28945
-
28946
- // We also want to adjust the spacing if there are no negative y-axis labels as the extra space is not needed
28947
- const marginSpacing = yLowestTick >= 0 ? defaultSpacing * 2 : defaultSpacing;
28948
-
28949
- // Determine the text alignment based on whether the y-axis is right of the grid
28950
- // We need to cast this as a const as it is used in the inline styles
28951
- const textAlign = yAxisRightOfGrid ? "left" : "right";
28952
- const yAxisStyles = {
28953
- left: `calc(${yLeftAdjustment}px - ${marginSpacing}em)`,
28954
- top: `calc(${yTopPoint[1]}px - 0.5em)`,
28955
- height,
28956
- textAlign,
28957
- "--y-axis-label-height": yLabelHeight + "px"
28958
- };
28959
- const classNames = yAxisRightOfGrid ? "y-axis-tick-labels y-axis-right-of-grid" : "y-axis-tick-labels ";
28960
- return /*#__PURE__*/React__namespace.createElement("div", {
28961
- className: classNames,
28962
- "data-testid": "y-axis-tick-labels",
28963
- style: yAxisStyles
28964
- }, axisTicks.map(label => {
28965
- return /*#__PURE__*/React__namespace.createElement(AxisTickLabel, {
28966
- label: label,
28967
- axis: "y",
28968
- key: `y-grid-tick-${label}`,
28969
- graphConfig: graphConfig,
28970
- axisOutOfBounds: yAxisIsLeftOfGrid
28971
- });
28972
- }));
28973
- };
28974
-
28975
- // Generate the necessary styles and ticks for the x-axis tick labels container
28976
- const XGridAxis = props => {
28977
- const {
28978
- axisTicks,
28979
- graphConfig
28980
- } = props;
28981
- const {
28982
- width,
28983
- height
28984
- } = graphConfig;
28985
-
28986
- // Determine label width by calculating the difference between the first two x-axis labels
28987
- const xFirstPoint = pointToPixel([axisTicks[0], 0], graphConfig);
28988
- const xSecondPoint = pointToPixel([axisTicks[1], 0], graphConfig);
28989
- const xLabelWidth = Math.abs(xSecondPoint[0] - xFirstPoint[0]);
28990
-
28991
- // Determine the top position of the x-axis labels based on
28992
- // whether the x-axis is above, within, or below the graph
28993
- const xAxisAboveGraph = xFirstPoint[1] < 0;
28994
- const xTopAdjustment = xAxisAboveGraph ? 0 : Math.min(xFirstPoint[1] + tickPadding, height);
28995
-
28996
- // Determine whether the x-axis is outside of the graph's viewbox/range
28997
- // This is used to conditionally render the 0 label on the x-axis
28998
- const xAxisOutOfBounds = xTopAdjustment === height || xAxisAboveGraph;
28999
-
29000
- // Determine the available space to the right of the x-axis labels
29001
- const availableSpaceRight = width - xFirstPoint[0];
29002
-
29003
- // Calculate the total combined width of ALL the x-axis labels
29004
- const axisTicksWidth = axisTicks.length * xLabelWidth;
29005
-
29006
- // Determine if we need to increase the width of the axis past the graph width
29007
- const calculatedWidth = axisTicksWidth > width ? axisTicksWidth : width;
29008
-
29009
- // Determine the right adjustment for the x-axis labels based on the available space
29010
- // We're adjusting the right as the flex-direction is row-reverse
29011
- const xRightAdjustment = availableSpaceRight - xLabelWidth / 2;
29012
- const xAxisStyles = {
29013
- top: xTopAdjustment,
29014
- right: xRightAdjustment,
29015
- width: calculatedWidth,
29016
- "--x-axis-label-width": xLabelWidth + "px"
29017
- };
29018
- return /*#__PURE__*/React__namespace.createElement("div", {
29019
- className: `x-axis-tick-labels ${xAxisAboveGraph && "x-axis-top-of-grid"}`,
29020
- "data-testid": "x-axis-tick-labels",
29021
- style: xAxisStyles
29022
- }, axisTicks.map(label => {
29023
- return /*#__PURE__*/React__namespace.createElement(AxisTickLabel, {
29024
- axis: "x",
29025
- label: label,
29026
- key: `x-grid-tick-${label}`,
29027
- graphConfig: graphConfig,
29028
- axisOutOfBounds: xAxisOutOfBounds
29029
- });
29030
- }));
29031
- };
29032
-
29033
- // Generate the individual tick label elements
29034
- const AxisTickLabel = _ref => {
29035
- let {
29036
- label,
29037
- graphConfig,
29038
- axisOutOfBounds,
29039
- axis
29040
- } = _ref;
29041
- const {
29042
- gridStep,
29043
- tickStep
29044
- } = graphConfig;
29045
- // Determine the point on the axis based on the axis type
29046
- const pointOnAxis = axis === "x" ? [label, 0] : [0, label];
29047
- const pixelPoint = pointToPixel(pointOnAxis, graphConfig);
29048
-
29049
- // Determine the relevant edge of the graph based on the axis type
29050
- const graphEdge = axis === "x" ? graphConfig.width : graphConfig.height;
29051
-
29052
- // Determine the vector index based on the axis type
29053
- const vectorIndex = axis === "x" ? 0 : 1;
29054
-
29055
- // Determine whether or not to render the tick label text
29056
- const shouldShowLabel = showTickLabel({
29057
- label,
29058
- axisOutOfBounds,
29059
- graphEdge,
29060
- position: pixelPoint[vectorIndex],
29061
- gridStep: gridStep[vectorIndex],
29062
- tickStep: tickStep[vectorIndex]
29063
- });
29064
- return /*#__PURE__*/React__namespace.createElement("span", {
29065
- className: `${axis}-axis-tick-label`
29066
- }, shouldShowLabel && label);
29067
- };
29068
-
29069
- // Determine whether or not to show the tick label text
29070
- const showTickLabel = _ref2 => {
29071
- let {
29072
- gridStep,
29073
- tickStep,
29074
- label,
29075
- graphEdge,
29076
- position,
29077
- axisOutOfBounds
29078
- } = _ref2;
29079
- let showLabel = true;
29080
-
29081
- // We only want to show the initial negative tick labels (e.g. -1) on each
29082
- // axis if the tickStep > gridStep, to ensure that these labels do not overlap.
29083
- // e.g. If gridStep = 1 and tickStep = 2, there are 2 grid lines for every 1 tick,
29084
- // which allows enough room for both these tick labels to render.
29085
- showLabel = tickStep > gridStep ? true : label !== -tickStep;
29086
-
29087
- // If the tick label position is greater than the relevant edge of the graph, don't render it
29088
- if (position <= -graphEdge || position >= graphEdge) {
29089
- showLabel = false;
29090
- }
29091
-
29092
- // If the label is 0, we only want to show it if the axis is to the left of the graph's viewbox/range
29093
- // otherwise it will overlap with graph axis labels
29094
- if (label === 0 && !axisOutOfBounds) {
29095
- showLabel = false;
29096
- }
29097
- return showLabel;
29098
- };
29099
-
29100
- // Generate the tick labels for an axis based on the tick step
29101
- function generateTickLocations$1(tickStep, min, max) {
29102
- const positiveTicks = [];
29103
- const negativeTicks = [];
29104
- const ticks = [];
29105
-
29106
- // Add 0 if it is applicable.
29107
- // This will be conditionally rendered using showTickLabel.
29108
- if (min <= 0 && max >= 0) {
29109
- positiveTicks.push(0);
29110
- }
29111
-
29112
- // Add ticks on the positive axis
29113
- // Start at the first tick after 0 or the minimum value,
29114
- // taking into account that the graph may be zoomed in on the positive axis
29115
- let i = Math.max(min, 0) + tickStep;
29116
- for (i; min < i && i < max; i += tickStep) {
29117
- positiveTicks.push(i);
29118
- }
29119
-
29120
- // Add ticks on the negative axis
29121
- // Start at the first tick before 0 or the maximum value,
29122
- // taking into account that the graph may be zoomed in on the negative axis
29123
- i = Math.min(max, 0) - tickStep;
29124
- for (i; i > min; i -= tickStep) {
29125
- negativeTicks.push(i);
29126
- }
29127
-
29128
- // Reverse the ticks so that they are in the correct render order
29129
- positiveTicks.reverse();
29130
- ticks.push(...positiveTicks, ...negativeTicks);
29131
- return ticks;
29132
- }
29133
-
29134
- function pathBuilder() {
29135
- return new PathBuilder();
29136
- }
29137
- class PathBuilder {
29138
- path = [];
29139
- scaleFactor = 1;
29140
- build() {
29141
- return this.path.map(scaleCommandBy(this.scaleFactor)).map(commandToString).join("");
29142
- }
29143
-
29144
- // add a move (M) command to the path
29145
- move(x, y) {
29146
- this.path.push({
29147
- action: "M",
29148
- args: [x, y]
29149
- });
29150
- return this;
29151
- }
29152
- line(x, y) {
29153
- this.path.push({
29154
- action: "L",
29155
- args: [x, y]
29156
- });
29157
- return this;
29158
- }
29159
- circularArc(radius, toX, toY) {
29160
- let {
29161
- sweep = false,
29162
- largeArc = false
29163
- } = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
29164
- this.path.push({
29165
- action: "A",
29166
- args: [radius, radius, 0 /* rotation */, largeArc ? 1 : 0, sweep ? 1 : 0, toX, toY]
29167
- });
29168
- return this;
29169
- }
29170
-
29171
- // add a curve (C) command to the path
29172
- curve(control1X, control1Y, control2X, control2Y, endX, endY) {
29173
- this.path.push({
29174
- action: "C",
29175
- args: [control1X, control1Y, control2X, control2Y, endX, endY]
29176
- });
29177
- return this;
29178
- }
29179
-
29180
- // Scale all coordinates of the path by a factor
29181
- scale(factor) {
29182
- this.scaleFactor *= factor;
29183
- return this;
29184
- }
29185
- }
29186
- function commandToString(command) {
29187
- return `${command.action}${command.args.join(" ")}`;
29188
- }
29189
- function scaleCommandBy(scaleFactor) {
29190
- return command => {
29191
- switch (command.action) {
29192
- case "A":
29193
- // TODO(benchristel): do we want to refactor this switch to
29194
- // use polymorphism?
29195
- // Arc command
29196
- return {
29197
- ...command,
29198
- args: [command.args[0] * scaleFactor,
29199
- // x radius
29200
- command.args[1] * scaleFactor,
29201
- // y radius
29202
- command.args[2],
29203
- // rotation
29204
- command.args[3],
29205
- // largeArc flag
29206
- command.args[4],
29207
- // sweep flag
29208
- command.args[5] * scaleFactor,
29209
- // end x
29210
- command.args[6] * scaleFactor // end y
29211
- ]
29212
- };
29213
-
29214
- default:
29215
- return {
29216
- ...command,
29217
- args: command.args.map(c => c * scaleFactor)
29218
- };
29219
- }
29220
- };
29221
- }
29222
-
29223
28938
  // We use the pathBuilder here to scale up the SVG path coordinates used
29224
28939
  // elsewhere for arrowheads. We scale in the path itself instead of using a CSS
29225
28940
  // transform because a transform would also scale up the stroke weight, and we
@@ -29249,7 +28964,10 @@ function AxisArrows() {
29249
28964
  range: [[xMin, xMax], [yMin, yMax]]
29250
28965
  } = useGraphConfig();
29251
28966
  const axisColor = "var(--mafs-fg)";
29252
- return /*#__PURE__*/React__default["default"].createElement(React__default["default"].Fragment, null, /*#__PURE__*/React__default["default"].createElement(Arrowhead, {
28967
+
28968
+ // Only render the arrows if the axis is within the visible range
28969
+ // as otherwise the arrows will be rendered outside the graph
28970
+ return /*#__PURE__*/React__default["default"].createElement(React__default["default"].Fragment, null, !(yMin > 0 || yMax < 0) && /*#__PURE__*/React__default["default"].createElement(React__default["default"].Fragment, null, /*#__PURE__*/React__default["default"].createElement(Arrowhead, {
29253
28971
  color: axisColor,
29254
28972
  tip: [xMin, 0],
29255
28973
  angle: 180
@@ -29257,7 +28975,7 @@ function AxisArrows() {
29257
28975
  color: axisColor,
29258
28976
  tip: [xMax, 0],
29259
28977
  angle: 0
29260
- }), /*#__PURE__*/React__default["default"].createElement(Arrowhead, {
28978
+ })), !(xMin > 0 || xMax < 0) && /*#__PURE__*/React__default["default"].createElement(React__default["default"].Fragment, null, /*#__PURE__*/React__default["default"].createElement(Arrowhead, {
29261
28979
  color: axisColor,
29262
28980
  tip: [0, yMin],
29263
28981
  angle: 90
@@ -29265,39 +28983,76 @@ function AxisArrows() {
29265
28983
  color: axisColor,
29266
28984
  tip: [0, yMax],
29267
28985
  angle: 270
29268
- }));
28986
+ })));
28987
+ }
28988
+
28989
+ function AxisLabels() {
28990
+ const {
28991
+ range,
28992
+ labels,
28993
+ width,
28994
+ height
28995
+ } = useGraphConfig();
28996
+ const yAxisLabelLocation = [0, range[Y][MAX]];
28997
+ const xAxisLabelLocation = [range[X][MAX], 0];
28998
+ const [xAxisLabelText, yAxisLabelText] = labels;
28999
+ const graphInfo = {
29000
+ range,
29001
+ width,
29002
+ height
29003
+ };
29004
+ const [x1, y1] = pointToPixel(xAxisLabelLocation, graphInfo);
29005
+ // The default location for the y-axis-label is at the maximum y point
29006
+ // This is just underneath the tick line for that point. The -24 moves the
29007
+ // label up one grid square, so it sits on top of the graph
29008
+ const [x2, y2] = mafs.vec.add(pointToPixel(yAxisLabelLocation, graphInfo), [0, -24]);
29009
+ const {
29010
+ TeX
29011
+ } = getDependencies();
29012
+ return /*#__PURE__*/React__default["default"].createElement(React__default["default"].Fragment, null, /*#__PURE__*/React__default["default"].createElement("span", {
29013
+ style: {
29014
+ position: "absolute",
29015
+ left: x1,
29016
+ top: y1,
29017
+ fontSize: "14px",
29018
+ transform: "translate(7px, -50%)"
29019
+ }
29020
+ }, /*#__PURE__*/React__default["default"].createElement(TeX, null, xAxisLabelText)), /*#__PURE__*/React__default["default"].createElement("span", {
29021
+ style: {
29022
+ position: "absolute",
29023
+ left: x2,
29024
+ top: y2,
29025
+ fontSize: "14px",
29026
+ transform: "translate(-50%, 0px)"
29027
+ }
29028
+ }, /*#__PURE__*/React__default["default"].createElement(TeX, null, yAxisLabelText)));
29269
29029
  }
29270
29030
 
29031
+ // The size of the ticks and labels in pixels
29271
29032
  const tickSize = 10;
29272
- const tickStyle = {
29273
- stroke: "black",
29274
- strokeWidth: 1
29275
- };
29033
+ const tickLabelSize = 14;
29276
29034
  const YGridTick = _ref => {
29277
29035
  let {
29278
29036
  y,
29279
- graphInfo
29037
+ range
29280
29038
  } = _ref;
29039
+ // If the graph requires out-of-bounds labels, we want to make sure to set the
29040
+ // coordinates to the edge of the visible range of the graph. Otherwise,
29041
+ // the ticks and labels would render outside of the clipping-mask.
29281
29042
  let xPointOnAxis = 0;
29282
-
29283
- // If the graph is zoomed in, we want to make sure the ticks are still visible
29284
- // even if they are outside the graph's range.
29285
- if (graphInfo.range[X][MIN] > 0) {
29043
+ if (range[X][MIN] > 0) {
29286
29044
  // If the graph is on the positive side of the x-axis, lock the ticks to the left side of the graph
29287
- xPointOnAxis = graphInfo.range[X][MIN];
29045
+ xPointOnAxis = range[X][MIN];
29288
29046
  }
29289
- if (graphInfo.range[X][MAX] < 0) {
29047
+ if (range[X][MAX] < 0) {
29290
29048
  // If the graph is on the negative side of the x-axis, lock the ticks to the right side of the graph
29291
- xPointOnAxis = graphInfo.range[X][MAX];
29049
+ xPointOnAxis = range[X][MAX];
29292
29050
  }
29051
+
29052
+ // Convert the Vector2 coordinates to pixel coordinates
29293
29053
  const pointOnAxis = [xPointOnAxis, y];
29294
29054
  const [[xPosition, yPosition]] = useTransformVectorsToPixels(pointOnAxis);
29295
29055
 
29296
- // If the tick is on the edge of the graph's range, don't render it
29297
- if (yPosition === -graphInfo.height || yPosition === graphInfo.height + 20) {
29298
- return null;
29299
- }
29300
-
29301
29056
  // Position of the start of the tick
29302
29057
  const x1 = xPosition - tickSize / 2;
29303
29058
  const y1 = yPosition;
@@ -29305,40 +29060,56 @@ const YGridTick = _ref => {
29305
29060
  // Position of the end of the tick
29306
29061
  const x2 = xPosition + tickSize / 2;
29307
29062
  const y2 = yPosition;
29063
+
29064
+ // Adjust the y position of the x-axis labels based on
29065
+ // whether the x-axis is above, within, or below the graph
29066
+ const xAdjustment = range[X][MAX] <= 0 ? tickLabelSize * 1.5 : -tickLabelSize * 1.1;
29067
+ const xPositionText = xPosition + xAdjustment;
29068
+ const yPositionText = yPosition + tickLabelSize * 0.25; // Center the text vertically on the tick
29069
+
29070
+ // If the graph displays both the y and x axis lines within the graph, we want
29071
+ // to hide the label at -1 on the y-axis to prevent overlap with the x-axis label
29072
+ const showLabel = shouldShowLabel(y, range);
29308
29073
  return /*#__PURE__*/React__namespace.createElement("g", {
29309
- className: "y-axis-ticks"
29074
+ className: "tick"
29310
29075
  }, /*#__PURE__*/React__namespace.createElement("line", {
29311
29076
  x1: x1,
29312
29077
  y1: y1,
29313
29078
  x2: x2,
29314
29079
  y2: y2,
29315
- style: tickStyle
29316
- }));
29080
+ className: "axis-tick"
29081
+ }), showLabel && /*#__PURE__*/React__namespace.createElement("text", {
29082
+ className: "axis-tick-label",
29083
+ style: {
29084
+ fontSize: tickLabelSize
29085
+ },
29086
+ textAnchor: "end",
29087
+ x: xPositionText,
29088
+ y: yPositionText
29089
+ }, y.toString()));
29317
29090
  };
29318
29091
  const XGridTick = _ref2 => {
29319
29092
  let {
29320
29093
  x,
29321
- graphInfo
29094
+ range
29322
29095
  } = _ref2;
29096
+ // If the graph requires out-of-bounds labels, we want to make sure to set the
29097
+ // coordinates to the edge of the visible range of the graph. Otherwise,
29098
+ // the ticks and labels would render outside of the clipping-mask.
29323
29099
  let yPointOnAxis = 0;
29324
- // If the graph is zoomed in, we want to make sure the ticks are still visible
29325
- // even if they are outside the graph's range.
29326
- if (graphInfo.range[Y][MIN] > 0) {
29100
+ if (range[Y][MIN] > 0) {
29327
29101
  // If the graph is on the positive side of the y-axis, lock the ticks to the top of the graph
29328
- yPointOnAxis = graphInfo.range[Y][MIN];
29102
+ yPointOnAxis = range[Y][MIN];
29329
29103
  }
29330
- if (graphInfo.range[Y][MAX] < 0) {
29104
+ if (range[Y][MAX] < 0) {
29331
29105
  // If the graph is on the negative side of the x-axis, lock the ticks to the bottom of the graph
29332
- yPointOnAxis = graphInfo.range[Y][MAX];
29106
+ yPointOnAxis = range[Y][MAX];
29333
29107
  }
29108
+
29109
+ // Convert the Vector2 coordinates to pixel coordinates
29334
29110
  const pointOnAxis = [x, yPointOnAxis];
29335
29111
  const [[xPosition, yPosition]] = useTransformVectorsToPixels(pointOnAxis);
29336
29112
 
29337
- // If the tick is on the edge of the graph's range, don't render it
29338
- if (xPosition === -graphInfo.width / 2 || xPosition === graphInfo.width) {
29339
- return null;
29340
- }
29341
-
29342
29113
  // Position of the start of the tick
29343
29114
  const x1 = xPosition;
29344
29115
  const y1 = yPosition + tickSize / 2;
@@ -29346,21 +29117,56 @@ const XGridTick = _ref2 => {
29346
29117
  // Position of the end of the tick
29347
29118
  const x2 = xPosition;
29348
29119
  const y2 = yPosition - tickSize / 2;
29120
+
29121
+ // Adjust the Y position of the x-axis labels based on
29122
+ // whether the x-axis is above, within, or below the graph
29123
+ const yAdjustment = range[Y][MAX] < 0 ? -tickLabelSize : tickLabelSize * 1.75;
29124
+
29125
+ // Adjust the X position of the x-axis labels based on
29126
+ // whether the label is positive or negative, in order to
29127
+ // account for the width of the negative sign
29128
+ const xAdjustment = x < 0 ? -2 : 0;
29129
+
29130
+ // Apply the adjustments to the x and y positions for the text
29131
+ const xPositionText = xPosition + xAdjustment;
29132
+ const yPositionText = yPosition + yAdjustment;
29349
29133
  return /*#__PURE__*/React__namespace.createElement("g", {
29350
- className: "x-axis-ticks"
29134
+ className: "tick"
29351
29135
  }, /*#__PURE__*/React__namespace.createElement("line", {
29352
29136
  x1: x1,
29353
29137
  y1: y1,
29354
29138
  x2: x2,
29355
29139
  y2: y2,
29356
- style: tickStyle
29357
- }));
29140
+ className: "axis-tick"
29141
+ }), /*#__PURE__*/React__namespace.createElement("text", {
29142
+ className: "axis-tick-label",
29143
+ style: {
29144
+ fontSize: tickLabelSize
29145
+ },
29146
+ textAnchor: "middle",
29147
+ x: xPositionText,
29148
+ y: yPositionText
29149
+ }, x.toString()));
29150
+ };
29151
+
29152
+ // Determines whether to show the label for the given tick
29153
+ // Currently, the only condition is to hide the label at -1
29154
+ // on the y-axis when the x-axis is within the graph
29155
+ const shouldShowLabel = (number, range) => {
29156
+ let showLabel = true;
29157
+
29158
+ // If the x-axis is within the graph and the y-axis is at -1, hide the label
29159
+ if (range[X][MIN] < -1 && range[X][MAX] > 0 && number === -1) {
29160
+ showLabel = false;
29161
+ }
29162
+ return showLabel;
29358
29163
  };
29359
29164
  function generateTickLocations(tickStep, min, max) {
29360
29165
  const ticks = [];
29361
29166
 
29362
29167
  // Add ticks in the positive direction
29363
- for (let i = 0 + tickStep; i < max; i += tickStep) {
29168
+ const start = Math.max(min, 0);
29169
+ for (let i = start + tickStep; i < max; i += tickStep) {
29364
29170
  ticks.push(i);
29365
29171
  }
29366
29172
 
@@ -29375,34 +29181,34 @@ function generateTickLocations(tickStep, min, max) {
29375
29181
  const AxisTicks = () => {
29376
29182
  const {
29377
29183
  tickStep,
29378
- range,
29379
- width,
29380
- height
29184
+ range
29381
29185
  } = useGraphConfig();
29382
- const graphInfo = {
29383
- range,
29384
- width,
29385
- height
29386
- };
29387
29186
  const [[xMin, xMax], [yMin, yMax]] = range;
29388
29187
  const [xTickStep, yTickStep] = tickStep;
29188
+
29189
+ // Generate the tick locations & labels for the x and y axes
29389
29190
  const yGridTicks = generateTickLocations(yTickStep, yMin, yMax);
29390
29191
  const xGridTicks = generateTickLocations(xTickStep, xMin, xMax);
29391
29192
  return /*#__PURE__*/React__namespace.createElement("g", {
29392
- className: "axis-ticks"
29193
+ className: "axis-ticks",
29194
+ role: "presentation"
29195
+ }, /*#__PURE__*/React__namespace.createElement("g", {
29196
+ className: "y-axis-ticks"
29393
29197
  }, yGridTicks.map(y => {
29394
29198
  return /*#__PURE__*/React__namespace.createElement(YGridTick, {
29395
29199
  y: y,
29396
29200
  key: `y-grid-tick-${y}`,
29397
- graphInfo: graphInfo
29201
+ range: range
29398
29202
  });
29399
- }), xGridTicks.map(x => {
29203
+ })), /*#__PURE__*/React__namespace.createElement("g", {
29204
+ className: "x-axis-ticks"
29205
+ }, xGridTicks.map(x => {
29400
29206
  return /*#__PURE__*/React__namespace.createElement(XGridTick, {
29401
29207
  x: x,
29402
29208
  key: `x-grid-tick-${x}`,
29403
- graphInfo: graphInfo
29209
+ range: range
29404
29210
  });
29405
- }));
29211
+ })));
29406
29212
  };
29407
29213
 
29408
29214
  /**
@@ -29423,12 +29229,10 @@ const axisOptions = (props, axisIndex) => {
29423
29229
  };
29424
29230
  };
29425
29231
  const Grid = props => {
29426
- return props.markings === "none" ? null : /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(mafs.Coordinates.Cartesian, {
29232
+ return props.markings === "none" ? null : /*#__PURE__*/React__namespace.createElement(mafs.Coordinates.Cartesian, {
29427
29233
  xAxis: axisOptions(props, X),
29428
29234
  yAxis: axisOptions(props, Y)
29429
- }),
29430
- // Only render the axis ticks and arrows if the markings are set to a full "graph"
29431
- props.markings === "graph" && /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(AxisTicks, null), /*#__PURE__*/React__namespace.createElement(AxisArrows, null)));
29235
+ });
29432
29236
  };
29433
29237
 
29434
29238
  // Displays a stylized open eye: lint warnings are visible
@@ -32370,6 +32174,22 @@ const MafsGraph = props => {
32370
32174
  const uniqueId = React__namespace.useId();
32371
32175
  const descriptionId = `interactive-graph-description-${uniqueId}`;
32372
32176
  const graphRef = React__namespace.useRef(null);
32177
+
32178
+ // Set up the SVG attributes for the nested SVGs that help lock
32179
+ // the grid and graph elements to the bounds of the graph.
32180
+ const {
32181
+ viewboxX,
32182
+ viewboxY
32183
+ } = calculateNestedSVGCoords(state.range, width, height);
32184
+ const viewBox = `${viewboxX} ${viewboxY} ${width} ${height}`;
32185
+ const nestedSVGAttributes = {
32186
+ width,
32187
+ height,
32188
+ viewBox,
32189
+ preserveAspectRatio: "xMidYMin",
32190
+ x: viewboxX,
32191
+ y: viewboxY
32192
+ };
32373
32193
  return /*#__PURE__*/React__namespace.createElement(GraphConfigContext.Provider, {
32374
32194
  value: {
32375
32195
  range: state.range,
@@ -32391,7 +32211,7 @@ const MafsGraph = props => {
32391
32211
  padding: "25px 25px 0 0",
32392
32212
  boxSizing: "content-box",
32393
32213
  marginLeft: "20px",
32394
- marginBottom: "20px",
32214
+ marginBottom: "30px",
32395
32215
  pointerEvents: props.static ? "none" : "auto",
32396
32216
  userSelect: "none",
32397
32217
  width,
@@ -32426,7 +32246,7 @@ const MafsGraph = props => {
32426
32246
  bottom: 0,
32427
32247
  left: 0
32428
32248
  }
32429
- }, props.markings === "graph" && /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(AxisLabels, null), /*#__PURE__*/React__namespace.createElement(AxisTickLabels, null)), /*#__PURE__*/React__namespace.createElement(mafs.Mafs, {
32249
+ }, props.markings === "graph" && /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(AxisLabels, null)), /*#__PURE__*/React__namespace.createElement(mafs.Mafs, {
32430
32250
  preserveAspectRatio: false,
32431
32251
  viewBox: {
32432
32252
  x: state.range[X],
@@ -32437,19 +32257,22 @@ const MafsGraph = props => {
32437
32257
  zoom: false,
32438
32258
  width: width,
32439
32259
  height: height
32440
- }, /*#__PURE__*/React__namespace.createElement(SvgDefs, null), /*#__PURE__*/React__namespace.createElement(Grid, {
32441
- tickStep: props.step,
32260
+ }, /*#__PURE__*/React__namespace.createElement(SvgDefs, null), /*#__PURE__*/React__namespace.createElement("svg", nestedSVGAttributes, /*#__PURE__*/React__namespace.createElement(Grid, {
32442
32261
  gridStep: props.gridStep,
32443
32262
  range: state.range,
32444
32263
  containerSizeClass: props.containerSizeClass,
32445
- markings: props.markings
32446
- }), props.lockedFigures && /*#__PURE__*/React__namespace.createElement(GraphLockedLayer, {
32264
+ markings: props.markings,
32265
+ width: width,
32266
+ height: height
32267
+ })),
32268
+ // Only render the axis ticks and arrows if the markings are set to a full "graph"
32269
+ props.markings === "graph" && /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(AxisTicks, null), /*#__PURE__*/React__namespace.createElement(AxisArrows, null)), /*#__PURE__*/React__namespace.createElement("svg", nestedSVGAttributes, props.lockedFigures && /*#__PURE__*/React__namespace.createElement(GraphLockedLayer, {
32447
32270
  lockedFigures: props.lockedFigures,
32448
32271
  range: state.range
32449
32272
  }), props.showProtractor && /*#__PURE__*/React__namespace.createElement(Protractor, null), renderGraph({
32450
32273
  state,
32451
32274
  dispatch
32452
- })))), renderGraphControls({
32275
+ }))))), renderGraphControls({
32453
32276
  state,
32454
32277
  dispatch
32455
32278
  })));
@@ -32485,6 +32308,56 @@ const renderGraphControls = props => {
32485
32308
  return null;
32486
32309
  }
32487
32310
  };
32311
+
32312
+ // Calculate the difference between the min and max values of a range
32313
+ const getRangeDiff = range => {
32314
+ const [min, max] = range;
32315
+ return Math.abs(max - min);
32316
+ };
32317
+
32318
+ // We need to adjust the nested SVG viewbox x and Y values based on the range of the graph in order
32319
+ // to ensure that the graph is sized and positioned correctly within the Mafs SVG and the clipping mask.
32320
+ // Exported for testing.
32321
+ const calculateNestedSVGCoords = (range, width, height) => {
32322
+ // X RANGE
32323
+ let viewboxX = 0; // When xMin is 0, we want to use 0 as the viewboxX value
32324
+ const totalXRange = getRangeDiff(range[X]);
32325
+ const gridCellWidth = width / totalXRange;
32326
+ const minX = range[X][MIN];
32327
+
32328
+ // If xMin is entirely positive, we need to adjust the
32329
+ // viewboxX to be the grid cell width multiplied by xMin
32330
+ if (minX > 0) {
32331
+ viewboxX = gridCellWidth * Math.abs(minX);
32332
+ }
32333
+ // If xMin is negative, we need to adjust the viewboxX to be
32334
+ // the negative value of the grid cell width multiplied by xMin
32335
+ if (minX < 0) {
32336
+ viewboxX = -gridCellWidth * Math.abs(minX);
32337
+ }
32338
+
32339
+ // Y RANGE
32340
+ let viewboxY = -height; // When yMin is 0, we want to use the negative value of the graph height
32341
+ const totalYRange = getRangeDiff(range[Y]);
32342
+ const gridCellHeight = height / totalYRange;
32343
+ const minY = range[Y][MIN];
32344
+
32345
+ // If the y range is entirely positive, we want a negative sum of the
32346
+ // height and the gridcell height multiplied by the absolute value of yMin
32347
+ if (minY > 0) {
32348
+ viewboxY = -height - gridCellHeight * Math.abs(minY);
32349
+ }
32350
+
32351
+ // If the yMin is negative, we want to multiply the gridcell height
32352
+ // by the absolute value of yMin, and subtract the full height of the graph
32353
+ if (minY < 0) {
32354
+ viewboxY = gridCellHeight * Math.abs(minY) - height;
32355
+ }
32356
+ return {
32357
+ viewboxX,
32358
+ viewboxY
32359
+ };
32360
+ };
32488
32361
  const renderGraph = props => {
32489
32362
  const {
32490
32363
  state,
@@ -45099,7 +44972,7 @@ const init = function (options) {
45099
44972
 
45100
44973
  // This file is processed by a Rollup plugin (replace) to inject the production
45101
44974
  const libName = "@khanacademy/perseus";
45102
- const libVersion = "33.0.0";
44975
+ const libVersion = "33.1.0";
45103
44976
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
45104
44977
 
45105
44978
  const apiVersion = {