The student is given two superimposed surfaceplots, and a surface equation (z = f(x,y)) with a number of unkown parameters. One of the surfaceplots corresponds to the “correct” function, while the other lets the student “tune” the unkown parameters. The student needs to find the values of the parameters that makes the two surfaceplots coincide

Question description

If we wanted to randomize the goal function, the parameter values and/or the number of unkown parameters then getting data to and from jsxgraph with the usual {# … #}-notation and binding of sliders/points gets difficult. In this tutorial we will explore how we can deal with dynamic Input/output to jsxgraph.

We will also implement a clickable button that hides/shows an additional jsxgraph-figure and let control elevation/rotation of both figures from the control sliders on one of them.

Pedagogical Motivation

The goal of this excercise is for the student to examine what effect different parameters of a multivariable function has on the plot of its graph.

The move from 2D to 3D challenges the students intuition about functions and their algebraic components. By observing the effect dynamically changing some parameter of a function has on a graph while trying to fit it to a certain shape, we hope to build some intuition on the role of said parameter in a specific function.

Implementation

The version implemented here is supposed to be flexible with regards to which function and which and how many parameter the student is to examine.

The teacher should be able to choose any kind of function with a set of parameters to be examined by creating a function and a couple of lists in maxima. The only parts of the javascript code that could need editing is the parts that determines the size of the axes.

Student View

Screenshot - Tune 3D Function

The student needs to manipulate the sliders corresponding to a parameter in the function such that the two surfaceplots coincide. Depending on the functions, this can initially be difficult when the plots are occupying the same figure. In that case the student can click a button that splits the graphs into to seperate figures, and merge them again for “fine- tuning”. When the graphs are split the rotation/elevation controls of the top figure controls the viewing angles of both plots. The student can also “scale” the z-axis to aid in a clear view of the plots.

Question Code

Question Variables

/*Non-randomized small example*/

func: a*x^2+b*x*y+c*y^2;
all_params: [a,b,c];
coef: [1,2,3];
params: [b];
param_sliders: [-4,0,4];

/*Randomized big example*/
/*
func: a*x^2+b*y^2+c*x*y+d*x+f*y+g;
all_params: [a,b,c,d,f,g];
coef: random_permutation(append([0,0],makelist(rand_with_prohib(-4,4,[0]),4)));
params: rand_selection(all_params,2);
param_sliders: [-4,0,4];
*/

/*x- and y- ranges */
xrang:[-7,7];
yrang:[-7,7];

/* Max error for tunable parameters */
maxError:0.2;

/*List of non-tunable parameters*/
non_params: sublist(all_params, lambda([x], not member(x, params)));

/*Stack-map, example: ["stack-map",
   ["a", 1], ["b", 2], ["pnames", ["a","b"]], ["sliders, [-4,0,4]]
]*/
params_json: append(["stack_map"],
   makelist([string(all_params[i]),coef[i]], i, length(all_params)),
   [["pnames", map(string,params)]], [["sliders", param_sliders]]
);

/*Parameter equation lists, example [a=1, b=2, c=3]*/
all_params_eq: makelist(all_params[i] = coef[i], i, length(all_params));
params_eq: sublist(all_params_eq, lambda ([x], member(lhs(x), params)));
non_params_eq: sublist(all_params_eq, lambda([x], member(lhs(x), non_params)));

/*Function evaluated with un-tunable parameters*/
fxy:ev(func,non_params_eq);

/*Function evaluated with all parameters given*/
fxy2:ev(func,all_params_eq);

In the question variables, the following part is expected to be changed by the teacher

func: a*x^2+b*y^2+c*x*y+d*x+f*y+g;
all_params: [a,b,c,d,f,g];
coef: random_permutation(append([0,0],makelist(rand_with_prohib(-4,4,[0]),4)));
params: rand_selection(all_params,2);
param_sliders: [-4,0,4];
xrang:[-7,7];
yrang:[-7,7];
maxError:0.2;

The variable func is the goal function, where the list all_params lists the name of all “potentially unknown” parameters. The list coef holds all the parameters values. In the example above, we have a 2. order polynomial with 6 parameters and they are given a random value between \([-4,4]\), excluding 0, with two parameters guaranteed to be zero. The list params holds the names of the unkown parameters, in the above example we draw two random parameters.

The last 4 lines above sets the ranges and starting values of the sliders, the x-, and y- domain and the tolerance of the students answer.

The rest of the maxima code should not need to be changed, and does a couple of things. It creates som formatted parameter lists and two functions. fxy is the function with only the known parameters evaluated, fxy2 is the function with all parameters evaluated.

To get the data from maxima to jsxgraph, the easiest way usually is to use {#<CAS expression>#} syntax. This “pastes” the maxima result between # directly into the javascript-code. This would probably suffice in this case, but if the data is big/complex/dynamic it could be a good idea to use a stackmap in maxima that can be converted and parsed into a JSON object in the jsxgraph block.

A strackmap consists of a list that starts with the string “stackmap” followed by a variable number of sublists of length 2. The first element of the sublists is the “key” and the second element is the “value”.

params_json: append(["stack_map"],
   makelist([string(all_params[i]),coef[i]], i, length(all_params)),
   [["pnames", map(string,params)]], [["sliders", param_sliders]]
);

The above code creates the stackmap. It holds the the value of the parameters, the names of the parameters and the slider sizes. It could look something like this:

["stackmap", 
    ["a",2],["b", 1],["c",-3], 
    ["pnames", ["a","b"]], 
    ["sliders", [-4,0,4]]
]

This can be converted and parsed into a JSON object, which we use to access the data from maxima:

 var stack_input = JSON.parse({#stackjson_stringify(params_json)#});
 var paramater_names = stack_input["pnames"];
 var param_a_value = stack_input["a"];

Question Text

Usually would want one hidden input box bound to each slider that is part of the students answer. In this case we might not know the name of, or more importantly, the number of sliders. In such cases we can manually “bind” all the relevant data form jsxgraph to a single answer variable/input box:

<p style="display:none">[[input:ans]] [[validation:ans]]</p>

The question text proper is straight forward.

We are given the function \(F(x,y) = {@fxy@}\) where 
the parameters \({@simplode(params,",")@}\) are unknown. 
The figure below shows the correct function, along with a function 
where you can tune the unknown parameters with the sliders.
Find the correct function by manipulating the sliders.

The critical part is the javascript code, in [[jsxgraph]] tags.

Coding the Plot

[[jsxgraph input-ref-ans="ansRef"]]
(function () {

   //Get divid for bottom jsxgraph figure by 
   //incrementing number in divid of top figure
   //
   //Regex that matches number between 1-99
   var regexMatchNumber = /([1-9][0-9]|[1-9])/g; 
   var nextDivNum = Number(divid.match(regexMatchNumber))+1;//Match+increment
   var divid2 = divid.replace(regexMatchNumber, nextDivNum);//Replace new number
   
   //Reference to the bottm jsxgraph figure
   var jsxgraph2 = document.getElementById(divid2);
   jsxgraph2.style.display="none";//Hide second figure by default


   //{#stackjson_stringify(params_json)#} inserts JSON string from maxima
   //JSON.parse() returns a JSON object of the data from maxima
   var stack_input = JSON.parse({#stackjson_stringify(params_json)#});
   var ans = document.getElementById(ansRef);//Reference to answer field
   ans.value= JSON.stringify(stack_input); //Write JSON object to ans as string

   // Create the board
   var board = JXG.JSXGraph.initBoard(divid, {
      boundingbox: [-11 ,11, 11, -11],
      keepaspectratio: false,
      axis: false
   });


   //Create the 3D view 
   var box = [-2, 2];
   var boxx = {#xrang#};
   var boxy = {#yrang#};
   var view = board.create('view3d', [[-6, -3], [8, 8], [boxx, boxy, box]], {
      xPlaneRear: {visible: false},
      yPlaneRear: {visible: false},
   });

   //Array of strings containing the names of the tunable parameters 
   //Example: ["a", "c",...]
   var pnames = stack_input["pnames"];

   //Start, initial and stop value of sliders
   var slider_size = stack_input["sliders"];

   var sliders = []; //Array holding a reference to sliders
   
   //For several sliders, we need to shift the y-position so they don't overlap
   var slider_shift = 0 

   // Iterate over the the names of the tunable parameters
   for (var i=0; i<pnames.length; i++){

      //Set the initial tunable parameter value to startvalue of sliders
      stack_input[pnames[i]] = slider_size[1];
      //Write new parameter values to answer field
      ans.value = JSON.stringify(stack_input);

      
      //Create and push new slider to list of sliders
      sliders.push(board.create('slider',  
         [[-7, -6-slider_shift], [5, -6-slider_shift], slider_size], 
         { name: pnames[i] }//New slider has same name as the parameter
      ));
      slider_shift++; //Shift y-position for next slider

      board.update();
   }

   //Create the scaleing slider for the function plot
   var scale_slider = board.create('slider', 
      [[-7,-6-slider_shift],[5,-6-slider_shift], [0,1,2]], 
      {name: "Scale z-axis"}
   );

   // JessieCode parsing of maxima function "fxy" - tunable
   var ff = board.jc.snippet('{#fxy#}', true, 'x,y', true);  
   
   // The function f calls the tunable function ff, andj scales it 
   // by mulitplying with z-axis slider 
   var f = (x,y) => scale_slider.Value()*ff(x,y);

   //Create the 3D tunable surface
   view.create('functiongraph3d', [f, boxx, boxy], {
      strokeColor: JXG.palette.blue,
      stepsU: 50, stepsV: 50, strokeWidth: 0.5
   });


   //JessieCode parsing of maxima function "fxy2"- non-tunable
   var gg = board.jc.snippet('{#fxy2#}', true, 'x,y', true); 
   // g(x,y) calls gg(x,y) but scales it with the slider
   var g = (x,y) => scale_slider.Value()*gg(x,y);

   //Create the 3D non-tunable function
   var func_g = view.create('functiongraph3d', [g, boxx, boxy], {
      strokeColor: JXG.palette.red,
      stepsU: 50, stepsV: 50, strokeWidth: 0.5
   });

   //Create a second board for the jsxgraph figure below (divid2)
   var board2 = JXG.JSXGraph.initBoard(divid2, {
      boundingbox: [-8, 8, 8, -8],
      keepaspectratio: false,
      axis: false
   });

   //Add board2 as a child to board - the bottom figure updates automatically
   //when the student manipulates the top figure
   board.addChild(board2);

   //Create 3D view for botoom figure
   var view2 = board2.create('view3d', 
      [[-6, -3], [8, 8], [boxx, boxy, box]], {
      xPlaneRear: {visible: false},
      yPlaneRear: {visible: false},
   });
   
   //Create 3D non-tunable surface in bottom figure
   view2.create('functiongraph3d', [g, boxx, boxy], {
      strokeColor: JXG.palette.red,
      stepsU: 50, stepsV: 50, strokeWidth: 0.5
   });
   board2.update();

   //We want to control rotation and elevation of bottom figure
   //with the controls in the top figure
   //Hide the controls in bottom figure
   view2.D3.az_slide.hide();
   view2.D3.el_slide.hide();
   //Set the rotation and elevation of bottom figure to 
   //that of the first figure
   view2.D3.az_slide = view.D3.az_slide;
   view2.D3.el_slide = view.D3.el_slide;

   //Rename elevation and rotation control sliders
   view.D3.el_slide.name = "elevation";
   view.D3.az_slide.name="rotate";
   board.update();

   // Set event listener for button that allows student 
   // to split the tunable and non-tunable surfaces into different figures
   //
   // Get reference to clickable button
   var button = document.getElementById('split-button');
   //Add the function to be called when the button is clicked
   button.addEventListener('click', function() { 
      
      // If value=0 we hide the goal-plot and show the bottom figure
      if (button.value == "0"){
         func_g.hide();                     //Hide target function 
         button.value = "1";                //Toggle button value
         button.textContent = "Merge";      //Toggle button text
         jsxgraph2.style.display = "block"; //Show bottom figure
      }
      else { //If value != 0 we show the goal-plot and hide the bottom figure
         func_g.show();                    //Show target function
         button.value="0";                 //Toggle button value
         button.textContent = "Split";     //Toggle button text
         jsxgraph2.style.display = "none"; //Hide bottom figure
      }
   });

   //Add a function that updates the answer variable going out to STACK
   //everytime the board changes
   board.on('update', function(){
      for (let i=0; i<sliders.length; i++) //Iterate over tunable sliders
      {
         var ans_name = sliders[i].getAttribute("name"); //Get name of slider
         stack_input[ans_name] = sliders[i].Value();  //Update value of parameter
      }
      //Write JSON object with updated parameter values to stack answer field
      ans.value=JSON.stringify(stack_input);
   });

})();	
[[/jsxgraph]]

The jsxgraph block starts with

[[jsxgraph input-ref-ans="ansRef"]]

In the opening jsxgraph block we specify that we want a reference to the STACK answerfield “ans” stored in a variable “ansRef”

Then we get the ID of the bottom jsxgraph figure. We want the students to be able to toggle between showing both graphs in a single figure or seperate figures by clicking a button. The code for both plots is contained in the first jsxgraph block. The ID for the jsxgraphblock is contained in divid and has value “stack-jsxgraph-X”, where X is a number controlled by stack, depending on how many jsxgraph figures is on the quiz page. We can get the ID of the bottom graph by incrementing X for the first graph, which we do with regex:

   var regexMatchNumber = /([1-9][0-9]|[1-9])/g; 
   var nextDivNum = Number(divid.match(regexMatchNumber))+1;//Match+increment
   var divid2 = divid.replace(regexMatchNumber, nextDivNum);//Replace new number

We use divid2 to get a reference to the bottom figure, in order to hide/show it.

   var jsxgraph2 = document.getElementById(divid2);
   jsxgraph2.style.display="none";//Hide second figure by default

The two boards an 3Dviews can now be created in their respective figures

   var board = JXG.JSXGraph.initBoard(divid, {...
   var board2 = JXG.JSXGraph.initBoard(divid2, { ...
   var view = board.create('view3d', ....
   var view2 = board2.create('view3d', ....

Next we nee to get our data from maxima. The maximafunction stackjson_stringify(params_json) encodes the stackmap into a JSON string, which we can parse into a JSON object with JSON.parse()

   var stack_input = JSON.parse({#stackjson_stringify(params_json)#});
   var ans = document.getElementById(ansRef);//Reference to answer field
   ans.value= JSON.stringify(stack_input); //Write JSON object to ans as string

We also get the answerfield element by using our supplied “ansRef” reference, and store our stack_input object as string in the answerfield with JSON.stringify. The goal is to hand the position of the sliders back to stack, by updating the stack_input object with the current slider values and write the entire object back to the answerfield.

From the stack_input object we can get the slider sizes and the parameter names:

   var pnames = stack_input["pnames"];
   var slider_size = stack_input["sliders"];

We can now create the sliders for the tunable parameters:

   var sliders = []; //Array holding a reference to sliders
   
   //For several sliders, we need to shift the y-position so they don't overlap
   var slider_shift = 0 

   // Iterate over the the names of the tunable parameters
   for (var i=0; i<pnames.length; i++){

      //Set the initial tunable parameter value to startvalue of sliders
      stack_input[pnames[i]] = slider_size[1];
      //Write new parameter values to answer field
      ans.value = JSON.stringify(stack_input);

      
      //Create and push new slider to list of sliders
      sliders.push(board.create('slider',  
         [[-7, -6-slider_shift], [5, -6-slider_shift], slider_size], 
         { name: pnames[i] }//New slider has same name as the parameter
      ));
      slider_shift++; //Shift y-position for next slider

      board.update();
   }

The sliders are given the same name as the tunable parameters, which means they will affect the functionplot. Also note that we set the parameter values in stack_input to the initial value of the corresponding slider and write it to the answerfield. Otherwise the answerfield would initially contain the correct values.

Next we parse the functionstrings fxy and fxy2 from maxima with with <board>.jc.snippet(), create the scaled function to be plotted and plot the functions in the same manner as in previous tutorials.

The point of having the option to view the plots in seperate figures, is that it can be messy and difficult to match the plots in the same figure initially. We also want the same rotation and elevation angles in the two plots at all times, which we can accomplish by hiding the el. az. controls in the bottom figure and set the controls in the first figure to also control the bottom figure:

   view2.D3.az_slide.hide();
   view2.D3.el_slide.hide();
   
   view2.D3.az_slide = view.D3.az_slide;
   view2.D3.el_slide = view.D3.el_slide;

   //Rename elevation and rotation control sliders
   view.D3.el_slide.name = "elevation";
   view.D3.az_slide.name="rotate";

The button itself is added in html between the two jsxgraph-blocks:

<button type="button" value="0" id="split-button"> Split </button>

Its logic is added made as follows:

   var button = document.getElementById('split-button');
   //Add the function to be called when the button is clicked
   button.addEventListener('click', function() { 
      
      // If value=0 we hide the goal-plot and show the bottom figure
      if (button.value == "0"){
         func_g.hide();                     //Hide target function 
         button.value = "1";                //Toggle button value
         button.textContent = "Merge";      //Toggle button text
         jsxgraph2.style.display = "block"; //Show bottom figure
      }
      else { //If value != 0 we show the goal-plot and hide the bottom figure
         func_g.show();                    //Show target function
         button.value="0";                 //Toggle button value
         button.textContent = "Split";     //Toggle button text
         jsxgraph2.style.display = "none"; //Hide bottom figure
      }
   });

The final piece of the puzzle is to make sure to write an updated JSON object as a string to the answerfield everytime the student manipulates the figure:

   board.on('update', function(){
      for (let i=0; i<sliders.length; i++) //Iterate over tunable sliders
      {
         var ans_name = sliders[i].getAttribute("name"); //Get name of slider
         stack_input[ans_name] = sliders[i].Value();  //Update value of parameter
      }
      //Write JSON object with updated parameter values to stack answer field
      ans.value=JSON.stringify(stack_input);
   });

Input:ans

Input variable settings
Input setting for answervariable “ans”

For the answer variable ans, we set the input type to “String”. The model answer is not really applicable since the variable stores several things, and we put “NA” here. We also set the option “hideanswer” in the extra options box, to stop the student getting freedback regarding the answer variable

Partial Response Tree

Feedback variables

The feedback variables does two main things: It parses the JSON-string contained in the answer variables and collect the student’s answer, and it checks if the tunable parameters given by the student are within the set tolerance maxErrorof the correct values.

/*Parse json-string from jsxgraph*/
tmp:stackjson_parse(ans);

/*Student answer example [[a,4], [b,6], ... ]*/
sanswer:map(lambda([x], [x,stackmap_get(tmp, string(x))]), params);

/*Teacher answer */
tanswer:map(lambda([x], [lhs(x), rhs(x)]), params_eq);

/*Find parameterval for par in tanswer*/
findParameterVal(par):=
   (sublist(tanswer, lambda([x], is(equal(x[1],par)))))[1][2];

/*List of errors in the parameters, example [[a,0], [b,0.35], [c,0]] */
errors:makelist( [sanswer[i][1],
   abs(sanswer[i][2]-findParameterVal(sanswer[i][1]))],
   i,1,length(sanswer));

/*List of correct parameters (error<maxError) */
correct:sublist(errors, lambda([x], x[2]<maxError));
/*Formatted list of correct parameters */
correct_list:sublist(params_eq,
   lambda([x], member(lhs(x), map(first, correct))));

/*List of incorrect parameters*/
incorrect:sublist(errors, lambda([x], x[2]>=maxError));

/*Number of correct answers*/
n_stud:length(correct);
n_ans:length(tanswer);

To get a hold of the variables in the JSON string, we use tmp:stackjson_parse(ans). This parses the JSON string from jsxgraph and returns a stackmap. We can then use the function stackmap_get(<stackmap>, <key>) to lookup the value of a key in the stackmap.

We look up the values for all tunable parameters and store them in a list sanswer = [[<parameter>, <value>], ...] here:

sanswer:map(lambda([x], [x,stackmap_get(tmp, string(x))]), params);

The list tanswer is of the same form, and contains the correct parameter names/values.

Next we calculate a list of the student’s error and filter out the parameters whose error is above the tolerance.

findParameterVal(par):=
   (sublist(tanswer, lambda([x], is(equal(x[1],par)))))[1][2];

errors:makelist( [sanswer[i][1],
   abs(sanswer[i][2]-findParameterVal(sanswer[i][1]))],
   i,1,length(sanswer));

correct:sublist(errors, lambda([x], x[2]<maxError));
correct_list:sublist(params_eq,
   lambda([x], member(lhs(x), map(first, correct))));

We have to use the function findParameterVal(par) to look up the correct parameter value in tanswer as the parameternames not necessarily are in the same order after we in the question variables form chose 2 random parameters.

The variables n_stud and n_ans hold the number of correctly tuned parameters, and the total number of parameters, respectively

Response Tree Nodes

The response tree is quite straightforward - It compares the number of correct parameters from the student n_stud with the total number of tunable parameters n_ans. If they are equal, the student gets full marks. If the student gets at least 1 parameter right they get half marks, and otherwise no marks.