JSXGraph in Moodle Formulas with two boards: Binding to input fields not working

382 views Asked by At

I have created a Moodle Formulas questions in the field of kinematics with two boards. While I managed to get simpler questions with only one board to work flawlessly, the problem with this question is that the bound values are not inserted into formula's input entry fields. Consequently, the student cannot submit an answer because, effectively, nothing has been filled out. The rest of the question works though, as can be seen when the correct answers are filled in the question's preview.

I provide a Moodle XML file to make it easier to reproduce the problem: questions_formulas_JSXGraph_2boards.xml
You need a current version of Moodle with JSXGraph filter and question type Formulas installed.

The main JSXGraph code is this:

<jsxgraph width="400" height="300" numberOfBoards="2" ext_formulas>

// JavaScript code to create the construction.
var jsxCode = function (question) {
  
  // Import final coordinates after submission
  var x0={x0};
  var t1,t2,t3 , v1,v2,v3 , x1,x2,x3;
  [t1,t2,t3 , v1,v2,v3 , x1,x2,x3] = 
  question.getAllValues([1,2,3 , 1,2,3 , x0,x0,x0 ]);
  
  JXG.Options.point.infoboxDigits = 1;
  JXG.Options.point.snapSizeX = 1;
  JXG.Options.point.snapSizeY = 0.1;
  
  // Create boards
  var brd0 = JXG.JSXGraph.initBoard(BOARDID0, { 
    boundingbox: [-1, 11, 12, -11], axis:true,
    defaultAxes: {
      x: {withLabel: true, name: 't in s',
          label: {position: 'rt', offset: [-0, 15], anchorX: 'right'} },
      y: {withLabel:true, name: 'x in m',      
          label: {position: 'rt', offset: [+15, -0]} } },
      showCopyright: false, showNavigation: false 
    });
    
  var brd1 = JXG.JSXGraph.initBoard(BOARDID1, { 
    boundingbox: [-1, 3.5, 12, -3.5], axis:true,
    defaultAxes: {
      x: {withLabel: true, name: 't in s',
          label: {position: 'rt', offset: [-0, 15], anchorX: 'right'} },
      y: {withLabel:true, name: 'v_x in m/s',      
          label: {position: 'rt', offset: [+15, -0]} } },
      showCopyright: false, showNavigation: false
    });
      
  // Board brd0 needs to be updated when changes in brd1 occur
  brd1.addChild(brd0);
  
  // Attributes for points and lines
  function attrPfix(addAttr={}) {
    const attr = {fixed: true, visible: false, withLabel: false};
    return { ...attr, ...addAttr}; 
  }
  function attrPmov(addAttr={}) {
    const attr = {fixed: question.isSolved, snapToGrid: true, withLabel: false};
    return { ...attr, ...addAttr};
  }
  function attrPsma(addAttr={}) {
    const attr = {visible: true, withLabel: false, color:'#4285F4', size: 1};
    return { ...attr, ...addAttr};
  }
  const attrLine = {borders: {strokeColor:'#4285F4', strokeWidth: 3} };
  const attrGlid = {visible:false};


  // Define lines and points on brd1
  brd1.suspendUpdate();
  var lV0 = brd1.create('segment', [[0,-10], [0,10]], {visible:false}),
      lV3 = brd1.create('segment', [[-10,0], [20,0]], {visible:false});
  var pV0 = brd1.create('glider', [0, v1, lV0], attrPmov({name: "pV0"}) ),
      pV1 = brd1.create('point', [t1, v2], attrPmov({name: "pV1"}) ),
      pV2 = brd1.create('point', [t2, v3], attrPmov({name: "pV2"}) ),
      pV3 = brd1.create('glider', [t3, 0, lV3], attrPmov({name: "pV3"}) ),
      pV01 = brd1.create('point', ["X(pV1)", "Y(pV0)"], attrPsma() ),
      pV12 = brd1.create('point', ["X(pV2)", "Y(pV1)"], attrPsma() ),
      pV23 = brd1.create('point', ["X(pV3)", "Y(pV2)"], attrPsma() )  ;
  brd1.create('polygonalchain', [ pV0, pV01, pV1, pV12, pV2, pV23, pV3 ], attrLine);
  brd1.unsuspendUpdate();

  // Define lines and points on brd1
  // Q: Is it necessary/beneficial/wrong to suspendUpdate here?
  brd0.suspendUpdate();
  var lX1 = brd0.create('line', [[function(){return pV1.X();},-10], [function(){return pV1.X();},10]], attrGlid),
      lX2 = brd0.create('line', [[function(){return pV2.X();},-10], [function(){return pV2.X();},10]], attrGlid),
      lX3 = brd0.create('line', [[function(){return pV3.X();},-10], [function(){return pV3.X();},10]], attrGlid);
  var pX0 = brd0.create('point', [0, x0], attrPsma({fixed: true}) ),
      pX1 = brd0.create('glider', [t1, x1, lX1], attrPmov({face: 'diamond'}) ),
      pX2 = brd0.create('glider', [t2, x2, lX2], attrPmov({face: 'diamond'}) ),
      pX3 = brd0.create('glider', [t3, x3, lX3], attrPmov({face: 'diamond'}) );
  brd0.create('polygonalchain', [ pX0, pX1, pX2, pX3 ], attrLine);
  brd0.unsuspendUpdate();

  // Q: Are these updates necessary?
  brd0.update();
  brd1.update();

  // Whenever the construction is altered the values of the points are sent to formulas.
  question.bindInput(0, () => { return pV1.X(); });
  question.bindInput(1, () => { return PV2.X(); });
  question.bindInput(2, () => { return pV3.X(); });
  question.bindInput(3, () => { return pV1.Y(); });
  question.bindInput(4, () => { return pV2.Y(); });
  question.bindInput(5, () => { return PV3.Y(); });
  question.bindInput(6, () => { return pX1.Y(); });
  question.bindInput(7, () => { return pX2.Y(); });
  question.bindInput(8, () => { return pX3.Y(); });
  };
  
  // Execute the JavaScript code.
  new JSXQuestion(BOARDID0, jsxCode, allowInputEntry=true);
  
