5.5 Developing the Graph Panel

The next step in our development process is to work on the graph. Just like with the inputs panel, we make a .js file for the graph and in this case, I named it as GraphLinePlot. The next step is to import a bunch of classes that you are going to use for this part.

5.5.1 Imports

import Range from '../../../../dot/js/Range.js';
import Vector2 from '../../../../dot/js/Vector2.js';
import Orientation from '../../../../phet-core/js/Orientation.js';
import PhetFont from '../../../../scenery-phet/js/PhetFont.js';
import Node from '../../../../scenery/js/nodes/Node.js';
import Text from '../../../../scenery/js/nodes/Text.js';
import AxisNode from '../../../../bamboo/js/AxisNode.js';
import ChartTransform from '../../../../bamboo/js/ChartTransform.js';
import ChartRectangle from '../../../../bamboo/js/ChartRectangle.js';
import LabelSet from '../../../../bamboo/js/LabelSet.js';
import LinePlot from '../../../../bamboo/js/LinePlot.js';
import ScatterPlot from '../../../../bamboo/js/ScatterPlot.js';
import TickMarkSet from '../../../../bamboo/js/TickMarkSet.js';
import newtonRaphson from '../../newtonRaphson.js';

Here’s a basic overview of the components used:

Component Description

  • Vector2 - Basic 2-dimensional vector, represented as (x,y).
  • Orientation - Either horizontal or vertical, with helper values.
  • AxisNode - Shows a line that depicts an axis.
  • ChartTransform - ChartTransform defines the chart dimensions in model and view coordinate frames, and provides transform methods for moving between those coordinate frames.
  • ChartRectangle - Shows the background and border for a chart, and updates when the chart dimensions change.
  • LabelSet - Shows a set of labels within or next to a chart.
  • LinePlot - Renders a line by connecting the data points of a data set with line segments.
  • ScatterPlot - Renders a scatter plot using points of some radius for each point in the data set.
  • TickMarkSet - Shows tick marks within or next to a chart.

5.5.2 Functions

Let me re-introduce to you the functions that we are using for this simulation:

Function choices

  • \(x^2 - 2 = 0\)
  • \(cos(x) = 0\)
  • \(x^3 - 7 = 0\)
  • \(x^3 - 3x^2 + x - 1 = 0\)

In order to display the function for each choice on our graph, we need to use the LinePlot library from bamboo. The LinePlot object for our function will need a data set to plot the function. However, this data set can’t just be an 2-D array of (x, y) data points. By virtue of design, we need to use the Vector2 class to create an object for each data point and all these data points are added to a dataSet array. This is the array which is passed to the LinePlot object to display our function. To create these data sets, here are the functions that I have developed:

// creating four functions for each respective dataSet.

    // x^2 - 2 = 0
    function createDataSetFirstFunction( min, max, delta = 0.005 ) {
      const dataSet = [];
      for ( let x = min; x <= max; x += delta ) {
        dataSet.push( new Vector2( x, (Math.pow(x,2) - 2) ) );
      }
      return dataSet;
    };

    // cos(x) = 0
    function createDataSetSecondFunction( min, max, delta = 0.005 ) {
      const dataSet = [];
      for ( let x = min; x <= max; x += delta ) {
        dataSet.push( new Vector2( x, Math.cos(x) ) );
      }
      return dataSet;
    };

    // x^3 - 7 = 0
    function createDataSetThirdFunction( min, max, delta = 0.005 ) {
      const dataSet = [];
      for ( let x = min; x <= max; x += delta ) {
        dataSet.push( new Vector2( x, (Math.pow(x,3) -7) ) );
      }
      return dataSet;
    };

    // x^3 - 3x^2 + x - 1 = 0
    function createDataSetFourthFunction( min, max, delta = 0.005 ) {
      const dataSet = [];
      for ( let x = min; x <= max; x += delta ) {
        dataSet.push( new Vector2( x, (Math.pow(x,3) - 3*Math.pow(x,2) + x - 1) ) );
      }
      return dataSet;
    };

5.5.3 Graph Constraints

Now we need to define the size and area of the graph which is done like so:

    // defines chart constraints.
    const chartTransform = new ChartTransform( {
      viewWidth: 400,
      viewHeight: 300,
      modelXRange: new Range( -0.2, 7),
      modelYRange: new Range(-2, 40)
    } );

    // chart area.
    const chartRectangle = new ChartRectangle( chartTransform, {
      fill: 'white',
      stroke: 'black',
      cornerXRadius: 6,
      cornerYRadius: 6
    } );

