├── Javascript Engineer Take Home Challenge (2).pdf ├── index.html ├── libraries └── p5.min.js ├── photos ├── balanced.png ├── blank.png ├── fill.png ├── manynodes.png ├── search.png └── singlenode.png └── src ├── Controls.js ├── Explorer.js ├── Node.js ├── Tree.js ├── sketch.js └── style.css /Javascript Engineer Take Home Challenge (2).pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techsolutioncat/Javascript-Binary-Tree/a8c380f5ed2b85fe110b1874d382cec0eac92271/Javascript Engineer Take Home Challenge (2).pdf -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | Binary Tree Visualization 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 | 23 | 26 | 27 | Animation Speed: 28 | 29 | 30 |
31 | 32 | 33 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /photos/balanced.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techsolutioncat/Javascript-Binary-Tree/a8c380f5ed2b85fe110b1874d382cec0eac92271/photos/balanced.png -------------------------------------------------------------------------------- /photos/blank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techsolutioncat/Javascript-Binary-Tree/a8c380f5ed2b85fe110b1874d382cec0eac92271/photos/blank.png -------------------------------------------------------------------------------- /photos/fill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techsolutioncat/Javascript-Binary-Tree/a8c380f5ed2b85fe110b1874d382cec0eac92271/photos/fill.png -------------------------------------------------------------------------------- /photos/manynodes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techsolutioncat/Javascript-Binary-Tree/a8c380f5ed2b85fe110b1874d382cec0eac92271/photos/manynodes.png -------------------------------------------------------------------------------- /photos/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techsolutioncat/Javascript-Binary-Tree/a8c380f5ed2b85fe110b1874d382cec0eac92271/photos/search.png -------------------------------------------------------------------------------- /photos/singlenode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techsolutioncat/Javascript-Binary-Tree/a8c380f5ed2b85fe110b1874d382cec0eac92271/photos/singlenode.png -------------------------------------------------------------------------------- /src/Controls.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Controls.js 3 | * 4 | * Thie file defines the Controls class, which provides a friendly button-based 5 | * interface to the Tree class's animation methods 6 | * 7 | * The Tree class relies on the Controls "animationInterval" property to set 8 | * the time in between animation frames 9 | ***/ 10 | class Controls { 11 | // Using the static keyword to define readable class constants 12 | 13 | // Constants for the ids of various user interface elements 14 | static CLEARID = "clear-btn"; 15 | static QUICKFILLID = "quick-fill-btn"; 16 | static SLOWFILLID = "slow-fill-btn"; 17 | static ADDID = "add-btn"; 18 | static SEARCHID = "search-btn"; 19 | static SLIDERID = "speed-slider"; 20 | static BINARY = "canvas-placeholder"; 21 | 22 | // The number of nodes above which the user will be warned before running 23 | // "Fill" or "Quick Fill" 24 | static NODELIMIT = 500; 25 | 26 | constructor(tree) { 27 | this.tree = tree; 28 | this.tree.bindControls(this); // Provide the tree a reference to this class 29 | 30 | this.animationInterval = null; // Property Tree class relies on for animation 31 | 32 | // Store all of the user interface elements based on the IDs 33 | this.clearBtn = document.getElementById(Controls.CLEARID); 34 | // this.quickFillBtn = document.getElementById(Controls.QUICKFILLID); 35 | // this.slowFillBtn = document.getElementById(Controls.SLOWFILLID); 36 | //this.addBtn = document.getElementById(Controls.ADDID); 37 | this.searchBtn = document.getElementById(Controls.SEARCHID); 38 | this.speedSlider = document.getElementById(Controls.SLIDERID); 39 | this.node = document.getElementById(Controls.BINARY); 40 | 41 | // Set the animation interval based on the slider's value 42 | this.setAnimationSpeed(); 43 | 44 | // Append event listeners to run each animation 45 | this.clearBtn.addEventListener('click', 46 | () => this.triggerAnimation(this.clear)); 47 | // this.quickFillBtn.addEventListener('click', 48 | // () => this.triggerAnimation(this.quickFill)); 49 | // this.slowFillBtn.addEventListener('click', 50 | // () => this.triggerAnimation(this.slowFill)); 51 | document.addEventListener('keyup', event => { 52 | if (event.code === 'Space') {//vitaly 53 | //Controls.triggerAnimation(Controls.add); 54 | var value = Math.floor(random(-100, 100)); 55 | 56 | if(value !== null && this.tree.search(value)) { 57 | alert(value + ' is already in the tree'); 58 | } else if(value !== null){ 59 | this.tree.addValueVisual(value); 60 | } 61 | } 62 | }) 63 | //this.addBtn.addEventListener('click', 64 | //() => this.triggerAnimation(this.add)); 65 | 66 | this.node.addEventListener('click', function(e) { 67 | console.log(e); 68 | }); 69 | 70 | this.searchBtn.addEventListener('click', 71 | () => this.triggerAnimation(this.search)); 72 | 73 | // Append an event listener to change the animation interval 74 | this.speedSlider.addEventListener('input', this.setAnimationSpeed.bind(this)); 75 | } 76 | 77 | // Completly resets the tree, removing all nodes, stopping all animations 78 | clear() { 79 | this.tree.clear(); 80 | this.tree.stopAnimation(() => {}) 81 | this.tree.draw(); 82 | } 83 | 84 | // Called by event listeners to run a certain animation if one is not running 85 | triggerAnimation(animation) { 86 | if(this.tree.running) { 87 | alert('Please wait for the current animation to finish'); 88 | } else { 89 | // Bind the animation here so it doesn't have to be bound as an argument 90 | animation.bind(this)(); 91 | } 92 | } 93 | 94 | // Prompts the user with a given piece of text 95 | // Returns: null => if no valid number was entered 96 | // postive integer => if a valid positive integer is provided 97 | getNumber(text) { 98 | var value = prompt(text); 99 | 100 | if(value === null) { 101 | return null; 102 | } else if(isNaN(parseInt(value)) || value === "" || parseInt(value) < 0) { 103 | alert('Please enter a positive integer'); 104 | return null; 105 | } else { 106 | return parseInt(value); 107 | } 108 | } 109 | 110 | // Method for the Quick Fill animation 111 | quickFill() { 112 | var count = this.getNumber("Number of nodes: "); 113 | 114 | if(count !== null && (count < Controls.NODELIMIT || 115 | confirm(count + ' nodes may reduce performance. Continue anyways?'))) { 116 | this.tree.fill(count); 117 | } 118 | } 119 | 120 | // Method for the Fill animation 121 | slowFill() { 122 | var count = this.getNumber("Number of nodes: "); 123 | 124 | if(count !== null && (count < Controls.NODELIMIT || 125 | confirm(count + ' nodes may reduce performance. Continue anyways?'))) { 126 | this.tree.fillVisual(count); 127 | } 128 | } 129 | 130 | // Method for the Add animation 131 | add() { 132 | //var value = this.getNumber("Value to add: "); 133 | 134 | var value = Math.floor(random(-100, 100)); 135 | 136 | if(value !== null && this.tree.search(value)) { 137 | alert(value + ' is already in the tree'); 138 | } else if(value !== null){ 139 | this.tree.addValueVisual(value); 140 | } 141 | } 142 | 143 | // Method for the search animation 144 | search() { 145 | var value = this.getNumber("Value to search for: "); 146 | 147 | if(value !== null) { 148 | this.tree.searchVisual(value) 149 | } 150 | } 151 | 152 | // Inverts and exponentiates the linear output of the slider to set the interval 153 | setAnimationSpeed() { 154 | this.animationInterval= 1000/Math.pow(10, this.speedSlider.value); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/Explorer.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Explorer.js 3 | * 4 | * This file defines the Explorer class, which transforms the graphics buffer 5 | * that the Tree class uses to draw itself to allow for users to pan and zoom 6 | * as they view the binary tree. 7 | * 8 | * The Explorer uses the translate and scale functions without resetting the 9 | * transformation at the end of the frame. This allows pieces of the picture to 10 | * be updated without redrawing the entire picture, which can improve 11 | * performance. The Explorer currently must redraw the picture whenever the user 12 | * pans or zooms. Ideally, the explorer could simply change the size and 13 | * position of the graphics, but the way the canvas works, the picture would 14 | * be blurry when zoomed in. It may be possible to achieve this behavoir with 15 | * svg graphics instead, but that's a project for the future. 16 | * 17 | * The Explorer class relies on a graphics buffer to transform, a draw function 18 | * to redraw the picture in the graphics buffer, and a DOM object for the canvas 19 | * to register event listeners for panning and zooming 20 | ***/ 21 | 22 | class Explorer { 23 | static ZOOMFACTOR = 1.1; // The factor by which zooming occurs 24 | 25 | constructor(canvas, graphicsBuffer, drawFunction, zoomFactor = Explorer.ZOOMFACTOR) { 26 | // Canvas that clicks and mouse wheel events are detected on 27 | this.canvas = canvas; 28 | 29 | // The buffer that should be modified for panning and zooming 30 | this.graphicsBuffer = graphicsBuffer; 31 | 32 | // Function that will redraw the picture in the graphics buffer 33 | this.drawFunction = drawFunction; 34 | 35 | // The total transformations that have been applied to the graphics buffer 36 | this.offsetX = 0; 37 | this.offsetY = 0; 38 | this.zoom = 1; 39 | 40 | // The factor by which zooming occurs 41 | this.zoomFactor = zoomFactor; 42 | 43 | // Reset the graphics buffer (so it can be poppe to reset it later) 44 | this.graphicsBuffer.push(); 45 | 46 | // Register event listeners for zooming and panning 47 | canvas.addEventListener("wheel", this.wheel.bind(this)); 48 | canvas.addEventListener("mousemove", this.mouseMove.bind(this)); 49 | } 50 | 51 | // Returns the graphics buffer to its "Home" view. Not used right now 52 | reset() { 53 | this.offsetX = 0; 54 | this.offsetY = 0; 55 | this.zoom = 1; 56 | 57 | this.graphicsBuffer.pop(); 58 | this.graphicsBuffer.push(); 59 | 60 | this.drawFunction(); 61 | } 62 | 63 | // Event listener for panning. Detects when the mouse is pressed to detect 64 | // dragging, and uses the movementX/Y of the event to determine distance to pan 65 | mouseMove(event) { 66 | if(mouseIsPressed) { 67 | var deltaX = (1/this.zoom) * event.movementX; 68 | var deltaY = (1/this.zoom) * event.movementY; 69 | 70 | this.graphicsBuffer.translate(deltaX, deltaY); 71 | 72 | this.offsetX += deltaX; 73 | this.offsetY += deltaY; 74 | 75 | this.drawFunction(); 76 | } 77 | } 78 | 79 | // Even listener for zooming. Uses the event's deltaY to determine whether 80 | // to zoom in or zoom out 81 | wheel(event) { 82 | var deltaX = (CANVASWIDTH/2) - this.offsetX; 83 | var deltaY = (CANVASHEIGHT/2) - this.offsetY; 84 | var zoomFactor; 85 | 86 | if(event.deltaY < 0) { 87 | zoomFactor = this.zoomFactor 88 | } else if(event.deltaY > 0){ 89 | zoomFactor = 1/this.zoomFactor 90 | } else { 91 | // In case of sideways scroll on trackpad 92 | zoomFactor = 1; 93 | } 94 | 95 | this.graphicsBuffer.translate(deltaX, deltaY); 96 | this.graphicsBuffer.scale(zoomFactor); 97 | this.graphicsBuffer.translate(-deltaX, -deltaY); 98 | 99 | this.zoom *= zoomFactor; 100 | 101 | this.drawFunction(); 102 | 103 | return false; // Prevent default scrolling behavoir (maybe?) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Node.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Node.js 3 | * 4 | * This file defines the Node class. Nodes serve as the building blocks of the 5 | * binary tree. The Node has basic data structure properties that would be 6 | * required of a non-visual node object. This includes storing its value, and 7 | * storing references to its left and right children. A node is said to be 8 | * filled if it contains a value other than null. When a node is filled, it will 9 | * create two empty (not filled) nodes as it children. This allows recursive 10 | * calls to simply check if the node being processed is filled, rather than 11 | * checking that each left and right child of a node has been defined. 12 | * 13 | * Nodes also store information that is necessary to visualize them. This 14 | * includes all the properties of the appearance of the circle which represents 15 | * the node (e.g. size, color, outline), as well as properties that are 16 | * necessary to visualize the edge connecting the node to its parent. 17 | ***/ 18 | 19 | class Node { 20 | // Note: this section uses the static keyword to define static properties. 21 | // Although perhaps not the intended function, it makes defining static 22 | // properties much more readable than adding them at the end 23 | 24 | // Constants that control the default appearance of the nodes 25 | static SIZE = 20; // Diameter of the nodes 26 | static COLOR = color(255, 255, 255); // Fill color of the nodes 27 | static STROKE = color(0, 0, 0, 0); // Outline color of the nodes 28 | static TEXTSIZE = 10; // Text size of the node values 29 | static TEXTCOLOR = color(0, 0, 0); // Text color of the node values 30 | static EDGECOLOR = color(0, 0, 0); // Color of this node's upper edge 31 | static EDGETHICKNESS = 2; // Thickness this node's upper edge 32 | 33 | // Color-related constants for visualization purposes 34 | static VISITED = color(0, 0, 255); // Color when this node has been visited 35 | static SUCCESS = color(0, 255, 0); // Color when this node was added/the 36 | // value inside was being searched for 37 | static FAILURE = color(255, 0, 0); // Color when the value being searched 38 | // for is not found in this node 39 | 40 | // Constants controlling the positions of the nodes relative to one another 41 | static HORIZONTALSPACING = 15; // Horizontal distance between two nodes 42 | static VERTICALSPACING = 50; // Vertical distance between tow nodes 43 | 44 | constructor(graphicsBuffer, parent = null, size = Node.SIZE, 45 | color = Node.COLOR, stroke = Node.STROKE, 46 | textSize = Node.TEXTSIZE, textColor = Node.TEXTCOLOR, 47 | edgeColor = Node.EDGECOLOR, 48 | edgeThickness = Node.EDGETHICKNESS) { 49 | 50 | this.value = null; // The value this node is holding 51 | 52 | // Reference to left/right children on this node 53 | this.leftNode = null; 54 | this.rightNode = null; 55 | 56 | // The off-screen buffer this node should draw itself to 57 | this.graphicsBuffer = graphicsBuffer; 58 | 59 | // A reference to the parent for drawing purposes 60 | this.parent = parent; 61 | 62 | // x and y coordinates to draw the node at 63 | this.x = 0; 64 | this.y = 0; 65 | 66 | // The horizontal space between this node and its left/right children 67 | this.rightSpacing = 0; 68 | this.leftSpacing = 0; 69 | 70 | // The total horizontal space all nodes below this node use in either direction 71 | this.cumulativeRightSpacing = 0; 72 | this.cumulativeLeftSpacing = 0; 73 | 74 | // Properties controlling the appearance of the node 75 | this.size = size; 76 | this.color = color; 77 | this.stroke = stroke; 78 | this.textSize = textSize; 79 | this.textColor = textColor; 80 | 81 | // Properties controlling the appearance of the upper edge of this node 82 | this.edgeColor = edgeColor; 83 | this.edgeThickness = edgeThickness; 84 | } 85 | 86 | // Definition of a "filled" node that should be processed recursively 87 | isFilled() { 88 | return this.value !== null; 89 | } 90 | 91 | // Checks if a node has a parent, or is the root of a tree 92 | hasParent() { 93 | return this.parent !== null; 94 | } 95 | 96 | /*** 97 | * Adds the specified value to the structure at or below this node 98 | * 99 | * Returns: The highest-level node whose coordinates (and children's 100 | * coordinates) must be adjusted to account for the addition of the new node. 101 | * 102 | * Returning the highest-level that needs to be adjusted increases 103 | * performance, because the coordinates of only a subset of the tree must 104 | * be recalculated 105 | ***/ 106 | addValue(value) { 107 | if (!this.isFilled()) { 108 | // If the node hasn't been filled yet, fill this node with the value 109 | // This node needs to have its coordinates set, to return this 110 | 111 | this.value = value; 112 | this.leftNode = new Node(this.graphicsBuffer, this); 113 | this.rightNode = new Node(this.graphicsBuffer, this); 114 | 115 | return this; 116 | 117 | } else if (value < this.value) { 118 | // The value is less than this node's value, so it belongs to the left 119 | 120 | var initialLeftSpacing = this.leftNode.cumulativeRightSpacing 121 | + Node.HORIZONTALSPACING; 122 | 123 | // Add this value to the left half of the tree 124 | var shiftedNode = this.leftNode.addValue(value); 125 | 126 | // To prevent overlapping nodes, the left child should be offset 127 | // slightly farther to the left than all the space taken up to the 128 | // right of the left node 129 | this.leftSpacing = this.leftNode.cumulativeRightSpacing 130 | + Node.HORIZONTALSPACING; 131 | 132 | // Update total spacing taken up to the left of this node 133 | this.cumulativeLeftSpacing = this.leftNode.cumulativeLeftSpacing 134 | + this.leftSpacing; 135 | 136 | // If this node's left spacing changed, then the coordinates of its 137 | // left child must be updated to account for this change, so return 138 | // the left child 139 | if(this.leftSpacing !== initialLeftSpacing) { 140 | return this.leftNode; 141 | } 142 | 143 | // If the left spacing didn't change, return the lower node that 144 | // needs to be adjusted 145 | return shiftedNode; 146 | 147 | } else if(value > this.value){ 148 | // The value is greater than this node's value, so it belongs to the left 149 | 150 | // The code below parallels the code above, but handles adding nodes 151 | // to the right half of this node 152 | 153 | var rightSpacing = this.rightNode.cumulativeLeftSpacing 154 | + Node.HORIZONTALSPACING; 155 | 156 | var shiftedNode = this.rightNode.addValue(value); 157 | 158 | this.rightSpacing = this.rightNode.cumulativeLeftSpacing 159 | + Node.HORIZONTALSPACING; 160 | 161 | this.cumulativeRightSpacing = this.rightNode.cumulativeRightSpacing 162 | + this.rightSpacing; 163 | 164 | if(this.rightSpacing !== rightSpacing) { 165 | return this.rightNode; 166 | } 167 | 168 | return shiftedNode; 169 | } 170 | } 171 | 172 | // Recursively sets the coordinates of this node and all nodes below it. 173 | // If no coordinates are supplied, the coordinates are based on the parent 174 | // node's location and spacing. If coordinates are supplied, the coordinates 175 | // are se to the specified values. 176 | // This function is called by the Tree class after a value is inserted 177 | // to position the nodes in the tree correctly 178 | setCoordinates(x, y) { 179 | if(this.isFilled()) { 180 | if(typeof x === "undefined" && typeof y === "undefined") { 181 | // No coordinates were passed into the function 182 | if(this.value < this.parent.value) { 183 | // Left node 184 | this.x = this.parent.x - this.parent.leftSpacing; 185 | } else { 186 | // Right node 187 | this.x = this.parent.x + this.parent.rightSpacing; 188 | } 189 | 190 | this.y = this.parent.y + Node.VERTICALSPACING; 191 | 192 | } else { 193 | // Coordinates were passed into the function 194 | this.x = x; 195 | this.y = y; 196 | } 197 | 198 | this.leftNode.setCoordinates(); 199 | this.rightNode.setCoordinates(); 200 | } 201 | } 202 | 203 | /*** 204 | * Recursively searches the tree for the specified value 205 | * 206 | * Returns: Boolean; if value was found in the tree (true) or not (false) 207 | ***/ 208 | search(value) { 209 | if (!this.isFilled()) { 210 | return false; 211 | 212 | } else if (this.value === value) { 213 | return true; 214 | 215 | } else if (value < this.value) { 216 | return this.leftNode.search(value); 217 | 218 | } else if (value > this.value) { 219 | return this.rightNode.search(value); 220 | } 221 | } 222 | 223 | // Draws this node's upper level edge, if the node has a parent 224 | drawEdge() { 225 | if (this.hasParent()) { 226 | this.graphicsBuffer.stroke(this.edgeColor); 227 | this.graphicsBuffer.strokeWeight(this.edgeThickness); 228 | this.graphicsBuffer.line(this.x, this.y, this.parent.x, this.parent.y); 229 | } 230 | } 231 | 232 | // Draws this node's circular face 233 | drawNode() { 234 | this.graphicsBuffer.fill(this.color); 235 | this.graphicsBuffer.stroke(this.stroke); 236 | this.graphicsBuffer.ellipse(this.x, this.y, this.size, this.size); 237 | 238 | this.graphicsBuffer.noStroke(); 239 | this.graphicsBuffer.fill(this.textColor); 240 | this.graphicsBuffer.textAlign(CENTER, CENTER); 241 | this.graphicsBuffer.textSize(this.textSize); 242 | this.graphicsBuffer.text(this.value, this.x, this.y + 1); 243 | } 244 | 245 | // Recursively draws this node and all nodes below it 246 | // Note: the parent of the node originally calling this function must be 247 | // redrawn, because the edge will cover the parent node. Use redraw() 248 | // if you want to redraw a single node 249 | draw() { 250 | if(this.isFilled()) { 251 | this.leftNode.draw(); 252 | this.rightNode.draw(); 253 | 254 | this.drawEdge(); 255 | this.drawNode(); 256 | } 257 | } 258 | 259 | // Redraws a singular node on the tree with no side-effects 260 | redraw() { 261 | if(this.isFilled()) { 262 | this.drawEdge(); 263 | 264 | this.drawNode(); 265 | 266 | if(this.hasParent()) { 267 | this.parent.drawNode(); 268 | } 269 | } 270 | } 271 | 272 | // Recursively sets the color and edge color of this node and all nodes 273 | // below it to the specified color 274 | recursivePaint(color) { 275 | if(this.isFilled()) { 276 | this.color = color; 277 | this.edgeColor = color; 278 | 279 | this.leftNode.recursivePaint(color); 280 | this.rightNode.recursivePaint(color); 281 | } 282 | } 283 | 284 | // Sets the color and edge color of this node, and redraws the node 285 | paint(color) { 286 | this.color = color; 287 | this.edgeColor = color; 288 | 289 | this.redraw(); 290 | } 291 | 292 | // Recursively set the appearnace of this node and all nodes below it to 293 | // defaults for the class 294 | resetVisuals() { 295 | if(this.isFilled()) { 296 | this.size = Node.SIZE; 297 | this.color = Node.COLOR; 298 | this.stroke = Node.STROKE; 299 | this.textSize = Node.TEXTSIZE; 300 | this.textColor = Node.TEXTCOLOR; 301 | 302 | this.edgeColor = Node.EDGECOLOR; 303 | this.edgeThickness = Node.EDGETHICKNESS; 304 | 305 | this.leftNode.resetVisuals(); 306 | this.rightNode.resetVisuals(); 307 | } 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /src/Tree.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Tree.js 3 | * 4 | * This file defines the Tree class. The Tree class serves as a wrapper for a 5 | * root node, wrapping methods such as addValue or search, but also provides 6 | * higher-level functionality, such as setting the coordinates of the entire 7 | * tree, or drawing the entire tree. 8 | * 9 | * The tree class is also responsible for animating itself, and relies only 10 | * on an instance of the Controls class to provide the interval between frames 11 | * of animation 12 | ***/ 13 | 14 | class Tree { 15 | constructor(x, y, backgroundColor) { 16 | // The buffer that this tree, and all the nodes in the tree draw to 17 | this.graphicsBuffer = createGraphics(CANVASWIDTH, CANVASHEIGHT); 18 | 19 | // The root node; the upper level node of a binary tree 20 | this.root = new Node(this.graphicsBuffer); 21 | 22 | // A reference to a Controls instance 23 | this.controls = null; 24 | 25 | // The x and y coordinate that the root node is drawn at 26 | this.x = x; 27 | this.y = y; 28 | 29 | // The background color of the tree visualization 30 | this.backgroundColor = backgroundColor; 31 | 32 | // Various properties for tracking animations 33 | this.running = false; // Whether or not an animation is running 34 | this.timeout = null; // The current timeout, so animations can be cancelled 35 | this.node = null; // The current node being animated 36 | 37 | // Draw the tree upon creation (to show the background) 38 | this.draw(); 39 | } 40 | 41 | // Sets this instance's reference to a Controls instance so that an 42 | // animation interval can be set 43 | bindControls(controls) { 44 | this.controls = controls; 45 | } 46 | 47 | // Resets the root node to an empty node, effectively clearing all nodes 48 | clear() { 49 | this.root = new Node(this.graphicsBuffer); 50 | } 51 | 52 | // Returns: a random number in the range [0, max) not yet in the tree 53 | uniqueRandom(max) { 54 | while(true) { 55 | var value = Math.floor(random(0, max)); 56 | 57 | if(!this.search(value)) { 58 | return value; 59 | } 60 | } 61 | } 62 | 63 | // Quickly fills the tree with a certain number of nodes 64 | fill(count) { 65 | this.clear(); 66 | 67 | for(var i = 0; i < count; i++) { 68 | var value = this.uniqueRandom(count); 69 | 70 | this.addValue(value); 71 | } 72 | 73 | this.draw(); 74 | } 75 | 76 | // Wraps the Node class's addValue method, and sets the coordinate of the 77 | // subset of the tree that needs to be adjusted after the value was added 78 | addValue(value) { 79 | var shiftedNode = this.root.addValue(value); 80 | 81 | this.setCoordinates(shiftedNode); 82 | } 83 | 84 | // Wraps the Node class's search method directly 85 | search(value) { 86 | return this.root.search(value); 87 | } 88 | 89 | // Wraps the Node class's setCoordinates method. Sets the root's position 90 | // to the x and y coordinates of the tree, or allows nodes to determine 91 | // their own position based off their parents (by passing no arguments) 92 | setCoordinates(node) { 93 | if(node === this.root) { 94 | node.setCoordinates(this.x, this.y); 95 | } else { 96 | node.setCoordinates(); 97 | } 98 | } 99 | 100 | // Draws the entire visulizatino, including the background, and each node 101 | draw() { 102 | this.graphicsBuffer.background(this.backgroundColor); 103 | 104 | if(this.root.isFilled()) { 105 | this.root.draw(); 106 | } 107 | 108 | this.updateDrawing(); 109 | } 110 | 111 | // Displays the tree without redrawing every node in the tree 112 | // This function is used when the color of singular nodes are updated 113 | updateDrawing() { 114 | image(this.graphicsBuffer, 0, 0); 115 | } 116 | 117 | // Wraps the Node class's resetVisuals method, and draws the result 118 | resetVisuals() { 119 | this.root.resetVisuals(); 120 | 121 | this.draw(); 122 | } 123 | 124 | // The first function called before an animation. Checks that no animation 125 | // is currently running, and then initializes various properties to track 126 | // the progress of the animation 127 | // The frame argument is a function representing one frame of the animation 128 | startAnimation(frame, ...args) { 129 | if(this.running) { 130 | throw Error('Animation is currently running'); 131 | } else { 132 | this.running = true; 133 | this.node = this.root; 134 | 135 | this.resetVisuals(); 136 | 137 | // Bind the frame method here so it doesn't have to be bound 138 | // when it is passed as an argument 139 | this.continueAnimation(frame.bind(this), ...args) 140 | } 141 | } 142 | 143 | // Schedules the next frame of the animation 144 | continueAnimation(frame, ...args) { 145 | // Bind the frame function inside the method, so it doesnt have to be 146 | // bound as an arguemtn 147 | this.timeout = setTimeout(() => frame.bind(this)(...args), 148 | this.controls.animationInterval); 149 | } 150 | 151 | // The last function called to end an animation. Resets various properties 152 | // tracking the progression of the animation so that a new animation can be 153 | // run, and runs a specified function with specified arguments when the 154 | // animation is complete 155 | stopAnimation(complete = () => {}, ...callbackArgs) { 156 | this.running = false; 157 | this.node = null; 158 | 159 | clearTimeout(this.timeout); 160 | 161 | setTimeout(() => complete(...callbackArgs), this.controls.animationInterval); 162 | } 163 | 164 | // Call for starting an addValue animation 165 | // value is the value to add 166 | // complete is a callback called when the animation finishes 167 | addValueVisual(value, complete = () => {}, ...callbackArgs) { 168 | this.startAnimation(this.addValueFrame, value, complete, ...callbackArgs); 169 | } 170 | 171 | // Single frame for the addValue animation 172 | // Arguments match addValueVisual 173 | addValueFrame(value, complete, ...callbackArgs) { 174 | if(!this.node.isFilled()) { 175 | this.addValue(value); // Add the value to the data structure 176 | 177 | this.node.paint(Node.SUCCESS); // Mark this node as inserted 178 | 179 | this.draw(); // Show the tree with the new value 180 | 181 | this.stopAnimation(complete, ...callbackArgs); 182 | 183 | } else { 184 | this.node.paint(Node.VISITED); // Mark this node as visited 185 | 186 | this.updateDrawing(); // Display the new color 187 | 188 | // Determine the node for the next frame 189 | if(value < this.node.value) { 190 | this.node = this.node.leftNode; 191 | 192 | } else if(value > this.node.value) { 193 | this.node = this.node.rightNode; 194 | } 195 | 196 | // Schedule the next frame, passing in all arguments for the next call 197 | this.continueAnimation(this.addValueFrame, value, complete, ...callbackArgs) 198 | } 199 | } 200 | 201 | // Call for starting the search animation 202 | // value is the value to search for 203 | // complete is a callback called when the animation finishes 204 | searchVisual(value, complete = () => {}, ...callbackArgs) { 205 | this.startAnimation(this.searchFrame, value, complete, ...callbackArgs); 206 | console.log('searching visually') 207 | } 208 | 209 | // Single frame for the search animatino 210 | // Arugment match serchVisual 211 | searchFrame(value, complete, ...callbackArgs) { 212 | if(this.node.color !== Node.VISITED) { 213 | // Mark the root node as visited first, then continue the search 214 | this.root.paint(Node.VISITED); 215 | 216 | this.updateDrawing(); 217 | 218 | this.continueAnimation(this.searchFrame, value, complete, ...callbackArgs); 219 | 220 | } else if(!this.node.isFilled()) { 221 | // The value isn't in the tree, stop the animation 222 | 223 | this.stopAnimation(complete, ...callbackArgs); 224 | 225 | } else if(this.node.value === value) { 226 | // The value is in this node 227 | 228 | this.node.paint(Node.SUCCESS); // Mark the node as found 229 | 230 | this.updateDrawing(); // Display the new color 231 | 232 | this.stopAnimation(complete, ...callbackArgs); 233 | 234 | } else { 235 | // The value may be in another node 236 | 237 | var nextHalf; // The half of the tree being searched next 238 | var cutHalf; // The hal of the tree that can be cut from search 239 | 240 | // Set the two variables correctly 241 | if(value < this.node.value) { 242 | nextHalf = this.node.leftNode; 243 | cutHalf = this.node.rightNode; 244 | 245 | } else if(value > this.node.value) { 246 | nextHalf = this.node.rightNode; 247 | cutHalf = this.node.leftNode; 248 | } 249 | 250 | // Set the node for the next frame 251 | this.node = nextHalf; 252 | 253 | // Mark the half of the tree the node is not in, draw it 254 | cutHalf.recursivePaint(Node.FAILURE); 255 | cutHalf.draw(); 256 | 257 | // Mark the next node as visited 258 | nextHalf.paint(Node.VISITED); 259 | 260 | // Display all the changes 261 | this.updateDrawing(); 262 | 263 | this.continueAnimation(this.searchFrame, value, complete, ...callbackArgs); 264 | } 265 | } 266 | 267 | // Call for starting the fill animation 268 | // count is the number of nodes to add 269 | // complete is a callback called when the animation finishes 270 | fillVisual(count, complete = () => {}) { 271 | this.clear(); 272 | 273 | this.startAnimation(this.fillFrame, count, 0, complete); 274 | } 275 | 276 | // Single frame of the fill animation 277 | // count is the number of nodes to add 278 | // filled is the number of nodes added so far 279 | // complete is a callback called when the animation finishes 280 | fillFrame(count, filled, complete) { 281 | if(filled === count) { 282 | // Stop the animation if the correct number of nodes were inserted 283 | this.stopAnimation(complete); 284 | } else { 285 | // Temporarily stop the fill animation to start the addValue animation 286 | this.stopAnimation(); 287 | 288 | var value = this.uniqueRandom(count); 289 | 290 | // Start the addValue animation, calling this frame again when the 291 | // animation is complete, and incrementing the number of nodes 292 | // filled so far 293 | this.startAnimation(this.addValueFrame, value, 294 | this.fillFrame.bind(this), count, filled + 1, complete); 295 | } 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /src/sketch.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * sketch.js 3 | * 4 | * The main file for the binary tree visualization, an easy-to-use web-based 5 | * binary tree visualization 6 | * 7 | * Created by Colin Siles on July 6th, 2019 8 | * Last updated July 14th, 2019 9 | * 10 | * Inspired by Coding Train's Binary Tree Coding Challenge: 11 | * https://thecodingtrain.com/CodingChallenges/065.2-binary-tree-viz.html 12 | ***/ 13 | 14 | // Controls the size of the visualization. Defaults to full-screen 15 | var CANVASWIDTH = window.innerWidth; 16 | var CANVASHEIGHT = window.innerHeight; 17 | 18 | // Constants for controlling the position of the binary tree 19 | const TREEX = CANVASWIDTH / 2; // The x-coordinate of the root node 20 | const TREEY = 100; // The y-coordinate of the root node 21 | const BACKGROUNDCOLOR = color(50); // Background color of the visualization 22 | 23 | function setup() { 24 | // Create the canvas and place it in the provided placeholder 25 | var canvas = createCanvas(CANVASWIDTH, CANVASHEIGHT); 26 | canvas.parent('canvas-placeholder'); 27 | 28 | // Create other necessary objects for the visualization 29 | var tree = new Tree(TREEX, TREEY, BACKGROUNDCOLOR); 30 | var explorer = new Explorer(canvas.canvas, tree.graphicsBuffer, tree.draw.bind(tree)); 31 | var controls = new Controls(tree) 32 | } 33 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | canvas { 7 | display: block; 8 | } 9 | 10 | button { 11 | min-width: 64px; 12 | background-color: #4CAF50; 13 | border: none; 14 | color: #FFFFFF; 15 | padding: 15px 32px; 16 | text-align: center; 17 | -webkit-transition-duration: 0.4s; 18 | transition-duration: 0.4s; 19 | margin: 16px 15px !important; 20 | text-decoration: none; 21 | font-size: 16px; 22 | cursor: pointer; 23 | } 24 | 25 | #controls { 26 | position: absolute; 27 | padding: 16px; 28 | top: 0px; 29 | display: flex; 30 | } 31 | 32 | #speed-label, #help-label { 33 | line-height: 200%; 34 | padding: 0px 8px; 35 | color: white; 36 | margin-top: 23px; 37 | margin-right: 10px; 38 | } 39 | --------------------------------------------------------------------------------