NAME

parserGraphTool.pl - Allow students to enter basic graphical answers via interactive JavaScript.

DESCRIPTION

GraphTool objects let you provide an interactive graphing tool for students to enter graphical answers.

To create a GraphTool object pass a list of graph objects (discussed below) for the students to graph to GraphTool(). For example:

$gt = GraphTool("{line,solid,(0,0),(1,1)}", "{circle,dashed,(2,2),(4,2)}");

or

$gt = GraphTool("{line,solid,(0,0),(1,1)}")->with(bBox => [-20, 20, 20, -20]);

Then, for standard PG use $gt->ans_rule() to insert the JavaScript graph into the problem (or a print graph when a hard copy is generated), and $gt->cmp to produce the answer checker. For example:

BEGIN_TEXT
Graph the line \(y = x\).
$PAR
\{$gt->ans_rule()\}
END_TEXT

ANS($gt->cmp);

For PGML you can just do

BEGIN_PGML
Graph the line [`y = x`].

[_]{$gt}
END_PGML

GRAPH OBJECTS

The following types of graph objects can be graphed:

points                          (GraphTool::GraphObject::Point)
lines                           (GraphTool::GraphObject::Line)
circles                         (GraphTool::GraphObject::Circle)
parabolas                       (GraphTool::GraphObject::Parabola)
quadratics                      (GraphTool::GraphObject::Qudratic)
cubics                          (GraphTool::GraphObject::Cubic)
intervals                       (GraphTool::GraphObject::Interval)
sine waves                      (GraphTool::GraphObject::SineWave)
triangles                       (GraphTool::GraphObject::Triangle)
quadrilaterals                  (GraphTool::GraphObject::Quadrilateral)
line segments                   (GraphTool::GraphObject::Segment)
vectors                         (GraphTool::GraphObject::Vector)
fills (or shading of a region)  (GraphTool::GraphObject::Fill)

The syntax for each of these objects to pass to the GraphTool constructor is summarized as follows. Each object must be enclosed in braces. The first element in the braces must be the name of the object. The following elements in the braces depend on the type of element.

For points the name "point" must be followed by the coordinates. For example:

"{point,(3,5)}"

For lines the name "line" must be followed by the word "solid" or "dashed" to indicate if the line is expected to be drawn solid or dashed. That is followed by two distinct points on the line. For example:

"{line,dashed,(1,5),(3,4)}"

For circles the name "circle" must be followed by the word "solid" or "dashed" to indicate if the circle is expected to be drawn solid or dashed. That is followed by the point that is to be the center of circle, and then by a point on the circle. For example:

"{circle,solid,(1,1),(4,5)}"

For parabolas the name "parabola" must be followed by the word "solid" or "dashed" to indicate if the parabola is expected to be drawn solid or dashed. The next element in the braces must be the word "vertical" for a parabola that opens up or down, or "horizontal" for a parabola that opens to the left or right. That is followed by the vertex and then another point on the parabola. For example:

"{parabola,solid,vertical,(1,0),(3,3)}"

For three point quadratics the name "quadratic" must be followed by the word "solid" or "dashed" to indicate if the quadratic is expected to be drawn solid or dashed. That is followed by the three points that define the quadratic. For example:

"{quadratic,solid,(-1,2),(1,0),(3,3)}"

For four point cubics the name "cubic" must be followed by the word "solid" or "dashed" to indicate if the cubic is expected to be drawn solid or dashed. That is followed by the four points that define the cubic. For example:

"{cubic,solid,(1,-3),(-1,2),(4,3),(3,2)}"

For fills the name "fill" must be followed by a point in the region that is to be filled. For example:

"{fill,(5,5)}"

For intervals the name "interval" must be followed by a single interval. Some examples are:

"{interval,[3,10)}"
"{interval,(-infinity,8]}"
"{interval,(2,infinity)}"

Note that for an infinite interval endpoint in a correct answer you may use "inf", or anything that is interpreted into a MathObject infinity. However, for static graph objects it must be "infinity". The JavaScript will always return "infinity" for student answers.

For sine waves the name "sineWave" must be followed by the word "solid" or "dashed" to indicate if the sine wave is expected to be drawn solid or dashed. That is followed by a point whose x-coordinate gives the phase shift (or x-translation) and y-coordinate gives the y-translation. The last two elements are the period and amplitude. For Example:

"{sineWave,solid,(2,-4),3,5}"

