5.4 Developing the Inputs Panel

The first step, as usual, is to create a .js file for the inputs panel. In my case, I named it as MyLineControlPanel.js. Once you’ve done that the next step is to import a bunch of files that you are going to potentially need for this part of the simulation.

5.4.1 Imports

Here are the imports I used for this simulation:

import Utils from '../../../../dot/js/Utils.js';
import Dimension2 from '../../../../dot/js/Dimension2.js';
import Range from '../../../../dot/js/Range.js';
import HStrut from '../../../../scenery/js/nodes/HStrut.js';
import HBox from '../../../../scenery/js/nodes/HBox.js';
import RichText from '../../../../scenery/js/nodes/RichText.js';
import PhetFont from '../../../../scenery-phet/js/PhetFont.js';
import LayoutBox from '../../../../scenery/js/nodes/LayoutBox.js';
import Node from '../../../../scenery/js/nodes/Node.js';
import Text from '../../../../scenery/js/nodes/Text.js';
import Checkbox from '../../../../sun/js/Checkbox.js';
import Panel from '../../../../sun/js/Panel.js';
import HSlider from '../../../../sun/js/HSlider.js';
import Font from '../../../../scenery/js/util/Font.js';
import newtonRaphson from '../../newtonRaphson.js';
import newtonRaphsonStrings from '../../newtonRaphsonStrings.js';
import HorizontalAquaRadioButtonGroup 
from '../../../../sun/js/HorizontalAquaRadioButtonGroup.js';
import VerticalAquaRadioButtonGroup 
from '../../../../sun/js/VerticalAquaRadioButtonGroup.js';
import NewtonRaphsonConstants from '../NewtonRaphsonConstants.js';
import EquationNode from './EquationNode.js';

Now I know all of these files seems a like of a whole lot but it’s not too complicated. Here’s a basic overview of the components being used:

Component Description

  • Utils - This library is used for numerical operations. For example: fixing the number of significant digits.
  • Dimension2 - Basic width and height, like a Bounds2 but without the location defined. We are using it for the slider track size and thumb size.
  • Range - It is basically a numeric range but with additional methods that help other classes that need to use this component.
  • HStrut - It is a Node meant to just take up horizontal space (usually for layout purposes).
  • HBox - This is a type of a LayoutBox used as a placeholder for all the other nodes. It’s orientation is horizontal.
  • RichText - Displays rich text by interpreting the input text as HTML, supporting a limited set of tags that prevent any security vulnerabilities. It does this by parsing the input HTML and splitting it into multiple Text children recursively.
  • PhetFont - It has the standard fonts used by PhET simulations.
  • LayoutBox - LayoutBox lays out its children in a row, either horizontally or vertically (based on an optional parameter).
  • Node - A Node for the Scenery scene graph. Supports general directed acyclic graphics (DAGs). Handles multiple layers with assorted types (Canvas 2D, SVG, DOM, WebGL, etc.).
  • Text - Displays text that can be filled/stroked.
  • Checkbox - Checkbox is a typical checkbox UI component.
  • Panel - This holds the content node. Dynamically adjusts its size to fit its contents.
  • HSlider - This is the class for a horizontal slider, in this case we are using this but you can also use a VSlider which is a vertical slider.
  • Font - This is used to define font size, type, and bold characteristics etc.
  • newtonRaphson - Creates the namespace for this simulation.
  • newtonRaphsonStrings - This is used to include any strings you have in your newton-raphson-strings_en.json file.
  • HorizontalAquaRadioButtonGroup - This is used for making radio button groups. The orientation is going to be horizontal.
  • VerticalAquaRadioButtonGroup - This is used for making radio button groups. The orientation is going to be vertical.
  • NewtonRaphsonConstants - Constants that are used in the Newton Raphson Simulation.
  • EquationNode - It renders a text node of a linear equation of the form y = m x + b where m and b are numerical values.

With all that overwhelming information out of the way, let’s start coding for our inputs panel.

5.4.2 Constants

We’ll start of by assigning some constants before we declare our MyLineControlPanel class.