5.5.4 Axis and Ticks

Now we want to make the blank canvas look more like a graph so we can do that by giving it the x and y axis. Additionally, we can give it tick marks and labels for each axis by doing this:

    // Creating ticks and labels. Creating these outside and assigning them.
    let tick_y = new TickMarkSet( chartTransform, Orientation.VERTICAL, 10,
    { edge: 'min' } );
    let label_y = new LabelSet( chartTransform, Orientation.VERTICAL, 10,
    { edge: 'min' } );

    // Anything you want clipped goes in here
    this.children = [

      // Background
      chartRectangle,

      // Clipped contents
      new Node( {
        clipArea: chartRectangle.getShape(),
        children: [

          // Axes nodes are clipped in the chart
          new AxisNode( chartTransform, Orientation.HORIZONTAL ),
          new AxisNode( chartTransform, Orientation.VERTICAL )
        ]
      } ),

      // Tick marks outside the chart
      tick_y,
      label_y,
      // tick and label for the X axis.
      new TickMarkSet( chartTransform, Orientation.HORIZONTAL, 1, { edge: 'min' } ),
      new LabelSet( chartTransform, Orientation.HORIZONTAL, 1, {
        edge: 'min',
        createLabel: value => new Text( Math.abs( value ) < 1E-6 
        ? value.toFixed( 0 ) 
        : value.toFixed( 2 ), {
          fontSize: 12
        } )
      } )
    ];

Now with all that out of the way, we can begin adding LinePlot objects and ScatterPlot objects to our graph in order to bring in functionality

5.5.5 LinePlot

Here’s how one would go about creating LinePlot objects for the chosen function choice (black line) and tracing the estimates (red line).

    // creating two LinePlots. 
    // First one for the function choice and the second for tracing the estimates.
    let lineplotFunction = new LinePlot( chartTransform, [], { stroke: 'black',
    lineWidth: 2 } );
    let lineplotXline = new LinePlot( chartTransform, [], { stroke: 'red',
    lineWidth: 2 } );

There are certain arguments needed to be provided to the LinePlot constructor. The general format looks like: new LinePlot(chartTransform, dataSet, options)

Arguments

  • chartTransform - This is the ChartTransform object we created earlier to define the constraints of the graph in terms of size.
  • dataSet - This is the dataSet (an array) filled with Vector2 data points.
  • options - These options pertain to the physical characteristics of our line. In this case, I have defined color and width of the lines.

In order to update the LinePlot object, we can make use of the setDataSet() method.

    // you will see this later in the guide.
    lineplotFunction.setDataSet(graph.currFuncDataSet(0.01, 7));

5.5.6 ScatterPlot

So for the purpose of this simulation, I needed a point for \(x_0\) which is controlled by the user with the input slider and 5 points to show the estimate per iteration. To accomplish that, this is what I did:

    // initial value of the black point.
    let dataSet_black = [new Vector2(graph.xProperty.value, 0)];

    // creating 6 points. 1st point is black and controlled by slider input.
    // Rest of the points adhere to function outputs.
    let point0 = new ScatterPlot( chartTransform, dataSet_black, {
      fill: 'black',
      radius: 5
    } );
    let point1 = new ScatterPlot( chartTransform, [], {
      fill: 'red',
      radius: 3
    } );
    let point2 = new ScatterPlot( chartTransform, [], {
      fill: 'red',
      radius: 3
    } );
    let point3 = new ScatterPlot( chartTransform, [], {
      fill: 'red',
      radius: 3
    } );
    let point4 = new ScatterPlot( chartTransform, [], {
      fill: 'red',
      radius: 3
    } );
    let point5 = new ScatterPlot( chartTransform, [], {
      fill: 'red',
      radius: 5
    } );

The general syntax for creating a ScatterPlot object is: new ScatterPlot(chartTransform, dataSet, options)

The only difference from LinePlot is that the options take a fill property for color and they also take a radius property for the size of the point.

Since there are multiple points, therefore for easier access and use, I have stored them in an array as such:

    let points = [];

    points.push(point0, point1, point2, point3, point4, point5);

5.5.8 Updating the graph

