Overview of the framework, its contents, and how to get started with it.
This framework serves as a base to implement Data Structure Visualisations, providing tools to create graphic items, animate and manage them. It was built in a way that anyone with some knowledge of Object Oriented Programming and JavaScript can use it, without having knowledge about SVG or D3.js.
In this user guide we'll begin by studying each class and then we will proceed to learn how to use them to create a visualisation.
The classes provided were designed to control the graphic part of the visualisation, and should be integrated with the algorithms of each data structure.
In this project we decided to use GitHub for source code control, which means that if you want to contribute you should be familiar with it. If you're not we recommend this tutorial. It goes straight to the point without any complications.
If you're already familiar with git, all you have to do is checkout the project repository by running the terminal command:
git clone https://github.com/DSVProject/dsvproject.git
If you're not confortable with terminal, github offers clients for all Operating Systems.
You've cloned the project to your computer and now what? Let's understand how the framework directories are organised:
DSVProject/ ├── code/ │ ├── bootstrap/ │ ├── d3/ │ ├── js/ │ │ ├── util.js │ │ ├── third-party/ │ │ ├── algorithms/ │ │ │ ├── template.js │ │ │ └── visualisation JS files │ │ ├── framework/ │ │ │ ├── coreObject.js │ │ │ ├── squareObject.js │ │ │ ├── circleObject.js │ │ │ ├── edgeObject.js │ │ │ ├── userObject.js │ │ │ ├── svg.js │ │ │ └── constant.js │ │ └── custom/ │ │ └── Custom shapes and sets of shapes JS files │ ├── css/ │ │ ├── animation.css │ │ ├── template.css │ │ └── Other CSS files │ ├── template.html │ └── Visualisatons HTML files ├── documentation/ │ ├── userGuide/ │ │ ├── css/ │ │ ├── js/ │ │ ├── img/ │ │ └── userGuide.html │ └── .doc files ├── index.html └── readme
Although it might look complex there are only 3 folders that you should worry about:
The .css file that controls the styling of the shapes if called animation.css.
The main website and the visualisation pages were built using Boostrap. The .css file that control the styling of the pages is called template.css, and the JavaScript functions related to the page behavior are located inside util.js.
The files used to develop a visualisation can be divided mainly into 2 layers: data and graphics. First let's take a look at the big picture:
Now let's understand what's going on in there. On top of everything we have the .html file which is going to be the page of our data structure visualisation. This file will interact with our data layer, that contains the algorithms of this data structure, in a JavaScript file. With the algorithms ready it's time to make them turn into graphics, what brings us to the next layer.
In your data structure JavaScript file you're going to need an instance of CoreObject.js, which is the main class of the graphic layer. It is responsible for the management and control of all graphical classes and animations. When creating a circle, or square, you'll do this through the core, that will store this object for you, but all the details of this are going to be clarified in the next sections of this guide.
All visualisations will be built using instances of these items. For example, an array will be a combination of Squares positioned side by side, while a node will be a combination of a Square with an Edge. For this reason let's take a look on each of them to understand how they're meant to be used.
The square is one of the basic shapes and here's how it will look on the screen:
As you noticed by the annotations on the image this object is composed of 3 elements: a shape, an inner text and a label (the last two being optional) but we'll get into this a little ahead.
The class is called SquareObject, and to create an instance of a class in JavaScript all you have to do is the same that you would do in any other OO language:
var newObj = new SquareObject(coreObj, id, x, y, text, label, shapeClass, textClass, labelClass);
Now let's understand all those parameters:
As you will be able to notice in the javadoc, some of this parameters are not obligatory (like text, label, classes), making it easy to customise a shape. Regarding the classes, it is always recommended to send "null" as parameter since all instances will inherit a default class, unless you would like it to have a different look.
Now let's take a look of a few examples:
Square 01 is an instance of a SquareObject without a label, while Square 02 has one. All the other Squares have an EdgeObject associated with them, where each instance was created with a different "outgoingPoint" parameter, which means that when the pointer is null (in here indicated by the color red) it will point into a different direction. When leaving from the bottom, if the SquareObject has a label, the Edge will take this into consideration (as seen in Square 08).
These fields will store all the important data regarding this instance.
this.coreObj = coreObj;
Store the reference to the object sent as coreObj parameter.
this.textAdjustX = defaultProperties.shape.width / 2; this.textAdjustY = defaultProperties.shape.height / 1.75; this.labelAdjustY = defaultProperties.shape.height + 30;
When drawing the properties onto the screen, the inner text and the label may need some positioning adjustments, which will be stored in these.
this.propObj = {
"id": id,
"shape": {
"class": shapeClass,
"x": x,
"y": y,
"width": defaultProperties.shape.width,
"height": defaultProperties.shape.height,
"fill": defaultProperties.shape.fill.default,
"fillOpacity": defaultProperties.shape["fill-opacity"].default,
"stroke": defaultProperties.shape.stroke.default,
"strokeWidth": defaultProperties.shape["stroke-width"].default
},
"text": {
"class": textClass,
"x": x + this.textAdjustX,
"y": y + this.textAdjustY,
"fill": defaultProperties.text.stroke.default,
"fontFamily": defaultProperties.text["font-family"],
"fontWeight": defaultProperties.text["font-weight"],
"fontSize": defaultProperties.text["font-size"],
"textAnchor": defaultProperties.text["text-anchor"],
"text": text
},
"label": {
"class": labelClass,
"x": x + this.textAdjustX,
"y": y + this.labelAdjustY,
"text": label
},
"toRemove": false,
"isValidTarget": false
}
This is the most important field of this object. It is a property map that will store all the data regarding the shape, the text, the label and other variables used for functionality. When drawing this object on the screen these values will be taken into consideration.
But as you might have noticed there's two properties in there that doesn't seem quite like "graphical", and you're right. Each one is a boolean flag to control an specific behavior:
this.edgeList = [];
This is an array that will contain all the instances of EdgeObject that are binded to this object.
this.widthAdjust = [];
This is an array that will contain the offsets to redraw this shape on the screen based on data from its parent and children.
Let's first take a look at all the methods from this class:
this.getAttributes = function () {...}
this.getEdges = function () {...}
this.getID = function () {...}
this.setShapeClass = function (newClass) {...}
this.getShapeClass = function () {...}
this.moveShape = function (x, y) {...}
this.getCoordinateX = function () {...}
this.getEdgeCoordinateX = function (point) {...}
this.getCoordinateY = function () {...}
this.getEdgeCoordinateY = function (point) {...}
this.setWidth = function (newWidth) {...}
this.getWidth = function () {...}
this.setHeight = function (newHeight) {...}
this.getHeight = function () {...}
this.setFill = function (newFill) {...}
this.setFillOpacity = function (newOpacity) {...}
this.setStroke = function (newStroke) {...}
this.setStrokeWidth = function (newStrokeWidth) {...}
this.setText = function (newText) {...}
this.getText = function () {...}
this.setTextClass = function (newClass) {...}
this.getTextClass = function () {...}
this.setFontColor = function (newColor) {...}
this.setLabel = function (newLabel) {...}
this.setLabelClass = function (newClass) {...}
this.getLabelClass = function () {...}
this.setToRemove = function (bool) {...}
this.getToRemove = function () {...}
this.setIsValidTarget = function (bool) {...}
this.getIsValidTarget = function () {...}
this.addEdge = function (edgeObj) {...}
this.getEdgeCount = function () {...}
this.repositionDAG = function(x, y, side, orientation) {...}
this.draw = function (duration) {...}
this.remove = function (duration) {...}
this.cloneObject = function () {...}
this.cloneProperties = function (prop) {...}
this.cloneEdges = function (edges) {...}
Most of them are just getters and setters for the property map and when creating your own visualisation they are the only ones you should worry. The other methods are used internally by the framework, so here's a brief explanation on them:
This method will add an instance of EdgeObject to this object edgeList[]. Edges are always stored in the origin object, even if the edge is bidirectional.
Knowing where to create each shape can get tricky when dealing with more complex data structures as a binary search tree, for example. With this function you'll be able to pass null as parameter for the coordinates, and the shape position will be calculated automatically. However this function only works with Directed acyclic graph.
The Draw method will get the data from the property map and bind it to HTML elements created with D3, making them appear on the screen or update the looks of an existing object.
The Remove method will delete the HTML elements, removing the object from the screen.
An EdgeObject is the representation of the Line SVG element, which goes from point (x1,y1) to point (x2,y2). When getting these coordinates this function will be called, returning them while taking into consideration the properties of this object.
Cloning objects is very important in the way this framework was designed and this will become clearer after we study the CoreObject. There is one main function that calls two subfunctions:
this.cloneObject = function () {
var clone = new SquareObject(this.coreObj);
clone.cloneProperties(this.propObj);
clone.cloneEdges(this.edgeList);
return clone;
}
Which calls:
this.cloneProperties = function (prop) {
this.propObj = clone(prop);
}
this.cloneEdges = function (edges) {
var newList = [];
var clone;
for (var key in edges) {
clone = new EdgeObject(this.coreObj);
clone.cloneProperties(edges[key].getAttributes());
newList[key] = clone;
}
this.edgeList = newList;
}
These functions create an exact clone of the current instance, without memory references, what will enable the animation to happen step by step.
When creating a clone, you have to create a new instance of the current object (SquareObject in this case) and the only parameter required is coreObj. The subfunctions will take care of copying the property map and the edge array.
The circle is one of the basic shapes and here's how it will look on the screen:
As you noticed by the annotations on the image this object is composed of 3 elements: a shape, an inner text and a label (the last two being optional) but we'll get into this a little ahead.
The class is called CircleObject, and to create an instance of a class in JavaScript all you have to do is the same that you would do in any other OO language:
var newObj = new CircleObject(coreObj, id, cx, cy, radius, text, label, shapeClass, textClass, labelClass);
Now let's understand all those parameters:
As you will be able to notice in the javadoc, some of this parameters are not obligatory (like text, label, classes), making it easy to customise a shape. Regarding the classes, it is always recommended to send "null" as parameter since all instances will inherit a default class, unless you would like it to have a different look.
Now let's take a look of a few examples:
Circle 01 is an instance of a CircleObject without a label, while Circle 02 have one. All the other Circles have an EdgeObject associated with them, where each instance was created with a different "outgoingPoint" parameter, which means that when the pointer is null (in here indicated by the color red) it will point into a different direction. When leaving from the bottom, if the CircleObject has a label, the Edge will take this into consideration (as seen in Circle 08).
These fields will store all the important data regarding this instance.
this.coreObj = coreObj;
Store the reference to the object sent as coreObj parameter.
this.textAdjust = defaultProperties.text["font-size"]/3; this.labelAdjust = defaultProperties.shape.radius + 30;
When drawing the properties onto the screen, the inner text and the label may need some positioning adjustments, which will be stored in these.
this.propObj = {
"id": id,
"shape": {
"class": shapeClass,
"cx": cx,
"cy": cy,
"r": radius,
"fill": defaultProperties.shape.fill.default,
"fillOpacity": defaultProperties.shape["fill-opacity"].default,
"stroke": defaultProperties.shape.stroke.default,
"strokeWidth": defaultProperties.shape["stroke-width"].default
},
"text": {
"class": textClass,
"x": cx,
"y": cy + this.textAdjust,
"fill": defaultProperties.text.stroke.default,
"fontFamily": defaultProperties.text["font-family"],
"fontWeight": defaultProperties.text["font-weight"],
"fontSize": defaultProperties.text["font-size"],
"textAnchor": defaultProperties.text["text-anchor"],
"text": text
},
"label": {
"class": labelClass,
"x": cx,
"y": cy + this.labelAdjust,
"text": label
},
"toRemove": false,
"isValidTarget": false
}
This is the most important field of this object. It is a property map that will store all the data regarding the shape, the text, the label and other variables used for functionality. When drawing this object on the screen these values will be taken into consideration.
But as you might have noticed there's two properties in there that doesn't seem quite like "graphical", and you're right. Each one is a boolean flag to control an specific behavior:
this.edgeList = [];
This is an array that will contain all the instances of EdgeObject that are binded to this object.
this.widthAdjust = [];
This is an array that will contain the offsets to redraw this shape on the screen based on data from its parent and children.
Let's first take a look at all the methods from this class:
this.getAttributes = function () {...}
this.getEdges = function () {...}
this.getID = function () {...}
this.setShapeClass = function (newClass) {...}
this.getShapeClass = function () {...}
this.moveShape = function (x, y) {...}
this.getCoordinateCX = function () {...}
this.getEdgeCoordinateX = function (point) {...}
this.getCoordinateCY = function () {...}
this.getEdgeCoordinateY = function (point) {...}
this.setRadius = function (newRadius) {...}
this.getRadius = function () {...}
this.setFill = function (newFill) {...}
this.setFillOpacity = function (newOpacity) {...}
this.setStroke = function (newStroke) {...}
this.setStrokeWidth = function (newStrokeWidth) {...}
this.setText = function (newText) {...}
this.getText = function () {...}
this.setTextClass = function (newClass) {...}
this.getTextClass = function () {...}
this.setFontColor = function (newColor) {...}
this.setLabel = function (newLabel) {...}
this.setLabelClass = function (newClass) {...}
this.getLabelClass = function () {...}
this.setToRemove = function (bool) {...}
this.getToRemove = function () {...}
this.setIsValidTarget = function (bool) {...}
this.getIsValidTarget = function () {...}
this.addEdge = function (edgeObj) {...}
this.getEdgeCount = function () {...}
this.repositionDAG = function(x, y, side, orientation) {...}
this.draw = function (duration) {...}
this.remove = function (duration) {...}
this.cloneObject = function () {...}
this.cloneProperties = function (prop) {...}
this.cloneEdges = function (edges) {...}
Most of them are just getters and setters for the property map and when creating your own visualisation they are the only ones you should worry. The other methods are used internally by the framework, so here's a brief explanation on them:
This method will add an instance of EdgeObject to this object edgeList[]. Edges are always stored in the origin object, even if the edge is bidirectional.
Knowing where to create each shape can get tricky when dealing with more complex data structures as a binary search tree, for example. With this function you'll be able to pass null as parameter for the coordinates, and the shape position will be calculated automatically. However this function only works with Directed acyclic graph.
The Draw method will get the data from the property map and bind it to HTML elements created with D3, making them appear on the screen or update the looks of an existing object.
The Remove method will delete the HTML elements, removing the object from the screen.
An EdgeObject is the representation of the Line SVG element, which goes from point (x1,y1) to point (x2,y2). When getting these coordinates this function will be called, returning them while taking into consideration the properties of this object.
Cloning objects is very important in the way this framework was designed and this will become clearer after we study the CoreObject. There is one main function that calls two subfunctions:
this.cloneObject = function () {
var clone = new CircleObject(this.coreObj);
clone.cloneProperties(this.propObj);
clone.cloneEdges(this.edgeList);
return clone;
}
Which calls:
this.cloneProperties = function (prop) {
this.propObj = clone(prop);
}
this.cloneEdges = function (edges) {
var newList = [];
var clone;
for (var key in edges) {
clone = new EdgeObject(this.coreObj);
clone.cloneProperties(edges[key].getAttributes());
newList[key] = clone;
}
this.edgeList = newList;
}
These functions create an exact clone of the current instance, without memory references, what will enable the animation to happen step by step.
When creating a clone, you have to create a new instance of the current object (SquareObject in this case) and the only parameter required is coreObj. The subfunctions will take care of copying the property map and the edge array.
The edge (in SVG known as line) is one of the basic elements, used to connect two shapes. It might also have a marker element, in case the edge needs to be directed.
There are three types of edges:
Although and EdgeObject is connecting two shapes it only belongs to the edgeList[] as we've mentioned earlier. This happens for two reasons: it makes the management of edges easier for the developer and during the drawing operations it will avoid redundancy.
The class is called EdgeObject, and to create an instance of a class in JavaScript all you have to do is the same that you would do in any other OO language:
var newObj = new EdgeObject(coreObj, id, idObjectA, idObjectB, edgeClass, edgeType, outboundPoint, inboundPoint);
Now let's understand all those parameters:
As you will be able to notice in the javadoc, some of this parameters are not obligatory (like idObjectB, class), making it easy to customise an edge. Regarding the class, it is always recommended to send "null" as parameter since all instances will inherit a default class, unless you would like it to have a different look.
These fields will store all the important data regarding this instance.
this.coreObj = coreObj;
Store the reference to the object sent as coreObj parameter.
this.propObj = {
"id": id,
"idObjectA": idObjectA,
"idObjectB": idObjectB,
"type": edgeType,
"outboundPoint": outboundPoint != null ? outboundPoint : EDGE_POSITION.CENTER,
"inboundPoint": inboundPoint != null ? inboundPoint : EDGE_POSITION.CENTER,
"markerStart": idObjectB != null ? defaultProperties.marker.start.default : defaultProperties.marker.start.null,
"markerEnd": idObjectB != null ? defaultProperties.marker.end.default : defaultProperties.marker.end.null,
"edge": {
"class": edgeClass,
"x1": null,
"y1": null,
"x2": null,
"y2": null,
"stroke": idObjectB != null ? defaultProperties.edge.stroke.default : defaultProperties.edge.stroke.null,
"strokeWidth": idObjectB != null ? defaultProperties.edge["stroke-width"].default : defaultProperties.edge["stroke-width"].null
}
}
This is the most important field of this object. It is a property map that will store all the data regarding the edge and other variables used for functionality. When drawing this object on the screen these values will be taken into consideration.
Let's first take a look at all the methods from this class:
this.getAttributes = function () {...}
this.getID = function () {...}
this.getIdObjectA = function () {...}
this.setIdObjectA = function (newID) {...}
this.getIdObjectB = function () {...}
this.setIdObjectB = function (newID) {...}
this.getType = function () {...}
this.setType = function (newType) {...}
this.setOutboundPoint = function (newValue) {...}
this.getOutboundPoint = function () {...}
this.setInboundPoint = function (newValue) {...}
this.getInboundPoint = function () {...}
this.setMarkerStart = function (newMarker) {...}
this.setMarkerEnd = function (newMarker) {...}
this.setEdgeClass = function (newClass) {...}
this.getCoordinateX1 = function(){...}
this.getCoordinateY1 = function(){...}
this.getCoordinateX2 = function(){...}
this.getCoordinateY2 = function(){...}
this.setStroke = function (newStroke) {...}
this.setStrokeWidth = function (newStrokeWidth) {...}
this.calculatePath = function (isClone) {...}
this.repositionDAG = function (x, y, side, adjustLeft, adjustRight, orientation) {...}
this.draw = function (duration) {...}
this.remove = function (duration) {...}
this.cloneProperties = function (prop) {...}
Most of them are just getters and setters for the property map and when creating your own visualisation they are the only ones you should worry. The other methods are used internally by the framework, so here's a brief explanation on them:
As we've mentioned earlier the instance of this object is added to ObjectA's edgeList[], so when changing this object (in a tree rotation for example) a special function will be called, that will take care of removing the edge from the old object edgeList[] and adding it to the new one.
The Draw method will get the data from the property map and bind it to HTML elements created with D3, making them appear on the screen or update the looks of an existing object.
The Remove method will delete the HTML elements, removing the object from the screen.
An EdgeObject is the representation of the Line SVG element, which goes from point (x1,y1) to point (x2,y2). The two objects which this edge is connecting determine this points. When this class is instantiated, or any of the two objects is changed, this function will be called getting the coordinates needed.
Cloning objects is very important in the way this framework was designed and this will become clearer after we study the CoreObject. Here is the cloning function for this object:
this.cloneProperties = function (prop) {
this.propObj = clone(prop);
this.calculatePath();
}
Shapes that possesses edges will call this function when cloning their edges. This will return a deep copy of this object properties, without the memory references.
In development. This guide will be updated as this new functionality is finished.
CoreObject.js is the most important file of this framework. It works as an interface between all the other classes and your JavaScript file containing the data structure's algorithm. In this sections we will study all Core methods and understand when their use.
This class shall be instantiated in any algorithm javascipt file without any parameters, like this:
var coreObj = new CoreObject();
With this instance you'll be able use of this class to create new shapes, manage and animate them.
All fields, except for objectList are for private use. Let's understand each of them:
this.objectList = [];
This array will store instances of all existing objects.
this.stateList = [];
This array will store all states used to create a step-by-step animation.
this.stateCount = 0; this.stateAnimation = 0;
stateCount stores how many states exists while stateAnimation store which step is currently on the screen.
this.animationStatus = ANIMATION_STATUS.STOP;
This variable is used to control the playing status of the animation.
this.variableWatchList = []; this.logList = [];
Those two arrays are used to manage the content of the log panel and the variable watch panel.
Summary:
this.isLearningMode = function () {...}
this.newActionEnabled = function () {...}
this.displayAlert = function (message) {...}
In development. This guide will be updated as this new functionality is finished.
In development. This guide will be updated as this new functionality is finished.
This function is used to display an alert message for the user. There are three types of alerts: Positive, which displays the message in a green box; Negative, which displays the message in a red box; Information, which displays the message in a blue box. It can be called like this:
coreObj.displayAlert(ALERT_TYPES.NEGATIVE, "Please finish or cancel the current action before making any further changes.");
Which will result in:
To have a step-by-step animation of the algorithms we make use of "states". Just like in state machines, we save the current state of each existing object in a given time, creating a series of snapshots so we can iterate through them later.
When a visualisation page is called the state list should be initialised, like this:
var StackArray = function(){
var self = this;
var coreObj = new CoreObject();
coreObj.newStateList();
...
}
This will initialise all variables used for controlling the states.
Then it's time to begin saving states, but first let's understand what composes a state:
var state = {
data : null,
log : null,
variables : null,
algorithmLine : null
};
Now let's see how the states should be saved in order to make an animation. To do this we'll use the enqueue method, from a Queue Array Implementation:
this.enqueue = function (item) {
if (tail.value >= cap || item.trim() == "") {
coreObj.displayAlert("The input should not be empty.");
return false;
}
this.generateAlgorithm(ENQUEUE);
mArray[tail.value].setText(item);
coreObj.saveState("Inserting the new value", 0);
tail.value++;
tail.drawing.setFill(defaultProperties["shape"]["fill"]["update"]);
coreObj.saveState();
tail.edge.setIdObjectB(mArray[tail.value].getID());
tail.drawing.setText(tail.value);
tail.drawing.setFill(defaultProperties["shape"]["fill"]["default"]);
coreObj.saveVariableToWatch("head", head.value);
coreObj.saveVariableToWatch("tail", tail.value);
coreObj.saveState("Update the tail pointer.", 1);
coreObj.begin();
}
To save a state only two things are required, both being optional: a message that will be printed on the log panel, and which algorithm line that will be highlighted. Variables should be saved separately (as seen in lines 20 and 21), this allows the programmer to save as many variables as he want.
Here's a summary of the animation methods, with a brief description of what they do:
this.newStateList = function () {...} // Erase all data from the current state list
this.saveState = function (logMessage, algorithmLine) {...} // Save a new state to the list
After you've saved all desired states is time to play with them. In order to do this there's only one function that you have to call:
coreObj.begin();
This function will begin playing the states related to the last action performed.
Functions such as play, pause, next, previous are called straight from the HTML file, to iterate through the steps of the animation, and they all will call draw, sending the a state to be displayed on the screen.
All this functions take into consideration the user speed preference, which is accessible with the getAnimationDuration function.
Here's the list of the mentioned functions:
this.getAnimationDuration = function () {...} // Get the user preference for the animation duration
this.draw = function (currentState, duration) {...} // Draw one state from stateList on the screen, applying all changes made
this.pause = function () {...} // Pause the animation
this.next = function (duration) {...} // Call the next step of the animation
this.previous = function () {...} // Call the previous step of the animation
this.play = function (duration) {...} // Play the animation
Here's the list of all the core functions that interact with the panels:
this.clearLog = function () {...}
this.saveLogMessageToList = function (message) {...}
this.printLog = function (logObj) {...}
this.clearVariableWatch = function(){...}
this.saveVariableToWatch = function (variableName, variableValue) {...}
this.printVariableWatch = function (variablesObj) {...}
this.clearAlgorithm = function () {...}
this.addAlgorithmLine = function (id, instruction) {...}
this.highlightAlgorithm = function (lineNumber) {...}
But when creating your own visualisation you'll only have to call 3 of them:
All other methods are called internally in the core, so you don't have to worry.
All basic items that we've studied before must be created or deleted using the active instance of CoreObject, which will return the just created instance reference to the algorithm JavaScript file. This is necessary so the core can manage and organise all existing objects, making the framework more consistent, otherwise changes made to the object graphics wouldn't appear on the screen. With the reference that is returned you will be able to interact with each instance, applying any desired graphic changes.
Here's how these functions look like:
/**
* Create a square graphic element.
*
* @param {!(String|Number)} id : the id of this object.
* @param {?Number} x : the x coordinate of this object inside the svg element.
* @param {?Number} y : the y coordinate of this object inside the svg element.
* @param {?String=} text : the inner text of this object, that will be displayed on the screen.
* @param {?String=} label : the text underneath this object, that will be displayed on the screen.
* @param {?String=} shapeClass : the CSS class of the shape svg element.
* @param {?String=} textClass : the CSS class of the text svg element (inside the shape).
* @param {?String=} labelClass : the CSS class of the text svg element (underneath the shape).
*
* @return {SquareObject} : the new object.
*/
this.newSquareObject = function (id, x, y, text, label, shapeClass, textClass, labelClass) {
if (this.objectList[id] != null) {
throw new Error("This id is already in use by another object.");
return;
}
this.objectList[id] = new SquareObject(this, id, x, y, text, label, shapeClass, textClass, labelClass);
return this.objectList[id];
}
/**
* Create a circle graphic element.
*
* @param {!(String|Number)} id : the id of this object.
* @param {!Number} cx : the cx coordinate of this object inside the svg element.
* @param {!Number} cy : the cy coordinate of this object inside the svg element.
* @param {!Number} radius : the radius of this object.
* @param {?String=} text : the inner text of this object, that will be displayed on the screen.
* @param {?String=} label : the text underneath this object, that will be displayed on the screen.
* @param {?String=} shapeClass : the CSS class of the rect svg element.
* @param {?String=} textClass : the CSS class of the text svg element (inside the shape).
* @param {?String=} labelClass : the CSS class of the text svg element (underneath the shape).
*
* @return {CircleObject} : the new object.
*/
this.newCircleObject = function (id, cx, cy, radius, text, label, shapeClass, textClass, labelClass) {
if (this.objectList[id] != null) {
throw new Error("This id is already in use by another object.");
return;
}
this.objectList[id] = new CircleObject(this, id, cx, cy, radius, text, label, shapeClass, textClass, labelClass);
return this.objectList[id];
}
/**
* Create an edge graphic element, that will be stored in the origin object edgelist[].
*
* @param {!(String|Number)} id : the id of this object.
* @param {!String} idObjectA : the id of the origin object.
* @param {?String=} idObjectB : the id of the destination object. If null a small edge will be created following the orientation of the origin point.
* @param {?String=} edgeClass : the CSS class of the line svg element.
* @param {!Const} edgeType : a constant value (defined at 'animation/constant.js' : EDGE_TYPE) indicating whether the vertex is unidirectional (from A -> B), bidirectional or has no direction.
* @param {?Const=} outboundPoint : a constant value (defined at 'animation/constant.js' : EDGE_POSITION) indicating from which point of the shape the edge will originate. If null the CENTER position will be used.
* @param {?Const=} inboundPoint : a constant value (defined at 'animation/constant.js' : EDGE_POSITION) indicating at which point of the shape the edge will arrive. If null the CENTER position will be used.
*
* @return {EdgeObject} : the new object.
*/
this.newEdgeObject = function (id, idObjectA, idObjectB, edgeClass, edgeType, outboundPoint, inboundPoint) {
var newEdge = new EdgeObject(this, id, idObjectA, idObjectB, edgeClass, edgeType, outboundPoint, inboundPoint);
this.objectList[idObjectA].addEdge(newEdge);
return newEdge;
}
To better understand this let's look at an example:
this.push = function(item) {
...
top = new Node();
top.item = item;
top.next = oldtop;
top.drawing = coreObj.newSquareObject(++counterID, 150, 200, item, null, "node", null, null);
top.edge = coreObj.newEdgeObject(counterID, top.drawing.getID(), null, null, EDGE_TYPE.UNIDIRECTIONAL, EDGE_POSITION.BOTTOM, EDGE_POSITION.TOP);
coreObj.saveState("Inserting new node.", 0);
...
}
For deletions we have those two functions:
/**
* Set a flag for the object to be removed on the next draw action.
*
* @param {!(String|Number)} id : the id of the item to be removed.
*/
this.removeShape = function (id) {
this.objectList[id].setToRemove(true);
}
/**
* Set a flag for all objects of the selected class to be removed on the next draw action.
*
* @param {String} selectedClass : the class of the items to be removed.
* @param {Number} duration : the duration of the animation.
*/
this.removeAll = function (selectedClass, duration) {
for (var key in this.objectList) {
if (this.objectList[key].getShapeClass() == selectedClass) {
this.objectList[key].setToRemove(true);
}
}
}
The first one is used to delete a single item, while the second will delete all items that match the provided class (for example deleting all nodes when clearing a linked list visualisation).
There are some methods that are called from the basic shapes to interact with each other, but mediated by the core. When creating a new visualisation you don't have to use any of these functions but let's take a look to understand what each of them do:
this.updateEdgeList = function (oldObjID, newObjID, edgeID) {...} // Edges are stored in this.edgeList of their origin object. If the origin has changed, the lists have to be updated.
Summary:
this.clearCustomClasses = function () {...}
this.repositionDAG = function (obj, x, y, orientation) {...}
this.createGroups = function () {...}
this.createMarkers = function () {...}
During your animation you may change the shape classes to better illustrate your algorithm. With clearCustomClasses you can reset their shape back to their original appearance at once.
repositionDAG is used to begin repositioning all the shapes on the screen. The root or first node of a data structure should be sent as parameter, as the initial coordinates and orientation.
The last two methods are called from constructor. They will create all necessary SVG groups and other elements, used to better organise the HTML DOM.
To improve code abstraction all default values and constants are defined in one single file. This make doing changes to the code a lot easier as changing the values here will affect all the framework. Normally, a parameter responsible for the aspect of an item will receive a value defined in this file.
Let's check some important items declared in this file:
Heres the list containing all of them:
/**
* Values used by CoreAnimObject, to control the playing status of the animation.
*
* @const
*/
const ANIMATION_STATUS = {
PAUSE: 0,
PLAY: 1,
STOP: -1
};
/**
* Values used when creating an instance of EdgeObject.
*
* @const
*/
const EDGE_TYPE = {
UNDIRECTED: 0,
UNIDIRECTIONAL: 1,
BIDIRECTIONAL: 2
};
/**
* Values used when creating an instance of EdgeObject.
*
* @const
*/
const EDGE_POSITION = {
CENTER: 0,
TOP: 1,
LEFT: 2,
BOTTOM: 3,
RIGHT: 4
};
/**
* Values used when repositioning objects on the screen.
*
* @const
*/
const ORIENTATION = {
TOP: 1,
LEFT: 2,
BOTTOM: 3,
RIGHT: 4
}
/**
* Types of alerts that can be displayed. Positive is green, Negative is red, information is blue.
*
* @const
*/
const ALERT_TYPES = {
POSITIVE: "success",
NEGATIVE: "danger",
INFORMATION: "info"
}
Heres the list containing all of them:
/**
* Default class names used across the framework, defined here to improve code abstraction.
*
* @const
*/
const DEFAULT_CLASSES = {
SHAPE:"shape",
EDGE:"edge",
TEXT:{
INNER:"innerText",
LABEL:"labelText"
},
MARKER:"marker",
LEARNING_MODE:{
SHAPE:"learning",
ACTIVE:"active",
PLACE_HOLDER:"placeHolder",
OBJECT_SELECTED:"selected"
},
PAGE:{
ALGORITHM:{
HIGHLIGHT:"codeHighlight"
}
}
}
/**
* Default IDs used across the framework, defined here to improve code abstraction.
*
* @const
*/
const DEFAULT_IDS = {
PAGE:{
LOG:"log",
VARIABLE:"variables",
ALGORITHM:"algorithm",
LEARNING_MODE:"chk-learn",
ANIMATION_DURATION:"animation-duration",
ALERT_PLACEHOLDER:"alert_placeholder"
},
HTML_ELEMENT:{
ALGORITHM_LINE:"line"
},
SVG_GROUP:{
MAIN:"g-main",
MARKER:"g-marker",
SHAPE:"g-shape",
TEXT:"g-text",
LABEL:"g-label",
EDGE:"g-edge"
},
SVG_ELEMENT:{
SHAPE:"shape-",
USER_SHAPE:"u-shape-",
TEXT:"text-",
USER_TEXT:"u-text-",
LABEL:"label-",
EDGE:"edge-",
USER_NEW_OBJ:"protoObj"
},
SVG_MARKER:{
START:{
DEFAULT:"reverseArrowDefault",
NULL:"reverseArrowNull"
},
END:{
DEFAULT:"arrowDefault",
NULL:"arrowNull"
}
}
}
/**
* Default properties used across the framework, defined here to improve code abstraction.
*
* @const
*/
const defaultProperties = {
shape:{
"radius":25,
"width":50,
"height":50,
"stroke":{
"default":"black",
"draggable":"tomato"
},
"stroke-width":{
"default":2,
"draggable":2
},
"fill":{
"default":"white",
"draggable":"grey",
"update":"lightskyblue",
"delete":"tomato"
},
"fill-opacity":{
"default":1.0,
"draggable":0.2
}
},
text:{
"font-family":"sans-serif",
"font-size":18,
"text-anchor":"middle",
"stroke":{
"default":"black",
"innerTextBlack":"black",
"innerTextWhite":"ivory"
}
},
edge:{
"stroke":{
"default":"black",
"null":"tomato"
},
"stroke-width":{
"default":3,
"null":4
}
},
marker:{
"width":5,
"height":3,
"refX":{
"start":-7,
"end":7
},
"start":{
"default": "url(#reverseArrowDefault)",
"null": "url(#reverseArrowNull)"
},
"end":{
"default": "url(#arrowDefault)",
"null": "url(#arrowNull)"
}
}
}
Now that you know all the items of the framework it's time to create your own Data Structure Visualisation.
At first, you'll learn how to create a visualisation in Exploration Mode, which means users can interact with the data structure using its methods and the step-by-step animations are going to happen in a pre-defined order.
In a second moment, you'll learn how to develop a Learning Mode for your visualisations. In this mode, the user will be prompted to schedule the changes to the graphics, being able to check the correction of the actions later on.
Before we begin we would like to recommend a developing tool: Brackets.
As described in its own website is an open source code editor for web designers and front-end developers. We've used it for the entire development of this tool and found it to be very productive. A few points that we would like to mention:
See the contents of a function and make changes on the go.
Preview of images inside the code.
See the output style while coding.
In most cases you will only have to create two files: an HTML and a JavaScript. This will only be different if you decide to create a custom shape or set of shapes, which will take another JavaScript file.
As seen at the beginning of this guide, the html is at the top of our architecture.
To create the HTML file of your data structure visualisation the first thing you have to do is to copy the template.html provided and rename it to fit the data structure. In you'll find the following lines:
<!-- Template: Page Title --> (line 7)
<!-- Template: Visualisation Title --> (line 37)
<!-- Template: Method's buttons --> (line 42)
<!-- Template: Text input button (methods that require an input value) --> (line 42)
<!-- Template: Textbox id, for future reference --> (line 52)
<!-- Template: onclick function --> (line 55)
<!-- Template: Normal button (methods that don't require an input value) --> (line 64)
<!-- Template: Learning Mode Buttons (If not available comment this div tag) --> (line 69)
<!-- Template: Your Visualisation Scripts --> (line 219)
// Template: Your js file instance and calls to methods. (line 223)
Below each of them you'll find the code that needs to be adapted for the new page.
For the methods in our visualisations we only use two kinds of buttons: with (a) and without (b) text input.
If a method requires an input, a text box (c) will appear, and the method will be executed after pressing the button. If no input is required the method will be executed immediately.
Below you'll find the html code for each type of button:
<!-- Template: Text input button (methods that require an input value) -->
<div class="btn-group">
<button type="button" class="btn btn-default navbar-btn dropdown-toggle" data-toggle="dropdown">
Method Name <span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">
<form class="navbar-form prevent-submit">
<div class="input-group">
<!-- Template: Textbox id, for future reference -->
<input id="txt-input" type="text" class="form-control" maxlength="4">
<span class="input-group-btn">
<!-- Template: onclick function -->
<button class="btn btn-default" type="submit" onclick="">
<span class="glyphicon glyphicon-ok">
</button>
</span>
</div>
</form>
</ul>
</div>
<!-- Template: Normal button (methods that don't require an input value) --> <button type="button" class="btn btn-default navbar-btn" onclick="">Method Name
There is also the Learning Mode button group that in most cases will be commented, unless you're also going to develop a learning mode for the current visualisation.
Here is how its code looks like:
<!-- Template: Learning Mode Buttons (If not available comment this div tag) -->
<!--
<div id="div-learning-buttons" class="btn-group popover-dismiss" data-popover="popover" data-html="true" data-placement="bottom" title="Learning Mode" data-content="Use this switch to toggle between the <b>Exploration</b> and the <b>Learning</b> modes.<br>In Exploration mode animations will happen automatically.<br>In Learning mode you will have to schedule the changes of each method.">
<button id="chk-answer-btn" type="button" disabled class="btn btn-default navbar-btn" data-toggle="tooltip" data-placement="bottom" title="Check Answer" onclick="">
<span class="glyphicon glyphicon-check"></span>
</button>
<button id="restart-btn" type="button" disabled class="btn btn-default navbar-btn" data-toggle="tooltip" data-placement="bottom" title="Restart" onclick="">
<span class="glyphicon glyphicon-repeat"></span>
</button>
<button id="cancel-btn" type="button" disabled class="btn btn-default navbar-btn" data-toggle="tooltip" data-placement="bottom" title="Cancel" onclick="">
<span class="glyphicon glyphicon-remove"></span>
</button>
<button type="button" id="chk-learn" class="btn btn-default navbar-btn" data-toggle="tooltip" data-placement="bottom" title="Toggle Mode">Learning Mode</button>
</div>
-->
To make it visible just remove the comment tags (<!-- -->).
The last thing to be changed in the html file is the scripts. You will have to make reference to the new file and create calls to your methods. For this example we're going to use the StackArray.html file.
<!-- Template: Your Visualisation Scripts -->
<script src="js/algorithms/stackArray.js"></script>
<script type="text/JavaScript">
var stack = new StackArray();
var core = stack.getCore();
function push () {
var input = $("#txt-input");
stack.push(input.val());
input.val("");
}
function pop () {
stack.pop();
}
function empty () {
stack.init();
}
// DEFAULT METHODS. DO NOT REMOVE.
function previous () {
core.previous();
}
function play () {
core.play();
}
function pause () {
core.pause();
}
function next () {
core.next();
}
</script>
As you can see in the visualisation above we've referenced our algorithm file in line 2. Then we'll have to create a new instance of our class (line 5). An important method that every algorithm js file should have is this:
This method is called in line 6, and will allow us to call the default media control methods (from line 23 until the end of the script).
Other than that you should create calls for the methods in your class, sending inputs if required, making use of the ids defined on the creation of the html buttons (line 9).
In our architecture, the JavaScript file is right under the HTML and interacting directly with the core.
To create the JS file of your data structure visualisation we recommend that you use the template.js file provided as it follows some guidelines of our framework. Here's how it looks like:
/**
* Defines a Pointer object, that contains:
* {Number} value
* {Object} drawing : an instance of one of the basic shapes (squareObject, nodeObject, etc)
* {Object} edge : an instance of the edgeObject
*/
var Pointer = function () {
var value;
var drawing;
var edge;
}
/**
* Defines a Stack object (Array implementation). Used to keep track of the object internally and to interact with the animations.
*/
var Template = function(){
var self = this;
var coreObj = new CoreObject();
// CONSTANTS FOR ALGORITHM TRACK GENERATION
const PUSH = 0,
POP = 1;
// CREATE INITIAL ITEMS IF ANY
coreObj.newStateList();
var cap = 16;
var top = new Pointer();
var mArray = [];
for (var i=0; i<16; i++){
mArray[i] = coreObj.newSquareObject(i, (i+1)*50, 300, null, i, null, null, null);
}
top.value = 0;
top.drawing = coreObj.newSquareObject("top", 50, 50, 0, "top", null, null, null);
top.edge = coreObj.newEdgeObject("top", top.drawing.getID(), mArray[top.value].getID(), null, EDGE_TYPE.UNIDIRECTIONAL, EDGE_POSITION.BOTTOM, EDGE_POSITION.TOP);
coreObj.saveState();
coreObj.begin(0);
// DEFAULT METHODS
this.getCore = function () {
return coreObj;
}
this.generateAlgorithm = function (command) {
coreObj.clearAlgorithm();
switch (command) {
case PUSH:
coreObj.addAlgorithmLine(lineNo, "Instruction");
break;
case POP:
coreObj.addAlgorithmLine(lineNo, "Instruction");
break;
}
}
// PARTICULAR METHODS
this.init = function() {...}
this.isEmpty = function () {...}
this.push = function (item) {...}
this.pop = function () {...}
}
Now let's understand what's happening in there.
As we've seen when studying the factories in CoreObject each graphic object created is returned to this class for you to manage. This will give you access to each instance, making it easier to do necessary graphic changes.
Let's pick a few examples to have an idea of how to manage all this:
for (var i=0; i<16; i++){
mArray[i] = coreObj.newSquareObject(i, (i+1)*50, 300, null, i, null, null, null);
}
Since you have the instance of your graphic object, to access its properties all you have to do is:
mArray[top.value].getText();
mArray[top.value].setText(item);
Just like in other OO languages, using "." will grant access to the class methods.
Normally this is the content of a Node:
var Node = function () {
var value;
var key;
var leftChild;
var rightChild;
}
In these variables we're going to store the data referring to this Node, and the instances of its children. But we also need to keep track of the visual structures, so let's see what will change:
var Node = function () {
var value;
var key;
var leftChild;
var rightChild;
var drawing;
var leftEdge;
var rightEdge;
}
We still have all the data variables, but now we also have variables to store instances of the graphic objects, that just like in the array example will enable us to make changes to the properties as we run our algorithms.
To make it even more clearer, let's take a look at this snipt:
this.root = new Node(); root.value = newValue; root.key = newKey; root.leftChild = null; root.rightChild = null; root.drawing = coreObj.newCircleObject(newKey, this.getCX(), this.getCY(), defaultProperties.radius, newValue, null, null, null, null); root.leftEdge = coreObj.newEdgeObject(newKey + "l", root.drawing.getID(), null, null, EDGE_TYPE.UNIDIRECTIONAL, EDGE_POSITION.CENTER, EDGE_POSITION.TOP); root.rightEdge = coreObj.newEdgeObject(newKey + "r", root.drawing.getID(), null, null, EDGE_TYPE.UNIDIRECTIONAL, EDGE_POSITION.CENTER, EDGE_POSITION.TOP);
In it we're creating a new Node and making it root of our tree. Now let's see what would happen if we would insert a new Node on the left subtree:
var newNode = new Node(); newNode.value = newValue; newNode.key = newKey; newNode.leftChild = null; newNode.rightChild = null; newNode.drawing = coreObj.newCircleObject(newKey, this.getCX(), this.getCY(), defaultProperties.radius, newValue, null, null, null, null); newNode.leftEdge = coreObj.newEdgeObject(newKey + "l", root.drawing.getID(), null, null, EDGE_TYPE.UNIDIRECTIONAL, EDGE_POSITION.CENTER, EDGE_POSITION.TOP); newNode.rightEdge = coreObj.newEdgeObject(newKey + "r", root.drawing.getID(), null, null, EDGE_TYPE.UNIDIRECTIONAL, EDGE_POSITION.CENTER, EDGE_POSITION.TOP); this.root.leftChild = newNode; this.root.leftEdge.setIdObjectB(newNode.drawing.getID());
We create a new node just as we did before, then we update the data (line 12) and then the graphics (line 13).
By doing this we can keep the data and the graphics apart from each other, but still easy to manage and access then. In the tree example, you would be able to use recursion to iterate through the tree by having pointers to the children (leftChild and rightChild) and while doing that you can change the edge properties to let the user know what's happening, by having an easy access to leftEdge and rightEdge.
There are only four default variables that have to be declared:
In certain visualisations some initial graphic elements may be necessary. For example, in an Array implementation you'll have to display the array itself.
To achieve this you'll have to create all these items in here as this lines of code will execute as this class is instantiated and the graphics will be displayed as soon as the page load.
The two important default methods are:
Any method related to this data structure, that will be called from the HTML file.
Here's some important details that you should pay attention to:
// Example from StackLinkedList.js
this.pop = function() {
// IMPORTANT: Any consistency verification must be done in this layer, before making changes to the graphics or data structure
if (this.isEmpty()) {
return false;
coreObj.displayAlert(ALERT_TYPES.NEGATIVE, "The stack is already empty.");
}
this.generateAlgorithm(POP);
// IMPORTANT: Always create a new state list for the current animation
coreObj.newStateList();
...
// IMPORTANT: After making all necessary changes to the data structure and graphics call begin to initilise the animation
coreObj.begin();
}
The basic classes provided in this framework are meant to cover all necessary items to implement a data structure visualisation, but there is still room for customisation.
Custom sets can be useful when creating initial shapes. If there's a lot of visualisations that will use an array why not create a custom set just for that? Let's see how the code of a custom set would look:
/**
* Defines a Pointer object, that contains:
* {Number} value
* {Object} drawing : an instance of one of the basic shapes (squareObject, nodeObject, etc)
* {Object} edge : an instance of the edgeObject
*/
var Node = function () {
var item,
pointer,
drawing,
edge;
}
/**
* Defines an Array of Squares with pointers.
*
* @param {!CoreAnimObject} coreObj : instance of the CoreAnimObject class.
* @param {!Number} positions : number of positions of the array.
*/
var ArrayPointer = function (coreObj, positions) {
var self = this;
this.coreObj = coreObj;
var mArray = [];
for (var i=0; i<positions; i++){
mArray[i] = new Node();
mArray[i].item = null;
mArray[i].drawing = coreObj.newSquareObject("a" + i, (i+1)*50, 400, null, i, null, null, null);
mArray[i].edge = coreObj.newEdgeObject("a" + i, mArray[i].drawing.getID(), null, null, EDGE_TYPE.UNIDIRECTIONAL, EDGE_POSITION.TOP, EDGE_POSITION.BOTTOM);
}
return mArray;
}
In this example we're creating an array of Squares with pointers, and for this we'll make use of an auxiliary structure (Node, line 7). Than we'll make use of the instance of CoreObject passed as parameter to create all objects that we'll need.
The main difference between creating a custom shape and a custom set is that in the second case you'll send back all the shapes created for who called this function to manage, so you don't have to worry about creating the methods again.
Let's see an example of this:
var Hashtable = function () {
var self = this;
var coreObj = new CoreObject();
coreObj.newStateList();
coreObj.saveState();
var mArray;
mArray = new ArrayPointer(coreObj, 15);
coreObj.saveState();
coreObj.play();
}
Which will result in:
That makes the processes of creating initial shapes smoother and the code clearer.
In development. This guide will be updated as this new functionality is finished.