@khanacademy/perseus 22.5.0 → 22.6.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,7 +29,7 @@ import { OptionItem, SingleSelect, MultiSelect } from '@khanacademy/wonder-block
29
29
  import caretDown from '@phosphor-icons/core/regular/caret-down.svg';
30
30
  import caretUp from '@phosphor-icons/core/regular/caret-up.svg';
31
31
  import debounce$2 from 'lodash.debounce';
32
- import { vec, Line as Line$4, Point as Point$2, useMovable, Text as Text$1, Polygon, Circle as Circle$1, Coordinates, Mafs } from 'mafs';
32
+ import { vec, Line as Line$4, Point as Point$2, useMovable, Text as Text$1, Polygon, Coordinates, Mafs } from 'mafs';
33
33
  import { PhosphorIcon } from '@khanacademy/wonder-blocks-icon';
34
34
  import { Body, LabelSmall, LabelMedium } from '@khanacademy/wonder-blocks-typography';
35
35
  import questionIcon from '@phosphor-icons/core/regular/question.svg';
@@ -28474,6 +28474,8 @@ Graph.defaultProps = {
28474
28474
 
28475
28475
  const defaultGraphConfig = {
28476
28476
  range: [[0, 1], [0, 1]],
28477
+ tickStep: [1, 1],
28478
+ gridStep: [1, 1],
28477
28479
  snapStep: [1, 1],
28478
28480
  markings: "none",
28479
28481
  showTooltips: false,
@@ -28488,6 +28490,7 @@ function useGraphConfig() {
28488
28490
  }
28489
28491
 
28490
28492
  const matrixBuilder = vec.matrixBuilder;
28493
+
28491
28494
  // When converting vectors, we don't need to translate because a vector
28492
28495
  // is like a pair of x and y distances, which are not bound to specific graph locations.
28493
28496
  function vectorsToPixels(vectors, graphState, translation = [0, 0]) {
@@ -28736,7 +28739,7 @@ function Arrowhead(props) {
28736
28739
  const [point] = useTransformVectorsToPixels(props.tip);
28737
28740
  return /*#__PURE__*/React.createElement("g", {
28738
28741
  className: "interactive-graph-arrowhead",
28739
- transform: `translate(${point[0]} ${point[1]}) rotate(${-props.angle})`
28742
+ transform: `translate(${point[0]} ${point[1]}) rotate(${props.angle})`
28740
28743
  }, /*#__PURE__*/React.createElement("g", {
28741
28744
  transform: "translate(-1.5)"
28742
28745
  }, /*#__PURE__*/React.createElement("path", {
@@ -28746,7 +28749,8 @@ function Arrowhead(props) {
28746
28749
  stroke: (_props$color = props.color) != null ? _props$color : "inherit"
28747
28750
  },
28748
28751
  strokeLinejoin: "round",
28749
- strokeLinecap: "round"
28752
+ strokeLinecap: "round",
28753
+ strokeWidth: "2px"
28750
28754
  })));
28751
28755
  }
28752
28756
 
@@ -28782,10 +28786,7 @@ const getIntersectionOfRayWithBox = (initialPoint, throughPoint, box) => {
28782
28786
  case isBetween(xAtYExtreme, xMin, xMax):
28783
28787
  return [xAtYExtreme, yExtreme];
28784
28788
  default:
28785
- // This default case is only reachable if the input is invalid
28786
- // (initialPoint is outside the graph bounds, or initialPoint and
28787
- // throughPoint are the same).
28788
- return [0, 0];
28789
+ return [xExtreme, yExtreme];
28789
28790
  }
28790
28791
  };
28791
28792
  const getLines = points => {
@@ -28825,7 +28826,7 @@ function Vector(props) {
28825
28826
  style
28826
28827
  } = props;
28827
28828
  const [tailPx, tipPx] = useTransformVectorsToPixels(tail, tip);
28828
- const direction = vec.sub(tip, tail);
28829
+ const direction = vec.sub(tipPx, tailPx);
28829
28830
  const angle = calculateAngleInDegrees(direction);
28830
28831
  return /*#__PURE__*/React.createElement("g", {
28831
28832
  style: {
@@ -28869,7 +28870,9 @@ const LockedLine = props => {
28869
28870
  } else {
28870
28871
  const LineType = kind === "segment" ? Line$4.Segment : Line$4.ThroughPoints;
28871
28872
  let arrowTip = kind === "segment" ? point2.coord : getIntersectionOfRayWithBox(point1.coord, point2.coord, range);
28872
- const direction = vec.sub(point2.coord, point1.coord);
28873
+ // eslint-disable-next-line react-hooks/rules-of-hooks
28874
+ const [tailPx, tipPx] = useTransformVectorsToPixels(point2.coord, point1.coord);
28875
+ const direction = vec.sub(tailPx, tipPx);
28873
28876
  let angle = calculateAngleInDegrees(direction);
28874
28877
  const startArrowHead = kind !== "segment" && /*#__PURE__*/React.createElement(Arrowhead, {
28875
28878
  angle: angle,
@@ -29683,14 +29686,12 @@ function initializeGraphState(params) {
29683
29686
  graph,
29684
29687
  step,
29685
29688
  snapStep,
29686
- range,
29687
- markings
29689
+ range
29688
29690
  } = params;
29689
29691
  const shared = {
29690
29692
  hasBeenInteractedWith: false,
29691
29693
  range,
29692
- snapStep,
29693
- markings
29694
+ snapStep
29694
29695
  };
29695
29696
  switch (graph.type) {
29696
29697
  case "segment":
@@ -29869,19 +29870,24 @@ const getLineCoords = ({
29869
29870
  range,
29870
29871
  step
29871
29872
  }) => {
29872
- var _graph$coords2;
29873
- return (
29874
- // Return two lines for a linear system, one for a ray or linear
29875
- ((_graph$coords2 = graph.coords) != null ? _graph$coords2 : graph.type === "linear-system") ? defaultLinearCoords.map(collinear => normalizePoints(range, step, collinear)) : [normalizePoints(range, step, defaultLinearCoords[0])]
29876
- );
29873
+ // Return two lines for a linear system, one for a ray or linear
29874
+ switch (graph.type) {
29875
+ case "linear-system":
29876
+ return defaultLinearCoords.map(points => normalizePoints(range, step, points));
29877
+ case "linear":
29878
+ case "ray":
29879
+ return [normalizePoints(range, step, defaultLinearCoords[0])];
29880
+ default:
29881
+ throw new UnreachableCaseError(graph);
29882
+ }
29877
29883
  };
29878
29884
  const getPolygonCoords = ({
29879
29885
  graph,
29880
29886
  range,
29881
29887
  step
29882
29888
  }) => {
29883
- var _graph$coords3;
29884
- let coords = (_graph$coords3 = graph.coords) == null ? void 0 : _graph$coords3.slice();
29889
+ var _graph$coords2;
29890
+ let coords = (_graph$coords2 = graph.coords) == null ? void 0 : _graph$coords2.slice();
29885
29891
  if (coords) {
29886
29892
  return coords;
29887
29893
  }
@@ -29967,11 +29973,12 @@ function MovableCircle(props) {
29967
29973
  cy: centerPx[1],
29968
29974
  rx: radiiPx[0] + 3,
29969
29975
  ry: radiiPx[1] + 3
29970
- }), /*#__PURE__*/React.createElement(Circle$1, {
29971
- center: center,
29972
- radius: radius,
29973
- fillOpacity: 0,
29974
- color: color.blue
29976
+ }), /*#__PURE__*/React.createElement("ellipse", {
29977
+ className: "circle",
29978
+ cx: centerPx[0],
29979
+ cy: centerPx[1],
29980
+ rx: radiiPx[0],
29981
+ ry: radiiPx[1]
29975
29982
  }), /*#__PURE__*/React.createElement(DragHandle, {
29976
29983
  center: center
29977
29984
  }));
@@ -30012,6 +30019,258 @@ function crossProduct(as, bs) {
30012
30019
  return result;
30013
30020
  }
30014
30021
 
30022
+ // Universal padding for the axis ticks to ensure that the labels
30023
+ // have enough space to render without overlapping the tick lines
30024
+ const tickPadding = 5;
30025
+ const AxisTickLabels = () => {
30026
+ const graphConfig = useGraphConfig();
30027
+ const {
30028
+ tickStep,
30029
+ range
30030
+ } = graphConfig;
30031
+ const [xMin, xMax] = range[0];
30032
+ const [yMin, yMax] = range[1];
30033
+ const yTickStep = tickStep[1];
30034
+ const xTickStep = tickStep[0];
30035
+ const yGridTicks = generateTickLocations$1(yTickStep, yMin, yMax);
30036
+ const xGridTicks = generateTickLocations$1(xTickStep, xMin, xMax);
30037
+ return /*#__PURE__*/React.createElement("div", {
30038
+ className: "axis-tick-labels"
30039
+ }, /*#__PURE__*/React.createElement(YGridAxis, {
30040
+ axisTicks: yGridTicks,
30041
+ graphConfig: graphConfig
30042
+ }), /*#__PURE__*/React.createElement(XGridAxis, {
30043
+ axisTicks: xGridTicks,
30044
+ graphConfig: graphConfig
30045
+ }));
30046
+ };
30047
+
30048
+ // Generate the necessary styles and ticks for the y-axis tick labels container
30049
+ const YGridAxis = props => {
30050
+ const {
30051
+ axisTicks,
30052
+ graphConfig
30053
+ } = props;
30054
+ const {
30055
+ width,
30056
+ height
30057
+ } = graphConfig;
30058
+
30059
+ // First get the label height by calculating the difference between the first two y-axis labels
30060
+ const yTopPoint = pointToPixel([0, axisTicks[0]], graphConfig);
30061
+ const ySecondPoint = pointToPixel([0, axisTicks[1]], graphConfig);
30062
+ const yLabelHeight = ySecondPoint[1] - yTopPoint[1];
30063
+
30064
+ // When rendering purely positive y-axis labels, we want to adjust the left position
30065
+ // from the lowest calculated tick, as yMin may be negative
30066
+ const yLowestTick = axisTicks[axisTicks.length - 1];
30067
+ const yLeftAdjustment = Math.min(Math.max(yTopPoint[0] - tickPadding, 0), width);
30068
+
30069
+ // We only want to render the 0 label on the y-axis if it is left of the grid
30070
+ const yAxisIsLeftOfGrid = yLeftAdjustment === 0;
30071
+
30072
+ // If the y-axis is right of the graph's viewbox/range,
30073
+ // we want to adjust both the positioning and text alignment
30074
+ const yAxisRightOfGrid = yLeftAdjustment === width;
30075
+
30076
+ // Determine the default spacing for the labels based on whether the y-axis is right of the grid
30077
+ const defaultSpacing = yAxisRightOfGrid ? 0.5 : 0;
30078
+
30079
+ // We also want to adjust the spacing if there are no negative y-axis labels as the extra space is not needed
30080
+ const marginSpacing = yLowestTick >= 0 ? defaultSpacing * 2 : defaultSpacing;
30081
+
30082
+ // Determine the text alignment based on whether the y-axis is right of the grid
30083
+ // We need to cast this as a const as it is used in the inline styles
30084
+ const textAlign = yAxisRightOfGrid ? "left" : "right";
30085
+ const yAxisStyles = {
30086
+ left: `calc(${yLeftAdjustment}px - ${marginSpacing}em)`,
30087
+ top: `calc(${yTopPoint[1]}px - 0.5em)`,
30088
+ height,
30089
+ textAlign,
30090
+ "--y-axis-label-height": yLabelHeight + "px"
30091
+ };
30092
+ const classNames = yAxisRightOfGrid ? "y-axis-tick-labels y-axis-right-of-grid" : "y-axis-tick-labels ";
30093
+ return /*#__PURE__*/React.createElement("div", {
30094
+ className: classNames,
30095
+ "data-testid": "y-axis-tick-labels",
30096
+ style: yAxisStyles
30097
+ }, axisTicks.map(label => {
30098
+ return /*#__PURE__*/React.createElement(AxisTickLabel, {
30099
+ label: label,
30100
+ axis: "y",
30101
+ key: `y-grid-tick-${label}`,
30102
+ graphConfig: graphConfig,
30103
+ axisOutOfBounds: yAxisIsLeftOfGrid
30104
+ });
30105
+ }));
30106
+ };
30107
+
30108
+ // Generate the necessary styles and ticks for the x-axis tick labels container
30109
+ const XGridAxis = props => {
30110
+ const {
30111
+ axisTicks,
30112
+ graphConfig
30113
+ } = props;
30114
+ const {
30115
+ width,
30116
+ height
30117
+ } = graphConfig;
30118
+
30119
+ // Determine label width by calculating the difference between the first two x-axis labels
30120
+ const xFirstPoint = pointToPixel([axisTicks[0], 0], graphConfig);
30121
+ const xSecondPoint = pointToPixel([axisTicks[1], 0], graphConfig);
30122
+ const xLabelWidth = Math.abs(xSecondPoint[0] - xFirstPoint[0]);
30123
+
30124
+ // Determine the top position of the x-axis labels based on
30125
+ // whether the x-axis is above, within, or below the graph
30126
+ const xAxisAboveGraph = xFirstPoint[1] < 0;
30127
+ const xTopAdjustment = xAxisAboveGraph ? 0 : Math.min(xFirstPoint[1] + tickPadding, height);
30128
+
30129
+ // Determine whether the x-axis is outside of the graph's viewbox/range
30130
+ // This is used to conditionally render the 0 label on the x-axis
30131
+ const xAxisOutOfBounds = xTopAdjustment === height || xAxisAboveGraph;
30132
+
30133
+ // Determine the available space to the right of the x-axis labels
30134
+ const availableSpaceRight = width - xFirstPoint[0];
30135
+
30136
+ // Calculate the total combined width of ALL the x-axis labels
30137
+ const axisTicksWidth = axisTicks.length * xLabelWidth;
30138
+
30139
+ // Determine if we need to increase the width of the axis past the graph width
30140
+ const calculatedWidth = axisTicksWidth > width ? axisTicksWidth : width;
30141
+
30142
+ // Determine the right adjustment for the x-axis labels based on the available space
30143
+ // We're adjusting the right as the flex-direction is row-reverse
30144
+ const xRightAdjustment = availableSpaceRight - xLabelWidth / 2;
30145
+ const xAxisStyles = {
30146
+ top: xTopAdjustment,
30147
+ right: xRightAdjustment,
30148
+ width: calculatedWidth,
30149
+ "--x-axis-label-width": xLabelWidth + "px"
30150
+ };
30151
+ return /*#__PURE__*/React.createElement("div", {
30152
+ className: `x-axis-tick-labels ${xAxisAboveGraph && "x-axis-top-of-grid"}`,
30153
+ "data-testid": "x-axis-tick-labels",
30154
+ style: xAxisStyles
30155
+ }, axisTicks.map(label => {
30156
+ return /*#__PURE__*/React.createElement(AxisTickLabel, {
30157
+ axis: "x",
30158
+ label: label,
30159
+ key: `x-grid-tick-${label}`,
30160
+ graphConfig: graphConfig,
30161
+ axisOutOfBounds: xAxisOutOfBounds
30162
+ });
30163
+ }));
30164
+ };
30165
+
30166
+ // Generate the individual tick label elements
30167
+ const AxisTickLabel = ({
30168
+ label,
30169
+ graphConfig,
30170
+ axisOutOfBounds,
30171
+ axis
30172
+ }) => {
30173
+ const {
30174
+ gridStep,
30175
+ tickStep
30176
+ } = graphConfig;
30177
+ // Determine the point on the axis based on the axis type
30178
+ const pointOnAxis = axis === "x" ? [label, 0] : [0, label];
30179
+ const pixelPoint = pointToPixel(pointOnAxis, graphConfig);
30180
+
30181
+ // Remove the negative sign from the label string as it is handled in the data-content attribute
30182
+ const labelString = Math.abs(label).toString();
30183
+
30184
+ // Determine the relevant edge of the graph based on the axis type
30185
+ const graphEdge = axis === "x" ? graphConfig.width : graphConfig.height;
30186
+
30187
+ // Determine the vector index based on the axis type
30188
+ const vectorIndex = axis === "x" ? 0 : 1;
30189
+
30190
+ // Determine whether or not to render the tick label text
30191
+ const shouldShowLabel = showTickLabel({
30192
+ label,
30193
+ axisOutOfBounds,
30194
+ graphEdge,
30195
+ position: pixelPoint[vectorIndex],
30196
+ gridStep: gridStep[vectorIndex],
30197
+ tickStep: tickStep[vectorIndex]
30198
+ });
30199
+
30200
+ // Determine whether or not to render a negative symbol for the tick label
30201
+ // as the negative sign is handled in the data-content attribute in order to
30202
+ // maintain the correct spacing between the labels
30203
+ const dataContent = label < 0 && shouldShowLabel ? "−" : null;
30204
+ return /*#__PURE__*/React.createElement("span", {
30205
+ className: `${axis}-axis-tick-label`,
30206
+ "data-content": dataContent
30207
+ }, shouldShowLabel && labelString);
30208
+ };
30209
+
30210
+ // Determine whether or not to show the tick label text
30211
+ const showTickLabel = ({
30212
+ gridStep,
30213
+ tickStep,
30214
+ label,
30215
+ graphEdge,
30216
+ position,
30217
+ axisOutOfBounds
30218
+ }) => {
30219
+ let showLabel = true;
30220
+
30221
+ // We only want to show the initial negative tick labels (e.g. -1) on each
30222
+ // axis if the tickStep > gridStep, to ensure that these labels do not overlap.
30223
+ // e.g. If gridStep = 1 and tickStep = 2, there are 2 grid lines for every 1 tick,
30224
+ // which allows enough room for both these tick labels to render.
30225
+ showLabel = tickStep > gridStep ? true : label !== -tickStep;
30226
+
30227
+ // If the tick label position is greater than the relevant edge of the graph, don't render it
30228
+ if (position <= -graphEdge || position >= graphEdge) {
30229
+ showLabel = false;
30230
+ }
30231
+
30232
+ // If the label is 0, we only want to show it if the axis is to the left of the graph's viewbox/range
30233
+ // otherwise it will overlap with graph axis labels
30234
+ if (label === 0 && !axisOutOfBounds) {
30235
+ showLabel = false;
30236
+ }
30237
+ return showLabel;
30238
+ };
30239
+
30240
+ // Generate the tick labels for an axis based on the tick step
30241
+ function generateTickLocations$1(tickStep, min, max) {
30242
+ const positiveTicks = [];
30243
+ const negativeTicks = [];
30244
+ const ticks = [];
30245
+
30246
+ // Add 0 if it is applicable.
30247
+ // This will be conditionally rendered using showTickLabel.
30248
+ if (min <= 0 && max >= 0) {
30249
+ positiveTicks.push(0);
30250
+ }
30251
+
30252
+ // Add ticks on the positive axis
30253
+ // Start at the first tick after 0 or the minimum value,
30254
+ // taking into account that the graph may be zoomed in on the positive axis
30255
+ let i = Math.max(min, 0) + tickStep;
30256
+ for (i; min < i && i < max; i += tickStep) {
30257
+ positiveTicks.push(i);
30258
+ }
30259
+
30260
+ // Add ticks on the negative axis
30261
+ // Start at the first tick before 0 or the maximum value,
30262
+ // taking into account that the graph may be zoomed in on the negative axis
30263
+ i = Math.min(max, 0) - tickStep;
30264
+ for (i; i > min; i -= tickStep) {
30265
+ negativeTicks.push(i);
30266
+ }
30267
+
30268
+ // Reverse the ticks so that they are in the correct render order
30269
+ positiveTicks.reverse();
30270
+ ticks.push(...positiveTicks, ...negativeTicks);
30271
+ return ticks;
30272
+ }
30273
+
30015
30274
  function PointGraph(props) {
30016
30275
  const {
30017
30276
  dispatch
@@ -30039,11 +30298,11 @@ function AxisArrows() {
30039
30298
  }), /*#__PURE__*/React__default.createElement(Arrowhead, {
30040
30299
  color: axisColor,
30041
30300
  tip: [0, yMin],
30042
- angle: 270
30301
+ angle: 90
30043
30302
  }), /*#__PURE__*/React__default.createElement(Arrowhead, {
30044
30303
  color: axisColor,
30045
30304
  tip: [0, yMax],
30046
- angle: 90
30305
+ angle: 270
30047
30306
  }));
30048
30307
  }
30049
30308
 
@@ -30052,81 +30311,119 @@ const tickStyle = {
30052
30311
  stroke: "black",
30053
30312
  strokeWidth: 1
30054
30313
  };
30055
-
30056
- // We only want to show the initial negative tick labels (e.g. -1) on each
30057
- // axis if the tickStep > gridStep, to ensure that these labels do not overlap.
30058
- // e.g. If gridStep = 1 and tickStep = 2, there are 2 grid lines for every 1 tick,
30059
- // which allows enough room for both these tick labels to render.
30060
- const showTickLabel = (gridStep, tickStep, label) => {
30061
- const showLabel = tickStep > gridStep ? true : label !== -tickStep;
30062
- return showLabel;
30063
- };
30064
30314
  const YGridTick = ({
30065
30315
  y,
30066
- gridStep,
30067
- tickStep
30316
+ graphInfo
30068
30317
  }) => {
30069
- const pointOnAxis = [0, y];
30318
+ let xPointOnAxis = 0;
30319
+
30320
+ // If the graph is zoomed in, we want to make sure the ticks are still visible
30321
+ // even if they are outside the graph's range.
30322
+ if (graphInfo.range[0][0] > 0) {
30323
+ // If the graph is on the positive side of the x-axis, lock the ticks to the left side of the graph
30324
+ xPointOnAxis = graphInfo.range[0][0];
30325
+ }
30326
+ if (graphInfo.range[0][1] < 0) {
30327
+ // If the graph is on the negative side of the x-axis, lock the ticks to the right side of the graph
30328
+ xPointOnAxis = graphInfo.range[0][1];
30329
+ }
30330
+ const pointOnAxis = [xPointOnAxis, y];
30070
30331
  const [[xPosition, yPosition]] = useTransformVectorsToPixels(pointOnAxis);
30332
+
30333
+ // If the tick is on the edge of the graph's range, don't render it
30334
+ if (yPosition === -graphInfo.height || yPosition === graphInfo.height + 20) {
30335
+ return null;
30336
+ }
30337
+
30338
+ // Position of the start of the tick
30339
+ const x1 = xPosition - tickSize / 2;
30340
+ const y1 = yPosition;
30341
+
30342
+ // Position of the end of the tick
30343
+ const x2 = xPosition + tickSize / 2;
30344
+ const y2 = yPosition;
30071
30345
  return /*#__PURE__*/React.createElement("g", {
30072
30346
  className: "y-axis-ticks"
30073
30347
  }, /*#__PURE__*/React.createElement("line", {
30074
- x1: xPosition - tickSize / 2,
30075
- y1: yPosition,
30076
- x2: xPosition + tickSize / 2,
30077
- y2: yPosition,
30348
+ x1: x1,
30349
+ y1: y1,
30350
+ x2: x2,
30351
+ y2: y2,
30078
30352
  style: tickStyle
30079
- }), showTickLabel(gridStep, tickStep, y) && /*#__PURE__*/React.createElement("text", {
30080
- height: 20,
30081
- width: 50,
30082
- textAnchor: "end",
30083
- x: xPosition - 10,
30084
- y: yPosition + 5
30085
- }, y.toString()));
30353
+ }));
30086
30354
  };
30087
30355
  const XGridTick = ({
30088
30356
  x,
30089
- gridStep,
30090
- tickStep
30357
+ graphInfo
30091
30358
  }) => {
30092
- const pointOnAxis = [x, 0];
30359
+ let yPointOnAxis = 0;
30360
+ // If the graph is zoomed in, we want to make sure the ticks are still visible
30361
+ // even if they are outside the graph's range.
30362
+ if (graphInfo.range[1][0] > 0) {
30363
+ // If the graph is on the positive side of the y-axis, lock the ticks to the top of the graph
30364
+ yPointOnAxis = graphInfo.range[1][0];
30365
+ }
30366
+ if (graphInfo.range[1][1] < 0) {
30367
+ // If the graph is on the negative side of the x-axis, lock the ticks to the bottom of the graph
30368
+ yPointOnAxis = graphInfo.range[1][1];
30369
+ }
30370
+ const pointOnAxis = [x, yPointOnAxis];
30093
30371
  const [[xPosition, yPosition]] = useTransformVectorsToPixels(pointOnAxis);
30372
+
30373
+ // If the tick is on the edge of the graph's range, don't render it
30374
+ if (xPosition === -graphInfo.width / 2 || xPosition === graphInfo.width) {
30375
+ return null;
30376
+ }
30377
+
30378
+ // Position of the start of the tick
30379
+ const x1 = xPosition;
30380
+ const y1 = yPosition + tickSize / 2;
30381
+
30382
+ // Position of the end of the tick
30383
+ const x2 = xPosition;
30384
+ const y2 = yPosition - tickSize / 2;
30094
30385
  return /*#__PURE__*/React.createElement("g", {
30095
30386
  className: "x-axis-ticks"
30096
30387
  }, /*#__PURE__*/React.createElement("line", {
30097
- x1: xPosition,
30098
- y1: yPosition + tickSize / 2,
30099
- x2: xPosition,
30100
- y2: yPosition - tickSize / 2,
30388
+ x1: x1,
30389
+ y1: y1,
30390
+ x2: x2,
30391
+ y2: y2,
30101
30392
  style: tickStyle
30102
- }), showTickLabel(gridStep, tickStep, x) && /*#__PURE__*/React.createElement("text", {
30103
- height: 20,
30104
- width: 50,
30105
- textAnchor: "middle",
30106
- x: xPosition,
30107
- y: yPosition + 25
30108
- }, x.toString()));
30393
+ }));
30109
30394
  };
30110
30395
  function generateTickLocations(tickStep, min, max) {
30111
30396
  const ticks = [];
30112
30397
 
30113
30398
  // Add ticks in the positive direction
30114
- for (let i = 0 + tickStep; i < max; i += tickStep) {
30115
- ticks.push(i);
30399
+ for (let _i = 0 + tickStep; _i < max; _i += tickStep) {
30400
+ ticks.push(_i);
30116
30401
  }
30117
30402
 
30118
30403
  // Add ticks in the negative direction
30119
- for (let i = 0 - tickStep; i > min; i -= tickStep) {
30404
+ // Start at the first tick after 0 or the maximum value if it is negative
30405
+ let i = Math.min(max, 0) - tickStep;
30406
+ for (i; i > min; i -= tickStep) {
30120
30407
  ticks.push(i);
30121
30408
  }
30122
30409
  return ticks;
30123
30410
  }
30124
- const AxisTicks = props => {
30125
- const range = props.range;
30411
+ const AxisTicks = () => {
30412
+ const {
30413
+ tickStep,
30414
+ range,
30415
+ width,
30416
+ height
30417
+ } = useGraphConfig();
30418
+ const graphInfo = {
30419
+ range,
30420
+ width,
30421
+ height
30422
+ };
30126
30423
  const [xMin, xMax] = range[0];
30127
30424
  const [yMin, yMax] = range[1];
30128
- const yTickStep = props.tickStep[1];
30129
- const xTickStep = props.tickStep[0];
30425
+ const yTickStep = tickStep[1];
30426
+ const xTickStep = tickStep[0];
30130
30427
  const yGridTicks = generateTickLocations(yTickStep, yMin, yMax);
30131
30428
  const xGridTicks = generateTickLocations(xTickStep, xMin, xMax);
30132
30429
  return /*#__PURE__*/React.createElement("g", {
@@ -30135,15 +30432,13 @@ const AxisTicks = props => {
30135
30432
  return /*#__PURE__*/React.createElement(YGridTick, {
30136
30433
  y: y,
30137
30434
  key: `y-grid-tick-${y}`,
30138
- gridStep: props.gridStep[0],
30139
- tickStep: yTickStep
30435
+ graphInfo: graphInfo
30140
30436
  });
30141
30437
  }), xGridTicks.map(x => {
30142
30438
  return /*#__PURE__*/React.createElement(XGridTick, {
30143
30439
  x: x,
30144
30440
  key: `x-grid-tick-${x}`,
30145
- gridStep: props.gridStep[1],
30146
- tickStep: xTickStep
30441
+ graphInfo: graphInfo
30147
30442
  });
30148
30443
  }));
30149
30444
  };
@@ -30171,11 +30466,7 @@ const Grid = props => {
30171
30466
  yAxis: axisOptions(props, 1)
30172
30467
  }),
30173
30468
  // Only render the axis ticks and arrows if the markings are set to a full "graph"
30174
- props.markings === "graph" && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(AxisTicks, {
30175
- range: props.range,
30176
- tickStep: props.tickStep,
30177
- gridStep: props.gridStep
30178
- }), /*#__PURE__*/React.createElement(AxisArrows, null)));
30469
+ props.markings === "graph" && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(AxisTicks, null), /*#__PURE__*/React.createElement(AxisArrows, null)));
30179
30470
  };