represents the function f(x) = 5 sin((2 pi / 3)(x - (-4))) + 2.

For triangles the name "triangle" must be followed by the word "solid" or "dashed" to indicate if the triangle is expected to be drawn solid or dashed. That is followed by the three vertices of the triangle. For example:

"{triangle,solid,(-1,2),(1,0),(3,3)}"

For quadrilaterals the name "quadrilateral" must be followed by the word "solid" or "dashed" to indicate if the triangle is expected to be drawn solid or dashed. That is followed by the four vertices of the quadrilateral. For example:

"{quadrilateral,solid,(0,0),(4,3),(2,3),(4,-3)}"

For line segments the name "segment" must be followed by the word "solid" or "dashed" to indicate if the segment is expected to be drawn solid or dashed. That is followed by the two points that are at the ends of the line segment. For example:

"{segment,solid,(0,0),(3,4)}"

For vectors the name "vector" must be followed by the word "solid" or "dashed" to indicate if the vector is expected to be drawn solid or dashed. That is followed by the initial point and the terminal point. For example:

"{vector,solid,(0,0),(3,4)}"

The student answers that are returned by the JavaScript will be a list of the list objects discussed above and will be parsed by WeBWorK and passed to the checker as such. The default checker is designed to grade the graph based on appearance. This means that if a student graphs duplicate objects, then the duplicates are ignored. Furthermore, if two objects are graphed whose only difference is that one is solid and the other is dashed (in this case the dashed object is covered by the solid object and only the solid object is really visible), then the dashed object is ignored.

CUSTOM CHECKERS

A custom list_checker may be provided instead of using the default checker. This can either be passed as part of the cmpOptions hash discussed below, or directly to the GraphTool object's cmp() method.

In a custom list checker the correct and student answers will have the MathObject class GraphObject and will be objects that derive from the GraphTool::GraphObject package (the specific packages for the various objects are listed above). If the == comparison operator is used between these objects it will return true if the objects are visually exactly the same, and false otherwise. For example, if a correct answer is $correct = {line, solid, (0, 0), (1, 1)} and the student graphs the line that is represented by $student = {line, solid, (-2, -2), (3, 3)}, then $correct == $student will be true.

In addition there are two methods all GraphTool::GraphObjects have that are useful.

The first is the pointCmp method. When it is called for most GraphTool::GraphObjects, passing a MathObject point it will return 0 if the point satisfies the equation of the object, -1 if the equation evaluated at the point is negative, and 1 if the equation evaluated at the point is positive. For a segment or vector it will return 0 if it is a point on the segment or vector, 1 if the point is on the segment or vector extended to infinity but not on the segment or vector, and otherwise it will return the same that it would for a line. For a triangle it will return 0 if the point is on an edge, 1 if it is inside, and -1 if it is outside. For a quadrilateral it will return 0 if the point is on an edge, and -1 if it is outside. But if the point is inside then it depends on if the quadrilateral is crossed or not. If the quadrilateral is not crossed it will return 1. If it is crossed, then it will return a positive number that is different depending on which part of the interior it is in. For a fill, the pointCmp method will return 0 if the point is in the same region as the fill point, and 1 otherwise.

The second method is the cmp method. When it is called for a GraphTool::GraphObject object passing it another GraphTool::GraphObject object it will return 1 if the two objects are visually exactly the same, and 0 otherwise (this is equivalent to using the == operator). A second parameter may be passed and if that parameter is 1, then the method will return 1 if the two objects are the same ignoring if the two objects are solid or dashed, and 0 otherwise. For example, if a correct answer is $correct = {line, solid, (0, 0), (1, 1)} and the student graphs the line that is represented by $student = {line, dashed, (-2, -2), (3, 3)}, then $correct->cmp($student, 1) will return 1.

Further note that a GraphTool::GraphObject derives from a MathObject List, and so the things that can be done with MathObject Lists can also be done with GraphTool::GraphObjects.

An example of a custom checker follows:

$m = 2 * random(1, 4);

