Search code examples
moodlejsxgraph

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


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?

Solution

  • 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