Source: graphicalModel.js

/*=============== Probability Graphic Model ====================*/

"use strict";


/**
 * The regular probability Graphical that supports auto play loops and zoom in capability.
 */
class GraphicalModel {

    /**
     * Create a defiend space for the graphical model.
     * @param {object} graphConfiguration - A configuration object for configuring the properties of this _pgm, it can be obtained via Config.getPGMConfig().
     * @param {string} divID - The id of the html tag that contains this pgm, it is of the form '#id_name'.
     */
    constructor(graphConfiguration, divID) {

        /**
         * This graph's configuration.
         * @memberof GraphicalModel
         * @type {object}
         */
        this.config = graphConfiguration;

        /**
         * Graph data includes labels, vertex and edge data.
         * @memberof GraphicalModel
         * @type {object}
         * @property {object} clusterMat - A matrix of vertex labels in every layer.
         * @property {object} data - An arry of vertex data where each vertex specifies its adjacency edges.
         */
        this.graphData = {
            clusterMat: [], // data specifies the nodes in each layer
            data: [] // data binds to the graph
        };

        this._weightedAdjMat = null; // Holds the adjacency matrix chart 

        this._directedPath = []; // _directedPath is a list of visited nodes' ID

        this._canClick = true; // Used to keep user from clicking when the graph is traversing

        this._speakerLayerProbabilityDistribution = []; //  an array of probability given to each node in the speaker layer, probabilityDistribution=[] if uniform distribution


        let _pgm = this;

        this._divID = divID;

        // Click on the node in the speaker layer to draw visited path
        this.onClick = d3.behavior.drag()
            .origin(d => d)
            .on("dragstart", function(d) {
                // Check if the clicked node is in the first layer
                // which are the num of nodes in first layer of clusterMat
                // Only allow user to click the node if autoplay is off
                if (_pgm._canClick && !_pgm.config.autoPlayable) {
                    d3.event.sourceEvent.stopPropagation();
                    d3.select(this).classed("dragging", true);
                    _pgm._triggerSpeakerNode(this.id);
                }
            });

        this.svg = d3.select(divID).append("svg")
            .attr("class", "graph")
            .attr("width", this.config.transform.width)
            .attr("height", this.config.transform.height)
            .append("g")
            .attr("transform", "translate(" + this.config.transform.x + "," + this.config.transform.y + ")");

        // Set up the background rect wrapper
        this.rect = this.svg.append("rect")
            .attr("class", "background")
            .attr("width", this.config.transform.width)
            .attr("height", this.config.transform.height)
            .style("fill", this.config.background.color)
            .style("pointer-events", "all")
            .on("click", d => {
                _pgm._backgroundOnClick();
            });

        this.container = this.svg.append("g");

        // Specify the function for generating path data   
        // "linear" for piecewise linear segments
        // Creating path using data in pathinfo and path data generator
        // Used in _drawEdges() and _drawVisitedPath();
        this.line = d3.svg.line()
            .x(d => d.x)
            .y(d => d.y)
            .interpolate("linear");

        this.vertices = null; // D3 object, initiated in drawVertices()


        // Zoom behavior
        this.zoom = d3.behavior.zoom().scaleExtent([1, 10])
            .on("zoom", () => {
                this.container.attr(
                    "transform",
                    "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")"
                );
            });