$gt = GraphTool("{line, solid, ($m / 2, 0), (0, -$m)}")->with(
    cmpOptions => {
        list_checker => sub {
            my ($correct, $student, $ans, $value) = @_;

            my $score = 0;
            my @errors;

            for (0 .. $#$student) {
                if ($correct->[0] == $student->[$_]) { ++$score; next; }

                my $nth = Value::List->NameForNumber($_ + 1);

                if ($student->[$_]->extract(1) ne 'line') {
                    push(@errors, "The $nth object graphed is not a line.");
                    next;
                }

                if ($student->[$_]->extract(2) ne 'solid') {
                    push(@errors, "The $nth object graphed should be a solid line.");
                    next;
                }

                if (!$correct->[0]->pointCmp($student->[$_]->extract(3))
                    || !$correct->[0]->pointCmp($student->[$_]->extract(4)))
                {
                    $score += 0.5;
                    push(@errors,
                        "One of points graphed on the $nth object is incorrect."
                    );
                    next;
                }

                push(@errors, "The $nth object graphed is incorrect.");
            }

            return ($score, @errors);
        }
    }
}

The following is deprecated. Do not use it in new problems. Existing problems that use this approach should be rewritten to use the above approach instead.

The variable $graphToolObjectCmps can be used in a custom checker and contains a hash whose keys are the types of the objects described above, and whose values are methods that can be called passing a MathObject list constructed from one of the objects described above. When one of these methods is called it will return two methods. The first method when called passing a MathObject point will return 0 if the point satisfies the equation of the object, -1 if the equation evaluated at the point is negative, and 1 if the equation evaluated at the point is positive. The second method when called passing another MathObject list constructed from one of the objects described as above will return 1 if the two objects are exactly the same, and 0 otherwise. A second parameter may be passed and if that parameter is 1, then the method will return 1 if the two objects are the same ignoring if the two objects are solid or dashed, and 0 otherwise.

In the following example, the $lineCmp method is defined to be the second method (indexed by 1) that is returned by calling the 'line' method on the first correct answer in the example.

$m = 2 * random(1, 4);

$gt = GraphTool("{line, solid, ($m / 2, 0), (0, -$m)}")->with(
    bBox       => [ -11, 11, 11, -11 ],
    cmpOptions => {
        list_checker => sub {
            my ($correct, $student, $ans, $value) = @_;
            return 0 if $ans->{isPreview};

            my $score = 0;
            my @errors;

            my $lineCmp = ($graphToolObjectCmps->{line}->($correct->[0]))[1];

         for (0 .. $#$student) {
                if ($lineCmp->($student->[$_])) { ++$score; next; }

                my $nth = Value::List->NameForNumber($_ + 1);

                if ($student->[$_]->extract(1) ne 'line') {
                    push(@errors, "The $nth object graphed is not a line.");
                    next;
                }

                if ($student->[$_]->extract(2) ne 'solid') {
                    push(@errors, "The $nth object graphed should be a solid line.");
                    next;
                }

                push(@errors, "The $nth object graphed is incorrect.");
            }

            return ($score, @errors);
        }
    }
}

Note that for 'vector' graph objects the GraphTool object must be passed in addition to the correct 'vector' object to compare to. For example,

my $vectorCmp = ($graphToolObjectCmps->{vector}->($correct->[0], $gt))[1];

This is so that the correct methods can be returned that take into account the vectorsArePositional option that is set for the particular $gt object.

OPTIONS

There are a number of options that you can supply to control the appearance and behavior of the JavaScript graph, listed below. These are set as parameters to the with() method called on the GraphTool object.

bBox (Default: bBox => [-10, 10, 10, -10])

This is an array of four numbers that represent the bounding box of the graph. The first two numbers in the array are the coordinates of the top left corner of the graph, and the last two numbers are the coordinates of the bottom right corner of the graph.

gridX, gridY (Default: gridX => 1, gridY => 1)

These are the distances between successive grid lines in the x and y directions, respectively.

ticksDistanceX, ticksDistanceY (Default: ticksDistanceX => 2, ticksDistanceY => 2)

These are the distances between successive major (labeled) ticks on the x and y axes, respectively.

minorTicksX, minorTicksY (Default: minorTicksX => 1, minorTicksY => 1)

These are the number of minor (unlabeled) ticks between major ticks on the x and y axes, respectively.

scaleX, scaleY (Default: scaleX => 1, scaleY => 1)

These are the scale of the ticks on the x and y axes. That is the distance between two successive ticks on the axis (including both major and minor ticks).

scaleSymbolX, scaleSymbolY (Default: scaleSymbolX => '', scaleSymbolY => '')

These are the scale symbols for the ticks on the x and y axes. The tick labels on the axis will be shown as multiples of this symbol.