</jsxgraph>

Is it possible that the problem is caused because the board ids are not properly handed over in

new JSXQuestion(BOARDID0, jsxCode, allowInputEntry=true);

Besides this problem, I would like to understand JSXGraph a bit better:

  1. Is it somehow possible to arrange multiple boards with respect to each other? That is, above, below, right aligned, centered etc.
  2. Does it make a difference whether boards are initialized as ‘const’ or ‘var’?
  3. Is it necessary/beneficial/wrong to suspend and unsuspend board updates in the example above case?
  4. Are the manual update commands in the code necessary/beneficial/useless?
  5. Are there any obvious lapses in my coding or usage of JSXGraph?
3

There are 3 answers

7
Andreas On BEST ANSWER

I now had the time to look at your problem and I was able to expand the Moodle filter. As of the new version v1.1.0-for3.10, several boards are also supported in formulas. You can find detailed instructions on how to use it and what to consider here on GitHub.

The new version of the plugin can be downloaded in the Plugins Directory.

I took the liberty of modifying your example from above and it works for me:

<jsxgraph width="400" height="300" numberOfBoards="2" ext_formulas>

// JavaScript code to create the construction.
var jsxCode = function (question) {
  
  // Import final coordinates after submission
  var x0={x0};
  var t1,t2,t3 , v1,v2,v3 , x1,x2,x3;
  [t1,t2,t3 , v1,v2,v3 , x1,x2,x3] = 
  question.getAllValues([1,2,3 , 1,2,3 , x0,x0,x0 ]);
  
  JXG.Options.point.infoboxDigits = 1;
  JXG.Options.point.snapSizeX = 1;
  JXG.Options.point.snapSizeY = 0.1;
  
  // Create boards
  var brds = question.initBoards( [
  { // attribs for BOARDID0 
    boundingbox: [-1, 11, 12, -11], axis:true,
    defaultAxes: {
      x: {withLabel: true, name: 't in s',
          label: {position: 'rt', offset: [-0, 15], anchorX: 'right'} },
      y: {withLabel:true, name: 'x in m',      
          label: {position: 'rt', offset: [+15, -0]} } },
      showCopyright: false, showNavigation: false 
    },
    { // attribs for BOARDID1 
    boundingbox: [-1, 3.5, 12, -3.5], axis:true,
    defaultAxes: {
      x: {withLabel: true, name: 't in s',
          label: {position: 'rt', offset: [-0, 15], anchorX: 'right'} },
      y: {withLabel:true, name: 'v_x in m/s',      
          label: {position: 'rt', offset: [+15, -0]} } },
      showCopyright: false, showNavigation: false
    }
  ] );

  var brd0 = brds[0];
  var brd1 = brds[1];
  console.log(brd0, brd1);
      
  // Board brd0 needs to be updated when changes in brd1 occur
  question.addChildsAsc();
  /* not needed anymore
    brd1.addChild(brd0);
  */
  
  // Attributes for points and lines
  function attrPfix(addAttr={}) {
    const attr = {fixed: true, visible: false, withLabel: false};
    return { ...attr, ...addAttr}; 
  }
  function attrPmov(addAttr={}) {
    const attr = {fixed: question.isSolved, snapToGrid: true, withLabel: false};
    return { ...attr, ...addAttr};
  }
  function attrPsma(addAttr={}) {
    const attr = {visible: true, withLabel: false, color:'#4285F4', size: 1};
    return { ...attr, ...addAttr};
  }
  const attrLine = {borders: {strokeColor:'#4285F4', strokeWidth: 3} };
  const attrGlid = {visible:false};


  // Define lines and points on brd1
  brd1.suspendUpdate();
  var lV0 = brd1.create('segment', [[0,-10], [0,10]], {visible:false}),
      lV3 = brd1.create('segment', [[-10,0], [20,0]], {visible:false});
  var pV0 = brd1.create('glider', [0, v1, lV0], attrPmov({name: "pV0"}) ),
      pV1 = brd1.create('point', [t1, v2], attrPmov({name: "pV1"}) ),
      pV2 = brd1.create('point', [t2, v3], attrPmov({name: "pV2"}) ),
      pV3 = brd1.create('glider', [t3, 0, lV3], attrPmov({name: "pV3"}) ),
      pV01 = brd1.create('point', ["X(pV1)", "Y(pV0)"], attrPsma() ),
      pV12 = brd1.create('point', ["X(pV2)", "Y(pV1)"], attrPsma() ),
      pV23 = brd1.create('point', ["X(pV3)", "Y(pV2)"], attrPsma() )    ;
  brd1.create('polygonalchain', [ pV0, pV01, pV1, pV12, pV2, pV23, pV3 ], attrLine);
  brd1.unsuspendUpdate();

  // Define lines and points on brd1
  // Q: Is it necessary/beneficial/wrong to suspendUpdate here?
  // A: It can be beneficial if you use a lot of objects. In this case the benefit is not worth mentioning, I think.
  brd0.suspendUpdate();
  var lX1 = brd0.create('line', [[function(){return pV1.X();},-10], [function(){return pV1.X();},10]], attrGlid),
      lX2 = brd0.create('line', [[function(){return pV2.X();},-10], [function(){return pV2.X();},10]], attrGlid),
      lX3 = brd0.create('line', [[function(){return pV3.X();},-10], [function(){return pV3.X();},10]], attrGlid);
  var pX0 = brd0.create('point', [0, x0], attrPsma({fixed: true}) ),
      pX1 = brd0.create('glider', [t1, x1, lX1], attrPmov({face: 'diamond'}) ),
      pX2 = brd0.create('glider', [t2, x2, lX2], attrPmov({face: 'diamond'}) ),
      pX3 = brd0.create('glider', [t3, x3, lX3], attrPmov({face: 'diamond'}) );
  brd0.create('polygonalchain', [ pX0, pX1, pX2, pX3 ], attrLine);
  brd0.unsuspendUpdate();

  // Q: Are these updates necessary?
  /* not with the new version
    brd0.update();
    brd1.update();
  */

  /* not necessary anymore
    question.board = brd0;
  */

  // Whenever the construction is altered the values of the points are sent to formulas.
  question.bindInput(0, () => { return pV1.X(); });
  question.bindInput(1, () => { return pV2.X(); }); // typo here
  question.bindInput(2, () => { return pV3.X(); });
  question.bindInput(3, () => { return pV1.Y(); });
  question.bindInput(4, () => { return pV2.Y(); });
  question.bindInput(5, () => { return pV3.Y(); }); // typo here
  question.bindInput(6, () => { return pX1.Y(); });
  question.bindInput(7, () => { return pX2.Y(); });
  question.bindInput(8, () => { return pX3.Y(); });
  };
  
  // Execute the JavaScript code.
  new JSXQuestion(BOARDIDS, jsxCode, allowInputEntry=true); // use BOARDIDS here!!
  