        // Zoom behavior
        if (this.config.zoomable) {
            this.svg.call(this.zoom);
        }

    }

    /**
     * Reset the adjacency matrix attached to this graphical model when the background is clicked. It is called in the on click listener function defined within this graphical model.
     * @private
     */
    _backgroundOnClickToResetAdjMatrix() {
        if (this._weightedAdjMat) {
            this._weightedAdjMat.resetMatrixWeight();
            this._weightedAdjMat.resetMatrixColorWeight();
            this._weightedAdjMat.redrawMatrix();
        }
    }

    /**
     * Reset the graph after visited path highlighting is finished. It is called in the on click listener function defined within this graphical model.
     * @private
     */
    _backgroundOnClick() {
        if (!this.config.autoPlayable) {
            if (this._canClick && !this.config.autoPlay.on) {
                this._clearVisitedPath();
                // Do not allow user to click until visited path highlighting is finished
                this._canClick = false;
                setTimeout(() => this._canClick = true, this.config.edge.timeInterval * (this._directedPath.length - 1));

                // click on background to reset adjacency matrix
                this._backgroundOnClickToResetAdjMatrix();
            }
        }
    }

    /**
     * Verifies if each vertex's id matches its position in the data array and if the weights of all edges comin from each vertex sum up to 1.
     * @private
     */
    _dataScreening(data) {

        if (data.length <= 1) {
            throw new Error("input graph data is empty");
        }

        let weightSum = 0;
        for (let vertexIdx = 0; vertexIdx < data.length; vertexIdx++) {
            if (data[vertexIdx].id !== vertexIdx) {
                throw new Error("Vertex's id must match its position index in the list of vertices. The " + vertexIdx + " th element in the list does not match its position index");
            }
            let allEdgeZero = true;
            let adjVertices = data[vertexIdx].edgeWeights;
            if (adjVertices) {
                // Check if all edges have weight 0

                for (let i = 0; i < adjVertices.length; i++) {
                    weightSum += adjVertices[i].weight;
                    if (adjVertices[i].weight !== 0) allEdgeZero = false;
                }

                if (weightSum !== 1.0 && allEdgeZero === false) {
                    throw new Error("The sum of a vertex's adjacent edge's weight must be 1 or all edges have a weight of 0. " + "The " + vertexIdx + "th vertex is invalid.");
                }
            }
            weightSum = 0;
        }
    }


    /**
     * Modifies the data in graphData by adding a list of edges into each vertex.
     * @private
     */
    _createEdgesInGraphData(data) {

        if (data.length <= 1) {
            throw new Error("GraphicalModel._createEdgesInGraphData(): Input graph data is empty");
        }

        // Go through each vertex in data and add 'edges' attribute to each vertex
        for (let vertexIdx = 0; vertexIdx < data.length; vertexIdx++) {
            let currentVertex = data[vertexIdx];
            if (!currentVertex.edgeWeights) {
                currentVertex.edges = null;
            } else {
                currentVertex.edges = [];
                for (let adjVertexIdx = 0; adjVertexIdx < currentVertex.edgeWeights.length; adjVertexIdx++) {
                    let targetVertexId = currentVertex.edgeWeights[adjVertexIdx].id;
                    let targetVertexWeight = currentVertex.edgeWeights[adjVertexIdx].weight;

                    let edge = {
                        edgeWeight: targetVertexWeight,
                        edgeNodes: [currentVertex, data[targetVertexId]]
                    };

                    currentVertex.edges.push(edge);
                }
            }
        }
    }

    /**
     * Choose a random adjacent vertex in the speaker layer based on the edge weights.
     * @private
     */
    _chooseRandomAdjVertexFromSpeakerLayer() {

        let weightDistribution = [0]; // weightDistribution is a distribution from 0 to 1, ex: [0, 0.4, 1]
        let weight = 0;
        for (let i = 0; i < this._speakerLayerProbabilityDistribution.length; i++) {
            weight += this._speakerLayerProbabilityDistribution[i];
            weightDistribution.push(weight);
        }

        let randomPick = Math.random();
        console.log("weight distribution corresponding to the speaker layer: (" + weightDistribution + ") random pick: " + randomPick);
        for (let i = 0; i < weightDistribution.length - 1; i++) {
            if (randomPick >= weightDistribution[i] && randomPick <= weightDistribution[i + 1]) {
                return this.graphData.data[i].id;
            }
        }
    }

    /**
     * Takes in a vertex data object from the array of data in graphData and chooses a random adjacent vertex in the next layer based on the edge weights.
     * @private
     */
    _chooseRandomAdjVertex(vertex) {

        let weightDistribution = [0]; // weightDistribution is a distribution from 0 to 1, ex: [0, 0.4, 1]
        let weight = 0;
        for (let i = 0; i < vertex.edgeWeights.length; i++) {
            weight += vertex.edgeWeights[i].weight;
            weightDistribution.push(weight);
        }

        let randomPick = Math.random();
        console.log("weight distribution corresponding to adjacent vertices in the next layer: (" + weightDistribution + ") random pick: " + randomPick);

        // if the sum of distribution is 0 then return -1
        let distributionSum = weightDistribution.reduce(function(a, b) {
            return a + b;
        }, 0);
        if (distributionSum === 0) {
            return -1;
        }

        for (let i = 0; i < weightDistribution.length - 1; i++) {
            if (randomPick >= weightDistribution[i] && randomPick <= weightDistribution[i + 1]) {
                return vertex.edgeWeights[i].id;
            }
        }
    }

    /**
     * Takes in the id of a node and traverse trough the graph to connect. impacted nodes and returns the id of the visited node.
     * @private
     */
    _traverseGraph(vertexId, data) {

        let visitedNodes = [vertexId];
        let node = data[vertexId];

        while (node !== undefined && node.edgeWeights !== undefined) {
            console.log("Current Vertex: " + vertexId);
            vertexId = this._chooseRandomAdjVertex(node);
            // if (vertexId < 0) break;
            console.log("Vextex chosen: " + vertexId);
            console.log("--------");
            node = data[vertexId];
            visitedNodes.push(vertexId);
        }

        this._directedPath = visitedNodes;
    }


    /**
     * Draws the grid in the background.
     * @private
     */
    _drawGrid() {

        this.container.append("g")
            .attr("class", "x axis")
            .selectAll("line")
            .data(d3.range(0, this.config.transform.width, 10))
            .enter().append("line")
            .attr("x1", d => d)
            .attr("y1", 0)
            .attr("x2", d => d)
            .attr("y2", this.config.transform.height);

        this.container.append("g")
            .attr("class", "y axis")
            .selectAll("line")
            .data(d3.range(0, this.config.transform.height, 10))
            .enter().append("line")
            .attr("x1", 0)
            .attr("y1", d => d)
            .attr("x2", this.config.transform.width)
            .attr("y2", d => d);
    }

    /**
     * Draws the text labels in each vertex.
     * @private
     */
    _drawText() {
        /* Add a text element to the previously added g element. */
        this.vertices.append("text")
            .attr("font-size", d => d.r * this.config.text.size)
            .attr("text-anchor", this.config.text.anchor)
            .attr("alignment-baseline", this.config.text.alignment)
            .attr("fill", this.config.text.color)
            .text(d => {
                if (d.label) {
                    return d.label;
                } else {
                    return d.id;
                }
            });
    }

    /**
     * Draws the grpah vertices.
     * @private
     */
    _drawVertices(data) {
        /* clear vertices then redraw all the vertices in the grpah */

        d3.selectAll(this._divID + " g .vertex").remove();

        // Create vertex groups, each group contains a cicle and a text
        this.vertices = this.container.append("g")
            .attr("class", "vertex")
            .selectAll("circle")
            .data(data).enter()
            .append("g")
            .attr("id", d => d.id)
            .attr("transform", d => "translate(" + d.x + "," + d.y + ")")
            .call(this.onClick);

        this.vertices.append("circle")
            .attr("r", d => d.r);

        this._drawText();
    }

    /**
     * Draws the grpah edges highlight the clicked vertex.
     * @private
     */
    _drawEdges(data) {

        // clear edges then redraw all the edges in the graph 
        d3.selectAll(this._divID + " path").remove();

        // Draw all edges based on weight in default color
        for (let vertexIdx = 0; vertexIdx < data.length; vertexIdx++) {
            // Iterate through each nodes in data
            let currentVertex = data[vertexIdx];
            if (currentVertex.edges) {
                for (let edgeIdx = 0; edgeIdx < currentVertex.edges.length; edgeIdx++) {
                    // Iterate through each edge in the current node
                    let edgeNodes = currentVertex.edges[edgeIdx].edgeNodes;
                    let edgeWeight = currentVertex.edges[edgeIdx].edgeWeight * this.config.edge.width;
                    this.container.append("svg:path")
                        .attr("d", this.line(edgeNodes))
                        .attr("stroke-width", edgeWeight + this.config.edge.baseWidth)
                        .style("stroke", this.config.edge.defaultColor)
                        .style("fill", "none");
                }
            }
        }

    }

    /**
     * Draw an highlighted edge between two vertices.
     * @private
     * @param {object} graphConfiguration - A configuration object for configuring the properties of this _pgm, it can be obtained via Config.getPGMConfig().
     * @param {object} EdgeNodes - A pair of nodes (e.g. [node1, node2]) which are two ends of an edge, lengthMultiplier is used to determine the magnitude of the edge.
     * @param {number} lengthMultiplier - Used to increase the length of the highlighted edge on both ends.
     * @return {object} highlightedEdge - A highlightedEdge objects that contains the nodes information and the length information
     */
    _drawHighlightedEdge(edgeNodes, lengthMultiplier) {
        let x0 = edgeNodes[0].x,
            y0 = edgeNodes[0].y,
            r0 = edgeNodes[0].r,
            x1 = edgeNodes[1].x,
            y1 = edgeNodes[1].y,
            r1 = edgeNodes[1].r,
            distX = x1 - x0,
            distY = y0 - y1,
            dist = Math.sqrt(distX * distX + distY * distY),
            ratio0 = r0 / (lengthMultiplier * dist),
            ratio1 = r1 / (lengthMultiplier * dist),

            // tempEdges for highlighting the visited edges
            highlightedEdgeNodes = [{
                x: x0 + distX * ratio0,
                y: y0 - distY * ratio0
            }, {
                x: x1 - distX * ratio1,
                y: y1 + distY * ratio1
            }];


        let highlightedEdge = {
            nodes: highlightedEdgeNodes,
            length: dist
        };

        return highlightedEdge;
    }

    _drawVisitedPath(data) {
        /* Draw visited edges based on weight in highlighted color */

        for (let vertexIdx = 0; vertexIdx < this._directedPath.length; vertexIdx++) {

            // check if there's -1 in _directedPath, if yes, do not draw the path and trigger a new speaker
            if (this._directedPath[vertexIdx] < 0) {

                // Draw the first vertex when the path start highlighting
                this.vertices.append("circle")
                    .attr("class", d => {
                        // if the node is in the path then draw it in a different color
                        if (this._directedPath[0] === d.id) {
                            return "visitedVertex";
                        }
                    })
                    .attr("r", d => d.r);

                // Add a text element to the previously added g element.
                this._drawText();

                setTimeout(() => {

                    // If autoplay is on, then restart the cycle after [timeIntervalBetweenCycle] milliseconds
                    if (this.config.autoPlay.on) {
                        console.log("Auto play is on!");
                        setTimeout(() => {
                            this._triggerSpeakerNodeAutoPlay();
                        }, this.config.autoPlay.timeIntervalBetweenCycle);
                    }

                }, this.config.edge.timeInterval);
            } else {

                // If there's no -1 in directed path
                // Iterate through the list of ID in _directedPath 
                let currentVertex = data[this._directedPath[vertexIdx]];
                if (currentVertex.edges) {
                    for (let edgeIdx = 0; edgeIdx < currentVertex.edges.length; edgeIdx++) {
                        let edgeNodes = currentVertex.edges[edgeIdx].edgeNodes;
                        let edgeWeight = currentVertex.edges[edgeIdx].edgeWeight * this.config.edge.width;
                        // If the edge is in the _directedPath then draw different color
                        if (this._directedPath.indexOf(edgeNodes[0].id) > -1 && this._directedPath.indexOf(edgeNodes[1].id) > -1) {

                            // Create two new points to draw a shorter edge so the new 
                            // edge will not cover the id in the node
                            let highlightingEdgeLengthMultiplier = 1.1; // Used to increase the length of the highlighted edge on both ends;
                            let highlightedEdge = this._drawHighlightedEdge(edgeNodes, highlightingEdgeLengthMultiplier);
                            let tempEdges = highlightedEdge.nodes
                            let lineLength = highlightedEdge.length;

                            // Wait for 0.8 second until the next node is highlighted
                            // Draw the next visited path after time Interval
                            setTimeout(() => {

                                // Append a path that completes drawing wthin a time duration
                                this.container.append("svg:path")
                                    .style("stroke-width", this.config.edge.baseWidth + edgeWeight)
                                    .style("stroke", this.config.edge.visitedColor)
                                    .style("fill", "none")
                                    .attr({
                                        'd': this.line(tempEdges),
                                        'stroke-dasharray': lineLength + " " + lineLength,
                                        'stroke-dashoffset': lineLength
                                    })
                                    .transition()
                                    .duration(this.config.edge.timeInterval)
                                    .attr('stroke-dashoffset', 0);

                            }, this.config.edge.timeInterval * vertexIdx);

                            // Draw the next visited vertex after time Interval
                            setTimeout(() => {
                                /* clear vertices then redraw all the vertices in the grpah */
                                this.vertices
                                    .append("circle")
                                    .attr("class", d => {
                                        // if the node is in the path then draw it in a different color
                                        if (this._directedPath.indexOf(d.id) <= (vertexIdx + 1) &&
                                            this._directedPath.indexOf(d.id) > -1) {
                                            return "visitedVertex";
                                        }
                                    })
                                    .attr("r", d => d.r);

                                // Add a text element to the previously added g element.
                                this._drawText();

                                // Visited path ending condition
                                let endingVertexIdx = this._directedPath.length - 2;
                                if (vertexIdx === endingVertexIdx) {

                                    // If _weightedAdjMat exists, update the _weightedAdjMat adjacency matrix after the visited path finish highlighting within [timeIntervalBetweenCycle/2] milliseconds
                                    if (this._weightedAdjMat) {
                                        setTimeout(() => {
                                            this._updateChart();
                                        }, this.config.autoPlay.timeIntervalBetweenCycle / 2.0);
                                    }
                                    // If autoplay is on, then restart the cycle after [timeIntervalBetweenCycle] milliseconds
                                    if (this.config.autoPlay.on) {
                                        console.log("Auto play is on!");
                                        setTimeout(() => {
                                            this._triggerSpeakerNodeAutoPlay();
                                        }, this.config.autoPlay.timeIntervalBetweenCycle);
                                    }
                                }

                                // 0.95 is a time offset multiplier to make vertex colored faster since
                                // there is an unknown lag
                            }, this.config.edge.timeInterval * (vertexIdx + 1));

                            // Draw the first vertex when the path start highlighting
                            this.vertices.append("circle")
                                .attr("class", d => {
                                    // if the node is in the path then draw it in a different color
                                    if (this._directedPath[0] === d.id) {
                                        return "visitedVertex";
                                    }
                                })
                                .attr("r", d => d.r);

                            // Add a text element to the previously added g element.
                            this._drawText();
                        }
                    }
                }
            }
        }
    }

    /**
     * Used to redraw the graph on start and when moving.
     * @private
     */
    _drawGraph(data) {
        this._drawEdges(data);
        this._drawVertices(data);
    }

    /**
     * Kill all setTimeOut used to draw the visited path.
     * @private
     */
    _killAllSetTimeOut() {
        for (var i = 1; i < 99999; i++) {
            window.clearInterval(i);
            window.clearTimeout(i);
            if (window.mozCancelAnimationFrame) window.mozCancelAnimationFrame(i); // Firefox
        }
    }

    /**
     * Clear the highlighted path and redraw the graph.
     * @private
     */
    _clearVisitedPath() {

        this._killAllSetTimeOut();

        // Then clear the path storage
        this._directedPath = [];
        this._drawGraph(this.graphData.data);
    }

    /**
     * Create the cycling speed control button on the top of the graph.
     * @private
     */
    _createCyclingSpeedControlButton() {
        let _pgm = this;

        let sliderID = this._divID.substring(1) + "-slider-range";
        let $DivSlider = $("<div>", {
            id: sliderID
        });
        $(this._divID).prepend($DivSlider);
        $("#" + sliderID).slider({
            range: false, // two buttons caps a range
            min: 2,
            max: 1000,
            value: _pgm.config.edge.timeInterval,
            slide: function(event, ui) {
                _pgm._cyclingSpeedControlButtonOnClick(ui);
            }
        });

        let sliderWidth = (this._weightedAdjMat === null) ? this.config.transform.width : this._weightedAdjMat.config.transform.width + this.config.transform.width;
        $("#" + sliderID).css("width", sliderWidth + "px");
    }

    /**
     * Called by jQuery slider function defined in _createCyclingSpeedControlButton to update the graph cycling speed based on the position of the UI button.
     * @private
     */
    _cyclingSpeedControlButtonOnClick(ui) {
        console.log("Slider Speed: " + ui.value);
        let sphereRad = ui.value;
        this.config.edge.timeInterval = ui.value;
        this.config.autoPlay.timeIntervalBetweenCycle = ui.value;
    }

    /**
     * Create the auto play button.
     * @private
     */
    _createPlayButton() {
        /* Used to create a play button, it modifies the default button property in button.css */

        var $Button = $("<div>", {
            class: "play-button paused"
        });
        var $left = $("<div>", {
            class: "left"
        });
        var $right = $("<div>", {
            class: "right"
        });
        var $triangle1 = $("<div>", {
            class: "triangle-1"
        });
        var $triangle2 = $("<div>", {
            class: "triangle-2"
        });

        $(this._divID).prepend($Button);

        $Button.append($left);
        $Button.append($right);
        $Button.append($triangle1);
        $Button.append($triangle2);

        // Update button dimension first
        let resizedButton = Array.min([this.config.transform.height, this.config.transform.width]) / 10.0 * this.config.autoPlay.button.dim;
        let maxButtonSize = 35.0; // The max button size is 40px so that buttons won't get too big
        this.config.autoPlay.button.dim = (resizedButton > maxButtonSize) ? maxButtonSize : resizedButton;

        $(this._divID + " .play-button").css("height", this.config.autoPlay.button.dim + "px")
            .css("width", this.config.autoPlay.button.dim + "px");

        $(this._divID + " .triangle-1").css("border-right-width", this.config.autoPlay.button.dim + "px")
            .css("border-top-width", this.config.autoPlay.button.dim / 2.0 + "px")
            .css("border-bottom-width", this.config.autoPlay.button.dim / 2.0 + "px");

        $(this._divID + " .triangle-2").css("border-right-width", this.config.autoPlay.button.dim + "px")
            .css("border-top-width", this.config.autoPlay.button.dim / 1.9 + "px")
            .css("border-bottom-width", this.config.autoPlay.button.dim / 2.0 + "px");

        $(this._divID + " .left").css("background-color", this.config.autoPlay.button.color);
        $(this._divID + " .right").css("background-color", this.config.autoPlay.button.color);

        let _pgm = this;
        $(this._divID + " .play-button").click(function() {
            $(this).toggleClass("paused");
            if (_pgm.config.autoPlay.on) {
                _pgm._stopAutoPlay();
            } else {
                _pgm._startAutoPlay();
            }
        });
    }


    /**
     * Triggers a speaker node randomly following the specified speaker ndoe probability distribution.
     * @private
     */
    _triggerSpeakerNodeAutoPlay() {

        let chosen_id;
        // If speaker node is of uniform distribution
        if (this._speakerLayerProbabilityDistribution.length == 0) {
            chosen_id = Math.floor(Math.random() * this.graphData.clusterMat[0].length);
        } else {
            chosen_id = this._chooseRandomAdjVertexFromSpeakerLayer();
        }
        this._triggerSpeakerNode(chosen_id);
    }

    /**
     * Triggers a speaker node by id, traverse down and draw the visited path.
     * @private
     */
    _triggerSpeakerNode(id) {

        let speakerLayerLength = this.graphData.clusterMat[0].length;

        // Only allow the node to be clicked if it is in the speaker layer
        if (id < speakerLayerLength) {
            let clickedVertexId = parseInt(id, 10);
            this._traverseGraph(clickedVertexId, this.graphData.data);
            log("visited path = [" + this._directedPath + "]");
            this._drawGraph(this.graphData.data);
            this._drawVisitedPath(this.graphData.data);

            // testing 
            $(this._divID + ' .path strong').text(this._directedPath);
        } else {
            // Else clear the path
            this._clearVisitedPath();
        }

        // Do not allow user to click
        this._canClick = false;
        setTimeout(() => this._canClick = true, this.config.edge.timeInterval * (this._directedPath.length - 1));

    }

    /**
     * Use this to redraw the graph after reset edge weights.
     * @return {object} This graphicalModel object.
     */
    redraw() {
        this._createEdgesInGraphData(this.graphData.data);
        this._drawGraph(this.graphData.data);
        return this;
    }

    /**
     * Used to create and display the graph. Normally called after createCluster().
     * @return {object} this graphicalModel object.
     * @example
     * graphicalModel.init();
     */
    init() {

        this._dataScreening(this.graphData.data);

        this.setUniformEdgeWeights();

        this._createEdgesInGraphData(this.graphData.data);

        if (this.config.autoPlayable) this._createPlayButton();

        if (this.config.cyclingSpeedControllable) this._createCyclingSpeedControlButton();

        if (this.config.background.grid) this._drawGrid();

        this._drawGraph(this.graphData.data);

        return this;
    }

    /**
     * Used to get the weighted adjacency matrix object attached to this graph.
     * @return {object} The weighted adjacency matrix object.
     */
    getWeightedAdjacencyMatrix() {
        return this._weightedAdjMat;
    }

    /**
     * Set the adjacency edges for a vertex by id.
     * @param {number} id - The id or the index of the vertex in the data array of the graphData.
     * @param {object} edges - The object contains the adjacency edges of a vertex and their weights.
       return this pgm to allow setEdgeWeights to be stacked.
     * @example
     * // Create three directed edges 0->3, 0->4, 0->5 with edge weiths 0.8, 0.1 and 0.1
     * graphicalModel.setEdgeWeights(0, [{
            id: 3,
            weight: 0.8
        }, {
            id: 4,
            weight: 0.1
        }, {
            id: 5,
            weight: 0.1
        }]);
     * @return {object} This grpahicalModel object.
     */
    setEdgeWeights(id, edges) {
        /* Set adjacent vertex for vertex with id 
            return this pgm to allow setEdgeWeights to be stacked
        */

        if (id === undefined || edges === undefined) {
            throw new Error("graphicalModel.setEdgeWeights(id, adjVtx) params are not defined.");
        }

        this.graphData.data[id].edgeWeights = edges;
        this.redraw();

        return this;
    }

    //    this.setLabel = function (id, label) {
    //        /* Set label for vertex */
    //        this.graphData.data[id].label = label;
    //    };

    //    this.getGraphData = function () {
    //        /* Returns the graphData as  JSON object */
    //        let jsonGraphData = Utils.cloneDR(this.graphData);
    //
    //        console.log(jsonGraphData);
    //
    //        // Delete all the edge circular structures in the object
    //        for (let i = 0; i < jsonGraphData.data.length; i++) {
    //            delete(jsonGraphData.data[i].edges);
    //        }
    //
    //        return JSON.stringify(jsonGraphData);
    //    };


    /** 
     * A helper method used by createCluster() to change the speaker layer ndoe radius based on the probability distribution.
     * @private
     */
    _changeNodeRadius(baseRadius) {
        /* 
        probabilityDistribution is the array of probability given to each node in the speaker layer
        set probabilityDistribution=[] for uniform distribution
        */

        let normalizeBaseRadiusMultiplier = 0.4; // increase base radius size
        let normalizeExtraRadiusBasedOnDistributionMultiplier = 0.7; // increase extra radius size

        for (let i = 0; i < this._speakerLayerProbabilityDistribution.length; i++) {
            // Normalize the radius
            let normalizationFactor = 1.0 / this._speakerLayerProbabilityDistribution.length / normalizeExtraRadiusBasedOnDistributionMultiplier;
            this.graphData.data[i].r = (baseRadius * normalizeBaseRadiusMultiplier) + this.graphData.data[i].r * (this._speakerLayerProbabilityDistribution[i] * 1.0) / normalizationFactor;
        }
    }

    /**
     * Used to create an array of vertix data in graphData based on the label cluster matrix (cMat). The graph edge weights are set to be uniform by defaut.
     * @param {array} cMat - The label cluster matrix holds the labels. Ex of cluster mat [layer1_label_array, layer2_label_array, layer3_label_array] where each layer_label_array holds an array of labelrs in one layer.
     * @param {array} probabilityDistribution - The array of probability given to each node in the speaker layer to be triggered. For uniform distribution, set probabilityDistribution = [].
     * @param {boolean} changeNodeRadiusBasedOnDistribution - Governs whether vertex radius are affected by its distribution.
     * @return {object} This graphicalModel object.
     * @example
     * graphicalModel.createCluster(clusterMat, speakerNodeProbabilityDistribution, true);
     */
    createCluster(cMat, probabilityDistribution, changeNodeRadiusBasedOnDistribution) {

        // Error checking
        if (probabilityDistribution.length != 0) {
            if (cMat[0].length != probabilityDistribution.length) {
                throw new Error("graphicalModel.createCluster(): the number of the nodes in the first layer in cMat does not match the length of the probabilityDistribution array");
            }
            let tempDistTotal = 0;
            for (let i = 0; i < probabilityDistribution.length; i++) {
                tempDistTotal += probabilityDistribution[i];
            }
            if (tempDistTotal != 1.0) {
                throw new Error("graphicalModel.createCluster(): the probability of each node in the speaker layer does not add up to 1.0 in probabilityDistribution array");
            }
        }

        this._speakerLayerProbabilityDistribution = probabilityDistribution;

        this.cMatDim = []; //cMatDim is the dimension of the matrix, ex: [3,3,3]

        // Populate cMatDim
        for (let i = 0; i < cMat.length; i++) {
            this.cMatDim[i] = cMat[i].length;
        }

        let offsetPosX = this.config.transform.width / (this.cMatDim.length + 1); // get the x offset for first node
        let minPosY = this.config.transform.height / (Array.max(this.cMatDim) + 1); // get the y offset for the layer with the most amount of nodes

        // Data properties: id, x, y, r 
        let data = [];
        let id = 0;
        let x;
        let y;
        let r = Array.min([offsetPosX, minPosY]) * this.config.vertex.radius;

        this.config.vertex.radius = r;

        for (let i = 0; i < this.cMatDim.length; i++) {
            // Reset offset Y coordinate for each layer
            let offSetPosY = this.config.transform.height / (this.cMatDim[i] + 1);
            for (let j = 0; j < this.cMatDim[i]; j++) {
                x = offsetPosX * (i + 1);
                y = offSetPosY * (j + 1);
                data.push({
                    id: id,
                    x: x,
                    y: y,
                    r: r
                });
                id++;
            }

        }


        // Label each vertex based on cMat labels
        let id_temp = 0;
        for (let i = 0; i < cMat.length; i++)
            for (let j = 0; j < cMat[i].length; j++)
                data[id_temp++].label = cMat[i][j];


        // Update the this.config edge width and baseWidth
        this.config.edge.width = r * this.config.edge.width;
        this.config.edge.baseWidth = r * this.config.edge.baseWidth;


        // Create the graphData member variable in _pgm
        this.graphData = {
            clusterMat: cMat,
            data: data
        };

        // Change speaker node radius based on distribution
        if (changeNodeRadiusBasedOnDistribution && probabilityDistribution.length > 0) this._changeNodeRadius(r);

        return this;
    }

    /**
     * Get the vertex id by vertexCoordinate.
     * @param {array} vertexCoordinate - A coordiante pair, e.g [layer index, vertex index at that layer]
     * @return {number} id_temp - The id of the vertex in the data array of the graphData.
     */
    getVertexId(vertexCoordinate) {

        let layerIdx = vertexCoordinate[0];
        let vertexIdx = vertexCoordinate[1];

        if (layerIdx >= this.cMatDim.length || vertexIdx >= this.cMatDim[layerIdx])
            throw new Error("graphicalModel.getVertexId(): invalid vertex coordinate input, the vertex being accessed does not exist in the graph. Your input vertex coordinate is [" + vertexCoordinate + "], but the dimention of the cluster matrix is [" + this.cMatDim + "].");

        let id_temp = 0;
        for (let i = 0; i < layerIdx; i++) id_temp += this.cMatDim[i];
        id_temp += vertexIdx;

        return id_temp;
    }


    /**
     * Set the graph edge weights to be uniform.
     */
    setUniformEdgeWeights() {
        for (let layerIdx = 0; layerIdx < this.cMatDim.length - 1; layerIdx++) {
            for (let vertexIdx = 0; vertexIdx < this.cMatDim[layerIdx]; vertexIdx++) {
                let vertexID = this.getVertexId([layerIdx, vertexIdx]);
                let numOfNodesNextLayer = this.cMatDim[layerIdx + 1];
                let edgeWeights = [];
                for (let i = 0; i < numOfNodesNextLayer; i++) {
                    edgeWeights[i] = {
                        id: this.getVertexId([layerIdx + 1, i]),
                        weight: 1.0 / numOfNodesNextLayer
                    };
                }
                this.setEdgeWeights(vertexID, edgeWeights);
            }
        }
    }


    /*=========== Graphical Model Autoplay ===========*/

    /**
     * Reset the weighted adjacency matrix weights.
     */
    resetChart() {
        this._weightedAdjMat.resetMatrixWeight();
        this._weightedAdjMat.redrawMatrix();
    }

    /**
     * Start the autoPlay cycle. Called in on click listener function defiend in the vertex.
     * @private
     */
    _startAutoPlay() {
        /* called by the play button to start autoplay */
        this._canClick = false;
        if (this._weightedAdjMat) this.resetChart();
        this.config.autoPlay.on = true;
        this._triggerSpeakerNodeAutoPlay();
    }

    /**
     * Stop the autoPlay cycle. Called in on click listener function defiend in the vertex.
     * @private
     */
    _stopAutoPlay() {
        /* called by the stop button to stop autoplay */
        this._canClick = true;
        this.config.autoPlay.on = false;

        this._clearVisitedPath();

        if (this._weightedAdjMat) {
            this._weightedAdjMat.resetMatrixWeight();
            this._weightedAdjMat.resetMatrixColorWeight();
            // this._weightedAdjMat.redrawMatrix();
        }
    }


    /*======== Binding Adjacency Matrix To The Graphical Model =======*/

    /**
     * Used in _drawVisitedPath() to update the adjacency matrix cell weights and color.
     * @private
     */
    _updateChart() {
        let _rowIdx = this._directedPath[0];
        let _colIdx = this._directedPath[this._directedPath.length - 1];
        if (_rowIdx < 0 || _colIdx < 0) return;

        let _rowLabel = this.graphData.data[_rowIdx].label;
        let _colLabel = this.graphData.data[_colIdx].label;
        let cellToUpdate = [_rowLabel, _colLabel];
        log("Update Cell: [" + cellToUpdate + "]");
        this._weightedAdjMat.increaseCellWeight(cellToUpdate, 1);
        this._weightedAdjMat.increaseCellColor(cellToUpdate, 1);
        this._weightedAdjMat.redrawMatrix();
    }


    /**
     * Used create a weighted adjacency matrix for this graph based on the matrix config object.
     * @param {object} chartConfig - The matrix configuration object. It can be obtained via Config.getAdjacencyMatrixConfig().
     * @return {object} This graphicalModel object.
     */
    createAdjacencyMatrix(chartConfig) {
        /* Create a _weightedAdjMat and bind to the graphic model */

        this.chartConfig = chartConfig;

        if (this.graphData.clusterMat.length < 2) {
            throw new Error("graphicalModel.createAdjacencyMatrix(): Can not create adjacency matrix for graphical model with layer number less than 2");
            return;
        }

        let _rowLabel = this.graphData.clusterMat[0];
        let _colLabel = this.graphData.clusterMat[this.graphData.clusterMat.length - 1];
        this._weightedAdjMat = new WeightedAdjacencyMatrix(this._divID, chartConfig);
        this._weightedAdjMat.createMatrix(_rowLabel, _colLabel);

        return this;
    }
}