// constants
const SLIDER_OPTIONS = {
  trackFill: 'black',
  trackSize: new Dimension2( 120, 4 ),
  thumbSize: new Dimension2( 30, 15 ),
  thumbTouchAreaXDilation: 8,
  majorTickLength: 0,
  // changing the interval of slider change.
  constrainValue: value => Utils.roundToInterval( value, 0.01 ) 
};

// decides the width of the equations and checkbox text.
const MAX_WIDTH = 200;

// used for the radio button group.
const BUTTON_FONT_SIZE = 16;
const BUTTON_FONT = new Font( { size: BUTTON_FONT_SIZE } );

The slider options are used later when we will create a new slider for our graph.xProperty that we assigned to be a new NumberProperty(0.01) in Graph.js. More about that can be found at Graph.js. Most of the properties inside SLIDER_OPTIONS are self-explanatory but I would like to point your attention towards the constrainValue property which decides the interval of the change in the slider value when it is moved by the user.

As I mentioned before, after all my explanation, there is a section for the actual code for the file. So, to be considerate of space and time, I will be talking about how to use different components. However, I will be doing it once for each component because in every other occurence of the component, the functionality and implementation remains unchanged for the most part.

5.4.3 EquationNode

Let’s start with creating a new EquationNode for our \(x_0\).

const equationCharacterMaxWidth = MAX_WIDTH / 6;
const xOText = new EquationNode( { maxCharacterWidth: equationCharacterMaxWidth },
'x<sub>0<sub>' );

The equationCharacterMaxWidth is an arbitrary value, I found that this works for me by trial and error.

5.4.4 Panel

Next, here’s how you would go about making the panel (with white background) for the xOText:

// create the equation panel with white background for x value
    const equationPanel = new Panel( xOText, {
      fill: 'white',
      cornerRadius: NewtonRaphsonConstants.SMALL_PANEL_CORNER_RADIUS,
      stroke: NewtonRaphsonConstants.SMALL_PANEL_STROKE,
      resize: false
    } );

For a quick review on NewtonRaphsonConstants checkout Defining your own Constants. The fill decides the color of the background and resize decides if based on input the size should increase dynamically or not.

5.4.5 Slider

Next up, here’s how one would go about creating a slider:

    // the xSlider controls the x value
    const xRange = new Range( 0.01, 6.11);

    const xSlider = new HSlider( graph.xProperty, xRange, SLIDER_OPTIONS );

    const checkboxTextOptions = { font: NewtonRaphsonConstants.CHECKBOX_TEXT_FONT,
    maxWidth: MAX_WIDTH };

Notice how we are giving new HSlider the graph.xProperty. This is crucial because later we can use a .link() for this property which will also us to capture the change in the slider values in real time.

5.4.6 Radio button group

Next up, let’s see how we can create a radio button group:

// creating radio button group for functions.
    const functionValues = [ 'x<sup>2</sup> - 2 = 0', 'cos(x) = 0',
    'x<sup>3</sup> - 7 = 0', 'x<sup>3</sup> - 3x<sup>2</sup> + x - 1 = 0'];
    const functionRadioButtonGroupContent = _.map( functionValues, stringValue => {
      return {
        value: stringValue,
        node: new RichText( stringValue, { font: BUTTON_FONT } ),
        labelContent: stringValue
      };
    } );
    const functionRadioButtonGroup =  new VerticalAquaRadioButtonGroup( 
    graph.functionValuesProperty, functionRadioButtonGroupContent, {
      spacing: 2,
      enabledProperty: graph.functionEnabledProperty
    } );
    const functionRadioButtonGroupPanel = new Panel( functionRadioButtonGroup, {
      stroke: 'black',
      xMargin: 10,
      yMargin: 10
    } );

All our options go inside the functionValues array and in the functionRadioButtonGroup we feed it the graph.functionValuesProperty so that we can capture any changes like when a different radial button is selected. In the case for iterations radio button group, I used HorizontalAquaRadioButtonGroup instead ofVerticalAquaRadioButtonGroup. Also I changed the spacing but that was based on preference.