30180
30471
 
30181
30472
  // Displays a stylized open eye: lint warnings are visible
@@ -31182,35 +31473,22 @@ function mafsStateToInteractiveGraph(state) {
31182
31473
  return _extends({}, state);
31183
31474
  }
31184
31475
  const StatefulMafsGraph = /*#__PURE__*/React.forwardRef((props, ref) => {
31476
+ const {
31477
+ onChange
31478
+ } = props;
31185
31479
  const [state, dispatch] = React.useReducer(interactiveGraphReducer, props, initializeGraphState);
31186
31480
  useImperativeHandle(ref, () => ({
31187
31481
  getUserInput: () => getGradableGraph(state, props.graph)
31188
31482
  }));
31189
- function onChange(next) {
31190
- props.onChange(mafsStateToInteractiveGraph(next));
31191
- }
31192
- return /*#__PURE__*/React.createElement(MafsGraph, _extends({}, props, {
31193
- state: state,
31194
- dispatch: dispatch,
31195
- onChange: onChange
31196
- }));
31197
- });
31198
- const MafsGraph = props => {
31199
- const {
31200
- state,
31201
- dispatch,
31202
- labels
31203
- } = props;
31204
- const [width, height] = props.box;
31205
31483
  const prevState = useRef(state);
31206
31484
  useEffect(() => {
31207
31485
  if (prevState.current !== state) {
31208
- props.onChange({
31486
+ onChange(mafsStateToInteractiveGraph({
31209
31487
  graph: state
31210
- });
31488
+ }));
31211
31489
  }
31212
31490
  prevState.current = state;
31213
- }, [props, state]);
31491
+ }, [onChange, state]);
31214
31492
 