In order to organize the structure of updating the graph, I have segrgated the process into different functions:


    function chooseFunc(value) {
      if (value === 'x<sup>2</sup> - 2 = 0') {
        graph.currFunc =  getXFirstFunction;
        graph.currFuncY = getYFirstFunction;
        graph.currFuncDataSet = createDataSetFirstFunction;
      } else if (value === 'cos(x) = 0') {
        graph.currFunc =  getXSecondFunction;
        graph.currFuncY = getYSecondFunction;
        graph.currFuncDataSet = createDataSetSecondFunction;
      } else if (value === 'x<sup>3</sup> - 7 = 0') {
        graph.currFunc =  getXThirdFunction;
        graph.currFuncY = getYThirdFunction;
        graph.currFuncDataSet = createDataSetThirdFunction;
      } else {
        graph.currFunc =  getXFourthFunction;
        graph.currFuncY = getYFourthFunction;
        graph.currFuncDataSet = createDataSetFourthFunction;
      }
    }

    function chooseRangeSpace(value) {
      if (value === 'x<sup>2</sup> - 2 = 0') {
        return {y1: -2, y2: 40, spacing: 10};
      } else if (value === 'cos(x) = 0') {
        return {y1: -1.3, y2: 1.3, spacing: 0.5};
      } else if (value === 'x<sup>3</sup> - 7 = 0') {
        return {y1: -15, y2:250, spacing: 50};
      } else {
        return {y1: -10, y2:140, spacing:20};
      }
    }
    
    function updatePoints(value_button) {
      points[0].setDataSet([new Vector2(graph.xProperty.value, 0)]);
      points.slice(1).map(point => point.setDataSet([]));
      let answerSet = [];
      let dataSet = [ new Vector2 (graph.xProperty.value, 0) ];
      let answer = graph.currFunc(graph.xProperty.value);
      if (value_button != 0) {
        dataSet.push(new Vector2 (graph.xProperty.value,
        graph.currFuncY(graph.xProperty.value)) );
        dataSet.push(new Vector2(answer.new_x, 0));
      }
      answerSet.push(answer);
      for (let i = 1; i < value_button; i++) {
        dataSet.push(new Vector2(answer.new_x, graph.currFuncY(answer.new_x) ) );
        answer = graph.currFunc(answer.new_x);
        dataSet.push(new Vector2(answer.new_x, 0));
        answerSet.push(answer);
      }
      lineplotXline.setDataSet(dataSet);
      for(let i = 1; i <= value_button; i++) {
        points[i].setDataSet([new Vector2(answerSet[i-1].new_x, 0)]);
      }
    }

    function updateGraph(y1, y2, spacing, value_button) {
      chooseFunc(graph.functionValuesProperty.value);
      lineplotFunction.setDataSet(graph.currFuncDataSet(0.01, 7));
      chartTransform.setModelYRange(new Range(y1, y2));
      tick_y.setSpacing(spacing);
      label_y.setSpacing(spacing);
      updatePoints(value_button);
    }

    function update() {
      ( {y1, y2, spacing} = chooseRangeSpace(graph.functionValuesProperty.value) );
      updateGraph(y1, y2, spacing, graph.iterationValuesProperty.value);
    }

The update() function that is called by the property.link() method of the graph.functionValuesProperty, graph.iterationValuesProperty, and graph.xProperty is the central point of initiation. It does two things:

update()

  • chooseRangeSpace(graph.functionValuesProperty.value) - It takes the current function choice as an argument and returns an object with hard-coded values for the range of the y axis and spacing required in terms of interval difference for each function.
  • updateGraph(y1, y2, spacing, graph.iterationValuesProperty.value) - It calls this function to continue the update process.

updateGraph(arguments)

  • Arguments: Takes the output from chooseRangeSpace() and the current number of iterations as arguments.
  • chooseFunc(graph.functionValuesProperty.value) - It calls this function to decide which function is supposed to be used for doing calculations.
  • Then based on function choice updates the data set of lineplotfunction, updates the y range, and updates the axis labels.
  • updatePoints(graph.iterationValuesProperty.value) - It calls this function to update the black and red points.

chooseFunc(arguments)

  • Arguments: Takes the function choice as an argument.
  • Based on function choice assigns the following:
    • graph.currFunc
    • graph.currFuncY
    • graph.currFuncDataSet

updatePoints(arguments)

  • Arguments: Takes the number of iterations as an argument.
  • Based on the number of iterations, it updates the dataSet of each data point.

5.5.9 Code

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