5.4.7 Assembly

Finally, after you have made all of the components for your inputs panel, now you can start placing them in a layout box with your desired orientation like so:

// assemble all the previous nodes in a vertical box
    const tempBox = new HBox( {
      spacing: 10,
      children: [
        xSlider,
        equationPanel,
      ]
    })
    const mainBox = new LayoutBox( {
      spacing: 10,
      children: [
        functionTextPanel,
        functionRadioButtonGroupPanel,
        iterationTextPanel,
        iterationGroupPanel,
        sliderTextPanel,
        tempBox
      ],
      align: 'left',
      excludeInvisibleChildrenFromBounds: false
    } );

5.4.8 Layout

I placed the slider and the equation for the slider in a new HBox because I wanted those two to have the same center.y. Later I even manually changed the layout of the slider and the equation based on how I wanted it to be. Later, I called the super() to call the constructor of the parent class.

    // layout the internal nodes of the right Aligned Node
    xSlider.top =  61;
    equationPanel.left = xSlider.right;
    
    super( mainBox, options );

5.4.10 Code

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

/**
 * My Line Control Panel component and all of its methods and data members.
 *
 * @author Mayank Pandey
 */

import Utils from '../../../../dot/js/Utils.js';
import Dimension2 from '../../../../dot/js/Dimension2.js';
import Range from '../../../../dot/js/Range.js';
import HStrut from '../../../../scenery/js/nodes/HStrut.js';
import HBox from '../../../../scenery/js/nodes/HBox.js';
import RichText from '../../../../scenery/js/nodes/RichText.js';
import PhetFont from '../../../../scenery-phet/js/PhetFont.js';
import LayoutBox from '../../../../scenery/js/nodes/LayoutBox.js';
import Node from '../../../../scenery/js/nodes/Node.js';
import Text from '../../../../scenery/js/nodes/Text.js';
import Checkbox from '../../../../sun/js/Checkbox.js';
import Panel from '../../../../sun/js/Panel.js';
import HSlider from '../../../../sun/js/HSlider.js';
import Font from '../../../../scenery/js/util/Font.js';
import newtonRaphson from '../../newtonRaphson.js';
import newtonRaphsonStrings from '../../newtonRaphsonStrings.js';
import HorizontalAquaRadioButtonGroup 
from '../../../../sun/js/HorizontalAquaRadioButtonGroup.js';
import VerticalAquaRadioButtonGroup 
from '../../../../sun/js/VerticalAquaRadioButtonGroup.js';
import NewtonRaphsonConstants from '../NewtonRaphsonConstants.js';
import EquationNode from './EquationNode.js';

// constants
const SLIDER_OPTIONS = {
  trackFill: 'black',
  trackSize: new Dimension2( 120, 4 ),
  thumbSize: new Dimension2( 30, 15 ),
  thumbTouchAreaXDilation: 8,
  majorTickLength: 0,
  // changing the interval of slider change.
  constrainValue: value => Utils.roundToInterval( value, 0.01 ) 
};

// decides the width of the equations and checkbox text.
const MAX_WIDTH = 200;

// used for the radio button group.
const BUTTON_FONT_SIZE = 16;
const BUTTON_FONT = new Font( { size: BUTTON_FONT_SIZE } );

class MyLineControlPanel extends Panel {

