5.3 Developing supporting view component

For creating a text panel of the form of a equation, I have implemented a class called EquationNode in the EquationNode.js file. This file is heavily inspired by the EquationNode.js file for the Least-Squares Regression simulation by PhET Interactive Simulations. The simulation can be found here https://phet.colorado.edu/en/simulations/least-squares-regression

5.3.1 Constructing the equation

For the purpose of this simulation, we need the form of x = c where c is the value referring to the current slider value. We make this equation in parts like so:

    // 'y'
    this.yText = new RichText( symbolYString, stringTextOptions ); 
    // the '=' sign
    this.equalText = new Text( MathSymbols.EQUAL_TO, stringTextOptions ); 
    // a number
    this.valueText = new Text( maxWidthString, numericalTextOptions );

5.3.2 Adding the equation child

Now in order to add this equation to our view, we do the following steps:

    const mutableEquationText = new Node( {
      children: [
        this.yText,
        this.equalText,
        this.valueText
      ]
    } );

    // layout of the entire equation
    this.yText.left = 0;
    this.equalText.left = this.yText.right + 3;
    this.valueText.left = this.equalText.right + 6;

    this.addChild( mutableEquationText );

5.3.3 Functions

In order to update the value of this equation with the current slider value, we are using the following functions:

  /**
   * Set the text of the data.
   * @public
   * @param {number} data
   */
  setText( data ) {
    this.valueText.text = Utils.toFixed(this.numberToString( data ).absoluteNumber, 2);
  }

  /**
   * Convert a number to String, rounding to a certain number of decimal places
   * @private
   * @param {number} number
   * @returns {{absoluteNumber: number, optionalSign: string, sign: string}}
   */
  numberToString( number ) {
    const isNegative = ( this.roundNumber( number ) < 0 );
    const signString = isNegative ? MathSymbols.MINUS : MathSymbols.PLUS;
    const optionalSignString = isNegative ? MathSymbols.MINUS : ' ';
    const absoluteNumber = this.roundNumber( Math.abs( this.roundNumber( number ) ) );
    const numberString = {
      absoluteNumber: absoluteNumber,
      optionalSign: optionalSignString,
      sign: signString
    };
    return numberString;
  }

  /**
   * Round a number to a certain number of decimal places.
   * @private
   * @param {number} number

   * @returns {number}
   */
  roundNumber( number ) {
    let roundedNumber;
    if ( Math.abs( number ) < 10 ) {
      // eg. 9.99, 0.01 if this.options.maxDecimalPlaces=2
      roundedNumber = Utils.toFixed( number, this.options.maxDecimalPlaces ); 
    }
    else if ( Math.abs( number ) < 100 ) {
      // eg. 10.1, 99.9
      roundedNumber = Utils.toFixed( number, this.options.maxDecimalPlaces - 1 ); 
    }
    else {
      // 100, 1000, 10000, 99999
      roundedNumber = Utils.toFixed( number, this.options.maxDecimalPlaces - 2 );
    }
    return roundedNumber;
  }
}

Here’s a quick rundown of what each function does:

setText()

  • Arguments: It takes the new data as an argument.
  • It updates the value of our equation to the new data.

numberToString()

  • Arguments: It takes a number as an argument.
  • Convert a number to a String, subject to rounding to a certain number of decimal places.

roundNumber()

  • Arguemnts - It takes a number as an argument.
  • Round a number to a certain number of decimal places. Higher numbers have less decimal places.

5.3.4 Code

Here’s the complete code of this file for better comprehension:

/**
 * Equation Node that renders a text node of a linear equation.
 *
 * @author Mayank Pandey
 */

import Utils from '../../../../dot/js/Utils.js';
import merge from '../../../../phet-core/js/merge.js';
import MathSymbols from '../../../../scenery-phet/js/MathSymbols.js';
import Node from '../../../../scenery/js/nodes/Node.js';
import Text from '../../../../scenery/js/nodes/Text.js';
import RichText from '../../../../scenery/js/nodes/RichText.js';
import newtonRaphson from '../../newtonRaphson.js';
import NewtonRaphsonConstants from '../NewtonRaphsonConstants.js';