/**
 * Displays the line plots and scatter plots on the graph.
 *
 * @author Mayank Pandey
 */

import Range from '../../../../dot/js/Range.js';
import Vector2 from '../../../../dot/js/Vector2.js';
import Orientation from '../../../../phet-core/js/Orientation.js';
import PhetFont from '../../../../scenery-phet/js/PhetFont.js';
import Node from '../../../../scenery/js/nodes/Node.js';
import Text from '../../../../scenery/js/nodes/Text.js';
import AxisNode from '../../../../bamboo/js/AxisNode.js';
import ChartTransform from '../../../../bamboo/js/ChartTransform.js';
import ChartRectangle from '../../../../bamboo/js/ChartRectangle.js';
import LabelSet from '../../../../bamboo/js/LabelSet.js';
import LinePlot from '../../../../bamboo/js/LinePlot.js';
import ScatterPlot from '../../../../bamboo/js/ScatterPlot.js';
import TickMarkSet from '../../../../bamboo/js/TickMarkSet.js';
import newtonRaphson from '../../newtonRaphson.js';

class GraphLinePlot extends Node {

  constructor( graph, options ) {

    super();

    // creating four functions for each respective dataSet.
    function createDataSetFirstFunction( min, max, delta = 0.005 ) {
      const dataSet = [];
      for ( let x = min; x <= max; x += delta ) {
        dataSet.push( new Vector2( x, (Math.pow(x,2) - 2) ) );
      }
      return dataSet;
    };

    function createDataSetSecondFunction( min, max, delta = 0.005 ) {
      const dataSet = [];
      for ( let x = min; x <= max; x += delta ) {
        dataSet.push( new Vector2( x, Math.cos(x) ) );
      }
      return dataSet;
    };

    function createDataSetThirdFunction( min, max, delta = 0.005 ) {
      const dataSet = [];
      for ( let x = min; x <= max; x += delta ) {
        dataSet.push( new Vector2( x, (Math.pow(x,3) -7) ) );
      }
      return dataSet;
    };

    function createDataSetFourthFunction( min, max, delta = 0.005 ) {
      const dataSet = [];
      for ( let x = min; x <= max; x += delta ) {
        dataSet.push( new Vector2( x, (Math.pow(x,3) - 3*Math.pow(x,2) + x - 1) ) );
      }
      return dataSet;
    };

    // defines chart constraints.
    const chartTransform = new ChartTransform( {
      viewWidth: 400,
      viewHeight: 300,
      modelXRange: new Range( -0.2, 7),
      modelYRange: new Range(-2, 40)
    } );

    // chart area.
    const chartRectangle = new ChartRectangle( chartTransform, {
      fill: 'white',
      stroke: 'black',
      cornerXRadius: 6,
      cornerYRadius: 6
    } );

    // creating two LinePlot's.
    // First one for the function choice and the second for tracing the estimates.
    let lineplotFunction = new LinePlot( chartTransform, [], { stroke: 'black',
    lineWidth: 2 } );
    let lineplotXline = new LinePlot( chartTransform, [], { stroke: 'red',
    lineWidth: 2 } );

    // initial value of the black point.
    let dataSet_black = [new Vector2(graph.xProperty.value, 0)];

    // creating 6 points. 1st point is black and controlled by slider input.
    // Rest of the points adhere to function outputs.
    let point0 = new ScatterPlot( chartTransform, dataSet_black, {
      fill: 'black',
      radius: 5
    } );
    let point1 = new ScatterPlot( chartTransform, [], {
      fill: 'red',
      radius: 3
    } );
    let point2 = new ScatterPlot( chartTransform, [], {
      fill: 'red',
      radius: 3
    } );
    let point3 = new ScatterPlot( chartTransform, [], {
      fill: 'red',
      radius: 3
    } );
    let point4 = new ScatterPlot( chartTransform, [], {
      fill: 'red',
      radius: 3
    } );
    let point5 = new ScatterPlot( chartTransform, [], {
      fill: 'red',
      radius: 5
    } );

    let points = [];

    points.push(point0, point1, point2, point3, point4, point5);

    let y1, y2, spacing;

    function chooseFunc(value) {
      if (value === 'x<sup>2</sup> - 2 = 0') {
        graph.currFunc =  getXFirstFunction;
        graph.currFuncY = getYFirstFunction;
        graph.currFuncDataSet = createDataSetFirstFunction;
      } else if (value === 'cos(x) = 0') {
        graph.currFunc =  getXSecondFunction;
        graph.currFuncY = getYSecondFunction;
        graph.currFuncDataSet = createDataSetSecondFunction;
      } else if (value === 'x<sup>3</sup> - 7 = 0') {
        graph.currFunc =  getXThirdFunction;
        graph.currFuncY = getYThirdFunction;
        graph.currFuncDataSet = createDataSetThirdFunction;
      } else {
        graph.currFunc =  getXFourthFunction;
        graph.currFuncY = getYFourthFunction;
        graph.currFuncDataSet = createDataSetFourthFunction;
      }
    }

    function chooseRangeSpace(value) {
      if (value === 'x<sup>2</sup> - 2 = 0') {
        return {y1: -2, y2: 40, spacing: 10};
      } else if (value === 'cos(x) = 0') {
        return {y1: -1.3, y2: 1.3, spacing: 0.5};
      } else if (value === 'x<sup>3</sup> - 7 = 0') {
        return {y1: -15, y2:250, spacing: 50};
      } else {
        return {y1: -10, y2:140, spacing:20};
      }
    }

    function updatePoints(value_button) {
      points[0].setDataSet([new Vector2(graph.xProperty.value, 0)]);
      points.slice(1).map(point => point.setDataSet([]));
      let answerSet = [];
      let dataSet = [ new Vector2 (graph.xProperty.value, 0) ];
      let answer = graph.currFunc(graph.xProperty.value);
      if (value_button != 0) {
        dataSet.push(new Vector2 (graph.xProperty.value,
        graph.currFuncY(graph.xProperty.value)) );
        dataSet.push(new Vector2(answer.new_x, 0));
      }
      answerSet.push(answer);
      for (let i = 1; i < value_button; i++) {
        dataSet.push(new Vector2(answer.new_x, graph.currFuncY(answer.new_x) ) );
        answer = graph.currFunc(answer.new_x);
        dataSet.push(new Vector2(answer.new_x, 0));
        answerSet.push(answer);
      }
      lineplotXline.setDataSet(dataSet);
      for(let i = 1; i <= value_button; i++) {
        points[i].setDataSet([new Vector2(answerSet[i-1].new_x, 0)]);
      }
    }

    function updateGraph(y1, y2, spacing, value_button) {
      chooseFunc(graph.functionValuesProperty.value);
      lineplotFunction.setDataSet(graph.currFuncDataSet(0.01, 7));
      chartTransform.setModelYRange(new Range(y1, y2));
      tick_y.setSpacing(spacing);
      label_y.setSpacing(spacing);
      updatePoints(value_button);
    }

    function update() {
      ( {y1, y2, spacing} = chooseRangeSpace(graph.functionValuesProperty.value) );
      updateGraph(y1, y2, spacing, graph.iterationValuesProperty.value);
    }

    // Creating ticks and labels. 
    // Creating these outside and assigning them so these can be changed later.
    let tick_y = new TickMarkSet( chartTransform, Orientation.VERTICAL, 10,
    { edge: 'min' } );
    let label_y = new LabelSet( chartTransform, Orientation.VERTICAL, 10,
    { edge: 'min' } );

    // Anything you want clipped goes in here
    this.children = [

      // Background
      chartRectangle,

      // Clipped contents
      new Node( {
        clipArea: chartRectangle.getShape(),
        children: [

          // Axes nodes are clipped in the chart
          new AxisNode( chartTransform, Orientation.HORIZONTAL ),
          new AxisNode( chartTransform, Orientation.VERTICAL ),

          // All lines and functions.
          lineplotFunction,
          lineplotXline,
          point0,
          point1,
          point2,
          point3,
          point4,
          point5
        ]
      } ),

      // Tick marks outside the chart
      tick_y,
      label_y,
      // tick and label for the X axis.
      new TickMarkSet( chartTransform, Orientation.HORIZONTAL, 1, { edge: 'min' } ),
      new LabelSet( chartTransform, Orientation.HORIZONTAL, 1, {
        edge: 'min',
        createLabel: value => new Text( Math.abs( value ) < 1E-6 
        ? value.toFixed( 0 ) 
        : value.toFixed( 2 ), {
          fontSize: 12
        } )
      } )
    ];

    graph.functionValuesProperty.link(() => {
      update();
    })

    graph.iterationValuesProperty.link(() => {
      update();
    })

    graph.xProperty.link(() => {
      update();
    })


  }
}

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