This can be used in combination with the scaleX option to show tick labels at multiples of pi, for instance. This can be accomplished using the settings scaleX => pi->value and scaleSymbolX => '\pi'.

xAxisLabel, yAxisLabel (Default: xAxisLabel => 'x', yAxisLabel => 'y')

Labels that will be added to the ends of the horizontal (x) and vertical (y) axes. Note that the values of these options will be used in MathJax online and in LaTeX math mode in print. These can also be set to the empty string '' to remove the labels.

ariaDescription (Default: ariaDescription => '')

This will be added to a hidden div that will be referenced in an aria-describedby attribute of the jsxgraph board.

JSXGraphOptions (Default: undef)

This is an advanced option that you usually do not want to use. It is usually constructed by the macro internally using the above options. If defined it should be a single string that is formatted in JavaScript object notation, and will override all of the above options. It will be passed to the JavaScript graphTool method which will pass it on to the JSX graph board when it is initialized. It may consist of any of the valid attributes documented for JXG.JSXGraph.initBoard at https://jsxgraph.org/docs/symbols/JXG.JSXGraph.html#.initBoard. For example the following value for JSXGraphOptions will give the same result for the JavaScript graph as the default values for the options above:

JSXGraphOptions => Mojo::JSON::encode_json({
    boundingBox => [-10, 10, 10, -10],
    defaultAxes => {
        x => { ticks => { ticksDistance => 2, minorTicks => 1} },
        y => { ticks => { ticksDistance => 2, minorTicks => 1} }
    },
    grid => { gridX => 1, gridY => 1 }
})
snapSizeX, snapSizeY (Default: snapSizeX => 1, snapSizeY => 1)

These restrict the x coordinate and y coordinate of points that can be graphed to being multiples of the respective parameter. These values must be greater than zero.

showCoordinateHints (Default: showCoordinateHints => 1)

Set this to 0 to disable the display of the coordinates. These are in the lower right corner of the graph for the default 2 dimensional graphing mode, and in the top left corner of the graph for the 1 dimensional mode when numberLine is 1.

coordinateHintsType (Default: coordinateHintsType => 'decimal')

This changes the way coordinate hints and axes tick labels are shown. By default these are displayed as decimal numbers accurate to five decimal places. If this is set to 'fraction', then those decimals will be converted and displayed as fractions. If this is set to 'mixed', then those decimals will be converted and displayed as mixed numbers. For example, if the snapSizeX is set to 1/3, then what would be displayed as 4.66667 with the default 'decimal' setting, would be instead be displayed as 14/3 with the 'fraction' setting, and '4 2/3' with the 'mixed' setting. Note that these fractions are typeset by MathJax.

Make sure that the snap size is given with decent accuracy. For example, if the snap size is set to 0.33333, then instead of 1/3 being displayed, 33333/1000000 will be displayed. It is recommended to actually give an actual fraction for the snap size (like 1/3), and let perl and javascript compute that to get the best result.

coordinateHintsTypeX (Default: coordinateHintsTypeX => undef)

This does the same as the coordinateHintsType option, but only for the x-coordinate and x-axis tick labels. If this is undefined then the coordinateHintsType option is used for the x-coordinate and x-axis tick labels.

coordinateHintsTypeY (Default: coordinateHintsTypeY => undef)

This does the same as the coordinateHintsType option, but only for the y-coordinate and y-axis tick labels. If this is undefined then the coordinateHintsType option is used for the y-coordinate and y-axis tick labels.

availableTools (Default: availableTools => [ "LineTool", "CircleTool", "VerticalParabolaTool", "HorizontalParabolaTool", "FillTool", "SolidDashTool" ])

This is an array of tools that will be made available for students to use in the graph tool. The order the tools are listed here will also be the order the tools are presented in the graph tool button box. In addition to the tools listed in the default options above, the following tools may be used:

"PointTool"
three point "QuadraticTool"
four point "CubicTool"
"IntervalTool"
"IncludeExcludePointTool"
"SineWaveTool"
"TriangleTool"
"QuadrilateralTool"
"SegmentTool"
"VectorTool"

Note that the case of the tool names must match what is shown.

staticObjects (Default: staticObjects => [])

This is an array of fixed objects that will be displayed on the graph. These objects will not be able to be moved around. The format for these objects is the same as those that are passed to the GraphTool constructor as the correct answers.

printGraph (Default: undef)