</jsxgraph>

I've already answered the other questions in the code.

I hope I could help you!

Greetings, Andreas

1
Andreas On

In fact, it is correct that our filter in combination with formulas does not work correctly with multiple boards. At the moment, only one board ID is transferred to the JSXQuestion object and thus and thus it (and formulas) does not know anything about the second board. That is also one of the problems your example raises.

In addition, the boards actually have to be initialized with the JSXQuestion.initBoard() method for the bindInput() method to work. In the end, that's the root problem why your example doesn't work.

I will dedicate myself to this issue after the Christmas holidays and will be releasing a new version of the Moodle filter in January. Maybe there will be something new from JSXGraph by then, too.

Unfortunately, I can't offer you a dirty hack until then, as it requires a few basic changes to the filter.

I hope to be able to tell you more in January. Have a nice Christmas and stay healthy!

Andreas

3
FlorianL On

Just for completeness sake, I post my final version of the JSXGraph code for a Formulas question based on Andreas' solution. My final touches were to

  1. make the axis labels use LaTeX,
  2. use the event handler .on('drag', ...) for two-way updates between the diagrams instead of using question.addChildsAsc().

Here is the final code:

<jsxgraph width="400" height="300" numberOfBoards="2" ext_formulas>

// JavaScript code to create the construction.
var jsxCode = function (question) {
  
  // Import final coordinates after submission
  var x0={x0};
  var t1,t2,t3 , v1,v2,v3 , x1,x2,x3;
  [t1,t2,t3 , v1,v2,v3 , x1,x2,x3] = 
  question.getAllValues([1,2,3 , 1,2,3 , x0,x0,x0 ]);
  
  JXG.Options.point.infoboxDigits = 1;
  JXG.Options.point.snapSizeX = 1;
  JXG.Options.point.snapSizeY = 0.1;
  
  // Attributes for points and lines
  function attrPfix(addAttr={}) {
    const attr = {fixed: true, visible: false, withLabel: false};
    return { ...attr, ...addAttr}; 
  }
  function attrPmov(addAttr={}) {
    const attr = {fixed: question.isSolved, snapToGrid: true, withLabel: false};
    return { ...attr, ...addAttr};
  }
  function attrPsma(addAttr={}) {
    const attr = {visible: true, withLabel: false, color:'#4285F4', size: 1};
    return { ...attr, ...addAttr};
  }
  const attrLine = {borders: {strokeColor:'#4285F4', strokeWidth: 3} };
  const attrGlid = {visible:false};


  // Create boards
  var brds = question.initBoards( [
  { // attribs for BOARDID0 
    boundingbox: [-1, 12, 13, -11], axis:true,
    defaultAxes: {
      x: {withLabel: true, name: '$$t\\;\\mathrm{(s)}$$',
          label: {position: 'rt', offset: [10, 26], anchorX: 'right', parse: false, fontSize: 12 } },
      y: {withLabel:true, name: '$$x\\;\\mathrm{(m)}$$',
          label: {position: 'rt', offset: [10, 15], parse: false, fontSize: 12 } } },
      zoom: {enabled:false, wheel: false}, pan: {enabled:false, needTwoFingers: false},
      showCopyright: false, showNavigation: false 
    },
    { // attribs for BOARDID1 
    boundingbox: [-1, 3.8, 13, -3.5], axis:true,
    defaultAxes: {
      x: {withLabel: true, name: '$$t\\;\\mathrm{(s)}$$',
          label: {position: 'rt', offset: [10, 26], anchorX: 'right', parse: false, fontSize: 12 } },
      y: {withLabel:true, name: '$$v_x\\;\\mathrm{(m/s)}$$',
          label: {position: 'rt', offset: [10, 15], parse: false, fontSize: 12 } } },
      zoom: {enabled:false, wheel: false}, pan: {enabled:false, needTwoFingers: false},
      showCopyright: false, showNavigation: false
    }
  ] );

  var brd0 = brds[0];
  var brd1 = brds[1];
  // console.log(brd0, brd1);

  // Board brd0 needs to be updated when changes in brd1 occur
  // question.addChildsAsc();
  

  // Define lines and points on brd1
  var pV1 = brd1.create('point',  [t1, v1], attrPmov({name: "pV1"}) ),
      pV2 = brd1.create('point',  [t2, v2], attrPmov({name: "pV2"}) ),
      pV3 = brd1.create('point',  [t3, v3], attrPmov({name: "pV3"}) ),
      pV01 = brd1.create('point', [0, "Y(pV1)"], attrPsma() ),
      pV12 = brd1.create('point', ["X(pV1)", "Y(pV2)"], attrPsma() ),
      pV23 = brd1.create('point', ["X(pV2)", "Y(pV3)"], attrPsma() )    ;
      pV34 = brd1.create('point', ["X(pV3)", 0], attrPsma() )    ;
  brd1.create('polygonalchain', [ pV01, pV1, pV12, pV2, pV23, pV3, pV34 ], attrLine);
  

  // Define lines and points on brd0
  var pX0 = brd0.create('point', [0, x0], attrPsma({fixed: true}) ),
      pX1 = brd0.create('point', [t1, x1], attrPmov({face: 'diamond'}) ),
      pX2 = brd0.create('point', [t2, x2], attrPmov({face: 'diamond'}) ),
      pX3 = brd0.create('point', [t3, x3], attrPmov({face: 'diamond'}) );
  brd0.create('polygonalchain', [ pX0, pX1, pX2, pX3 ], attrLine);
  

  // Define dependencies
  pV1.on('drag', function() { pX1.moveTo([this.X(), pX1.Y()], 0); });
  pV2.on('drag', function() { pX2.moveTo([this.X(), pX2.Y()], 0); });
  pV3.on('drag', function() { pX3.moveTo([this.X(), pX3.Y()], 0); });
  pX1.on('drag', function() { pV1.moveTo([this.X(), pV1.Y()], 0); });
  pX2.on('drag', function() { pV2.moveTo([this.X(), pV2.Y()], 0); });
  pX3.on('drag', function() { pV3.moveTo([this.X(), pV3.Y()], 0); });
  

  // Whenever the construction is altered the values of the points are sent to formulas.
  question.bindInput(0, () => { return pV1.X(); });
  question.bindInput(1, () => { return pV2.X(); });
  question.bindInput(2, () => { return pV3.X(); });
  question.bindInput(3, () => { return pV1.Y(); });
  question.bindInput(4, () => { return pV2.Y(); });
  question.bindInput(5, () => { return pV3.Y(); });
  question.bindInput(6, () => { return pX1.Y(); });
  question.bindInput(7, () => { return pX2.Y(); });
  question.bindInput(8, () => { return pX3.Y(); });
  };
  
  // Execute the JavaScript code.
  new JSXQuestion(BOARDIDS, jsxCode, allowInputEntry=false); // use BOARDIDS here!!
  
</jsxgraph>