  constructor( graph, options ) {

    const textOptions = {
      font: NewtonRaphsonConstants.TEXT_BOLD_FONT,
      maxDecimalPlaces: 10
     };

    const equationCharacterMaxWidth = MAX_WIDTH / 6;
    const xOText = new EquationNode( { maxCharacterWidth: equationCharacterMaxWidth },
    'x<sub>0<sub>' );

    // updates the value of the equation
    function updateTextXValue( xValue ) {
      xOText.setText( xValue );
    }

    // create the equation panel with white background for x value
    const equationPanel = new Panel( xOText, {
      fill: 'white',
      cornerRadius: NewtonRaphsonConstants.SMALL_PANEL_CORNER_RADIUS,
      stroke: NewtonRaphsonConstants.SMALL_PANEL_STROKE,
      resize: false
    } );

    // the xSlider controls the x value
    const xRange = new Range( 0.01, 6.11);

    const xSlider = new HSlider( graph.xProperty, xRange, SLIDER_OPTIONS );

    const checkboxTextOptions = { font: NewtonRaphsonConstants.CHECKBOX_TEXT_FONT,
    maxWidth: MAX_WIDTH };

    // creating radio button group for iterations.
    const iterationValues = [ '0', '1', '2', '3', '4', '5' ];
    const iterationGroupContent = _.map( iterationValues, stringValue => {
      return {
        value: stringValue,
        node: new Text( stringValue, { font: BUTTON_FONT } ),
        labelContent: stringValue
      };
    } );
    const iterationGroup = new HorizontalAquaRadioButtonGroup( 
    graph.iterationValuesProperty,
    iterationGroupContent, {
      spacing: 8,
      enabledProperty: graph.buttonsEnabledProperty
    } );
    const iterationGroupPanel = new Panel( iterationGroup, {
      stroke: 'black',
      xMargin: 10,
      yMargin: 10
    } );

    // creating radio button group for functions.
    const functionValues = [ 'x<sup>2</sup> - 2 = 0', 'cos(x) = 0',
    'x<sup>3</sup> - 7 = 0', 'x<sup>3</sup> - 3x<sup>2</sup> + x - 1 = 0'];
    const functionRadioButtonGroupContent = _.map( functionValues, stringValue => {
      return {
        value: stringValue,
        node: new RichText( stringValue, { font: BUTTON_FONT } ),
        labelContent: stringValue
      };
    } );
    const functionRadioButtonGroup =  new VerticalAquaRadioButtonGroup( 
    graph.functionValuesProperty, functionRadioButtonGroupContent, {
      spacing: 2,
      enabledProperty: graph.functionEnabledProperty
    } );
    const functionRadioButtonGroupPanel = new Panel( functionRadioButtonGroup, {
      stroke: 'black',
      xMargin: 10,
      yMargin: 10
    } );

    // creating text panels.
    const iterationTextPanel = new Panel( new Text('Number of Iterations', textOptions), {
       fill: 'white',
       cornerRadius: NewtonRaphsonConstants.SMALL_PANEL_CORNER_RADIUS,
       stroke: NewtonRaphsonConstants.SMALL_PANEL_STROKE,
       resize: false
     } );
    const functionTextPanel = new Panel( new Text('Equation', textOptions), {
       fill: 'white',
       cornerRadius: NewtonRaphsonConstants.SMALL_PANEL_CORNER_RADIUS,
       stroke: NewtonRaphsonConstants.SMALL_PANEL_STROKE,
       resize: false
     } );
    const sliderTextPanel = new Panel( new Text('Initial Estimate', textOptions), {
       fill: 'white',
       cornerRadius: NewtonRaphsonConstants.SMALL_PANEL_CORNER_RADIUS,
       stroke: NewtonRaphsonConstants.SMALL_PANEL_STROKE,
       resize: false
     } );

    // assemble all the previous nodes in a vertical box
    const tempBox = new HBox( {
      spacing: 10,
      children: [
        xSlider,
        equationPanel,
      ]
    })
    const mainBox = new LayoutBox( {
      spacing: 10,
      children: [
        functionTextPanel,
        functionRadioButtonGroupPanel,
        iterationTextPanel,
        iterationGroupPanel,
        sliderTextPanel,
        tempBox
      ],
      align: 'left',
      excludeInvisibleChildrenFromBounds: false
    } );

    // layout the internal nodes of the right Aligned Node
    xSlider.top =  61;
    equationPanel.left = xSlider.right;

    super( mainBox, options );

    // updating the x value based on xSlider input.
    graph.xProperty.link( xValue => {
      updateTextXValue( xValue );
    } );
  }
}

newtonRaphson.register( 'MyLineControlPanel', MyLineControlPanel );
export default MyLineControlPanel;