class EquationNode extends Node {
  /**
   * Scenery Node responsible for laying out the linear equation.
   * @param {Object} [options]
   */
  constructor( options, symbolYString ) {
    super();

    options = merge( {
      maxDecimalPlaces: 4,  // maximum of number of decimal places
      maxCharacterWidth: 600
    }, options );

    this.symbolYString = symbolYString;
    this.options = options;

    // options for the text elements of the equation
  
    // font and fill options for numerical strings , i.e.  '- 9.54'
    let numericalTextOptions; 
    // font and fill options for 'pure' strings, eg. 'y'
    let stringTextOptions; 

    numericalTextOptions = {
      font: NewtonRaphsonConstants.TEXT_BOLD_FONT_EQUATION,
      maxWidth: options.maxCharacterWidth
    };

    stringTextOptions = {
      font: NewtonRaphsonConstants.TEXT_FONT,
      fill: 'black',
      maxWidth: options.maxCharacterWidth
    };

    // use the widest possible numbers for laying out the equation

    let maxWidthString = '0.';
    for ( let j = 0; j < options.maxDecimalPlaces; j++ ) {
      maxWidthString = maxWidthString + '0';
    }

    // 'y'
    this.yText = new RichText( symbolYString, stringTextOptions ); 
    // the '=' sign
    this.equalText = new Text( MathSymbols.EQUAL_TO, stringTextOptions ); 
    // a number
    this.valueText = new Text( maxWidthString, numericalTextOptions );

    const mutableEquationText = new Node( {
      children: [
        this.yText,
        this.equalText,
        this.valueText
      ]
    } );

    // layout of the entire equation
    this.yText.left = 0;
    this.equalText.left = this.yText.right + 3;
    this.valueText.left = this.equalText.right + 6;

    this.addChild( mutableEquationText );

    this.mutate( options );

  }

  /**
   * Set the text of the data.
   * @public
   * @param {number} data
   */
  setText( data ) {
    this.valueText.text = Utils.toFixed(this.numberToString( data ).absoluteNumber, 2);
  }

  /**
   * Convert a number to String, rounding to a certain number of decimal places
   * @private
   * @param {number} number
   * @returns {{absoluteNumber: number, optionalSign: string, sign: string}}
   */
  numberToString( number ) {
    const isNegative = ( this.roundNumber( number ) < 0 );
    const signString = isNegative ? MathSymbols.MINUS : MathSymbols.PLUS;
    const optionalSignString = isNegative ? MathSymbols.MINUS : ' ';
    const absoluteNumber = this.roundNumber( Math.abs( this.roundNumber( number ) ) );
    const numberString = {
      absoluteNumber: absoluteNumber,
      optionalSign: optionalSignString,
      sign: signString
    };
    return numberString;
  }

  /**
   * Round a number to a certain number of decimal places.
   * @private
   * @param {number} number

   * @returns {number}
   */
  roundNumber( number ) {
    let roundedNumber;
    if ( Math.abs( number ) < 10 ) {
      // eg. 9.99, 0.01 if this.options.maxDecimalPlaces=2
      roundedNumber = Utils.toFixed( number, this.options.maxDecimalPlaces ); 
    }
    else if ( Math.abs( number ) < 100 ) {
      // eg. 10.1, 99.9
      roundedNumber = Utils.toFixed( number, this.options.maxDecimalPlaces - 1 ); 
    }
    else {
      // 100, 1000, 10000, 99999
      roundedNumber = Utils.toFixed( number, this.options.maxDecimalPlaces - 2 );
    }
    return roundedNumber;
  }
}

newtonRaphson.register( 'EquationNode', EquationNode );

export default EquationNode;