If the JSXGraphOptions option is set directly, then you will also need to provide a function that will generate the corresponding hard copy graph. Otherwise the hard copy graph will still be generated using the above options, and will not look the same as the JavaScript graph.

cmpOptions (Default: cmpOptions => {})

This is a hash of options that will be passed to the cmp() method. These options can also be passed as parameters directly to the GraphTool object's cmp() method.

texSize (Default: texSize => 400)

This is the size of the graph that will be output when a hard copy of the problem is generated.

showInStatic (Default: 1)

In "static" output forms (TeX, PTX) you may not want to print the graph if it is just taking space. In that case, set this to 0.

numberLine (Default: numberLine => 0)

If set to 0, then the graph will show both the horizontal and vertical axes. This is the default. If set to 1, then only the horizontal axis will be shown, and the graph can be interpreted as a number line. In this case the graph will also be displayed with a smaller height.

Note that if this option is set to 1, then some of the options listed above have different default values. The options with different default values and their corresponding default values are:

bBox           => [ -10, 0.4, 10, -0.4 ],
xAxisLabel     => '',
availableTools => [ 'IntervalTool', 'IncludeExcludePointTool' ],

In addition, bBox may be provided as an array reference with only two entries which will be interpreted as a horizontal range. For example,

bBox => [ -12, 12 ]

will give a graph with horizontal extremes -12 and 12.

Note that the horizontal extremes of the number line are interpreted as points at infinity. So in the above example, a point graphed at -12 will be interpreted to be a point at -infinity, and a point graphed at 12 will be interpreted to be a point at infinity.

The only graph objects that will work well with this graphing mode are the "point" and "interval" objects, which are created by the "PointTool" and "IntervalTool" respectively. Usually the "IncludeExcludePointTool" will be desired to control when interval end points are included or excluded from an interval. Of course "interval"s and the "IntervalTool" will not work well if this graph mode is not used.

useBracketEnds (Default: useBracketEnds => 0)

If set to 1, then parentheses and brackets will be used for interval end point delimiters instead of open and closed dots. This option only has effect when numberLine is 1, and the IntervalTool is used.

vectorsArePositional (Default: vectorsArePositional => 0)

If set to 1, then the default checker will consider two vectors that have the same magnitude and direction but different initial points to be different vectors. Otherwise two vectors that have the same magnitude and direction will be considered equal. This option only has effect when a vector is part of the answer, and the VectorTool is used.

useFloodFill (Default: useFloodFill => 0)

If set to 1, then a flood fill algorithm is used for filling regions. The flood fill algorithm fills from the selected point outward and stops at boundaries created by the graphed objects. The alternate fill that is used if useFloodFill is 0 (the default) is an inequality fill. It shades all points that satisfy the same inequalities relative to the graphed objects. The inequality fill algorithm is highly efficient and more reliable, but does not work well and doesn't even make sense with some graph objects. For example, it is quite counter intuitive for quadrilaterals, triangles, line segments and vectors.

METHODS

generateAnswerGraph

This method may be called for a GraphTool object to output a static version of the graph into the problem. The typical place where this might be desired is in the solution for the problem. For example

BEGIN_PGML_SOLUTION
The correct graph is

[@ $gt->generateAnswerGraph(ariaDescription => 'a better description than the default') @]*
END_PGML_SOLUTION

The following options may be passed to this method.

showCorrect

Whether to show correct answers in the graph. This is 1 by default.

cssClass

A css class that will be added to the containing div. The default value is 'graphtool-solution-container'. Note that this default class is provided in the graphtool.css file. A custom class may also be used, and injected into the header via HEADER_TEXT. It is recommended that this class be prefixed with the graph tool answer name to avoid possible conflict with other problems. This may be obtained with $gt->ANS_NAME. This class must set the width and height of the div.graphtool-graph contained within, or the div.graphtool-number-line contained within if numberLine is set. Note that this option is only used in HTML output.

ariaDescription

An aria description that will be added to the graph. The default value is 'graph of solution'. Note that this option is only used in HTML output.

objects

Additional objects to display in the graph. The default value is the empty string.

width and height

The width and height of the answer graph in HTML output. If neither of these are given, then the css class will be used instead. If only one of these is given, then the other will be computed from the given value.

texSize

This is the size of the image that will be output when a hard copy of the problem is generated. The default value is the value of the graph tool object texSize option which defaults to 400.