31215
31493
  // Destructuring first to keep useEffect from making excess calls
31216
31494
  const [xSnap, ySnap] = props.snapStep;
@@ -31223,11 +31501,26 @@ const MafsGraph = props => {
31223
31501
  useEffect(() => {
31224
31502
  dispatch(changeRange([[xMinRange, xMaxRange], [yMinRange, yMaxRange]]));
31225
31503
  }, [dispatch, xMinRange, xMaxRange, yMinRange, yMaxRange]);
31504
+ return /*#__PURE__*/React.createElement(MafsGraph, _extends({}, props, {
31505
+ state: state,
31506
+ dispatch: dispatch
31507
+ }));
31508
+ });
31509
+ const MafsGraph = props => {
31510
+ const {
31511
+ state,
31512
+ dispatch,
31513
+ labels
31514
+ } = props;
31515
+ const [width, height] = props.box;
31516
+ const tickStep = props.step;
31226
31517
  return /*#__PURE__*/React.createElement(GraphConfigContext.Provider, {
31227
31518
  value: {
31228
31519
  range: state.range,
31229
31520
  snapStep: state.snapStep,
31230
31521
  markings: props.markings,
31522
+ tickStep: tickStep,
31523
+ gridStep: props.gridStep,
31231
31524
  showTooltips: !!props.showTooltips,
31232
31525
  graphDimensionsInPixels: props.box,
31233
31526
  width,
@@ -31241,7 +31534,9 @@ const MafsGraph = props => {
31241
31534
  height,
31242
31535
  position: "relative",
31243
31536
  padding: "25px 25px 0 0",
31244
- boxSizing: "content-box"
31537
+ boxSizing: "content-box",
31538
+ marginLeft: "20px",
31539
+ marginBottom: "20px"
31245
31540
  }
31246
31541
  }, /*#__PURE__*/React.createElement(LegacyGrid, {
31247
31542
  box: props.box,
@@ -31252,11 +31547,11 @@ const MafsGraph = props => {
31252
31547
  bottom: 0,
31253
31548
  left: 0
31254
31549
  }
31255
- }, props.markings === "graph" && /*#__PURE__*/React.createElement(AxisLabels, null), /*#__PURE__*/React.createElement(Mafs, {
31550
+ }, props.markings === "graph" && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(AxisLabels, null), /*#__PURE__*/React.createElement(AxisTickLabels, null)), /*#__PURE__*/React.createElement(Mafs, {
31256
31551
  preserveAspectRatio: false,
31257
31552
  viewBox: {
31258
- x: props.range[0],
31259
- y: props.range[1],
31553
+ x: state.range[0],
31554
+ y: state.range[1],
31260
31555
  padding: 0
31261
31556
  },
31262
31557
  pan: false,
@@ -31266,12 +31561,12 @@ const MafsGraph = props => {
31266
31561
  }, /*#__PURE__*/React.createElement(SvgDefs, null), /*#__PURE__*/React.createElement(Grid, {
31267
31562
  tickStep: props.step,
31268
31563
  gridStep: props.gridStep,
31269
- range: props.range,
31564
+ range: state.range,
31270
31565
  containerSizeClass: props.containerSizeClass,
31271
31566
  markings: props.markings
31272
31567
  }), props.lockedFigures && /*#__PURE__*/React.createElement(GraphLockedLayer, {
31273
31568
  lockedFigures: props.lockedFigures,
31274
- range: props.range
31569
+ range: state.range
31275
31570
  }), renderGraph({
31276
31571
  state,
31277
31572
  dispatch
@@ -43372,7 +43667,7 @@ const init = function init(options) {
43372
43667
 
43373
43668
  // This file is processed by a Rollup plugin (replace) to inject the production
43374
43669
  const libName = "@khanacademy/perseus";
43375
- const libVersion = "22.5.0";
43670
+ const libVersion = "22.6.0";
43376
43671
  addLibraryVersionToPerseusDebug(libName, libVersion);
43377
43672
 
43378
43673
  const apiVersion = {