feat: implement recorder and visualization

Signed-off-by: Matej Focko <mfocko@redhat.com>
This commit is contained in:
Matej Focko 2022-05-01 17:10:24 +02:00
parent 70c3b55f1a
commit eb0b2a39f6
Signed by: mfocko
GPG key ID: 7C47D46246790496
5 changed files with 190 additions and 729 deletions

82
avl.js
View file

@ -54,22 +54,33 @@ class AVLTree extends RankedTree {
); );
} }
fix0Child(x, y, z, rotateLeft, rotateRight) { fix0Child(x, y, z, rotatingAroundRoot, rotateLeft, rotateRight) {
let newRoot = x.parent; let newRoot = x.parent;
if (!y || nodeDifference(y) == 2) { if (!y || nodeDifference(y) == 2) {
newRoot = rotateRight(z); newRoot = rotateRight(z);
z.demote(); z.demote();
if (rotatingAroundRoot) {
this.root = newRoot;
}
this.record();
} else if (nodeDifference(y) == 1) { } else if (nodeDifference(y) == 1) {
rotateLeft(x); rotateLeft(x);
this.record();
newRoot = rotateRight(z); newRoot = rotateRight(z);
if (rotatingAroundRoot) {
this.root = newRoot;
}
this.record();
y.promote(); y.promote();
x.demote(); x.demote();
z.demote(); z.demote();
}
return newRoot; this.record();
}
} }
insertRebalance(x) { insertRebalance(x) {
@ -78,6 +89,8 @@ class AVLTree extends RankedTree {
x.parent.promote(); x.parent.promote();
x = x.parent; x = x.parent;
this.record();
diffs = nodeDifferences(x.parent).sort(); diffs = nodeDifferences(x.parent).sort();
} }
@ -86,7 +99,6 @@ class AVLTree extends RankedTree {
} }
let rotatingAroundRoot = x.parent.parent == null; let rotatingAroundRoot = x.parent.parent == null;
let newRoot = x.parent;
let rankDifference = nodeDifference(x); let rankDifference = nodeDifference(x);
if (rankDifference != 0) { if (rankDifference != 0) {
@ -96,18 +108,24 @@ class AVLTree extends RankedTree {
let xParent = x.parent; let xParent = x.parent;
if (rankDifference == 0 && x.parent.left === x) { if (rankDifference == 0 && x.parent.left === x) {
newRoot = this.fix0Child( this.fix0Child(
x, x.right, xParent, rotateLeft, rotateRight x,
x.right,
xParent,
rotatingAroundRoot,
rotateLeft,
rotateRight
); );
} else if (rankDifference == 0 && x.parent.right === x) { } else if (rankDifference == 0 && x.parent.right === x) {
newRoot = this.fix0Child( this.fix0Child(
x, x.left, xParent, rotateRight, rotateLeft x,
x.left,
xParent,
rotatingAroundRoot,
rotateRight,
rotateLeft
); );
} }
if (rotatingAroundRoot) {
this.root = newRoot;
}
} }
deleteRotate(x, y, leaning, rotatingAroundRoot, rotateLeft, rotateRight) { deleteRotate(x, y, leaning, rotatingAroundRoot, rotateLeft, rotateRight) {
@ -124,14 +142,18 @@ class AVLTree extends RankedTree {
newRoot = rotateLeft(x); newRoot = rotateLeft(x);
break; break;
} }
[newRoot.left, newRoot.right, newRoot].filter(x => x).forEach(n => {
updateRank(n)
});
if (rotatingAroundRoot) { if (rotatingAroundRoot) {
this.root = newRoot; this.root = newRoot;
} }
this.record();
[newRoot.left, newRoot.right, newRoot]
.filter((x) => x)
.forEach((n) => {
updateRank(n);
this.record();
});
return factor == 0; return factor == 0;
} }
@ -142,6 +164,9 @@ class AVLTree extends RankedTree {
switch (factor) { switch (factor) {
case 0: case 0:
updateRank(x); updateRank(x);
this.record();
return false; return false;
case -1: case -1:
case 1: case 1:
@ -149,12 +174,29 @@ class AVLTree extends RankedTree {
} }
let rotatingAroundRoot = x.parent == null; let rotatingAroundRoot = x.parent == null;
let [z, leaning, toLeft, toRight] = [x.left, -1, rotateRight, rotateLeft]; let [z, leaning, toLeft, toRight] = [
x.left,
-1,
rotateRight,
rotateLeft,
];
if (factor == 2) { if (factor == 2) {
[z, leaning, toLeft, toRight] = [x.right, 1, rotateLeft, rotateRight]; [z, leaning, toLeft, toRight] = [
x.right,
1,
rotateLeft,
rotateRight,
];
} }
return this.deleteRotate(x, z, leaning, rotatingAroundRoot, toLeft, toRight); return this.deleteRotate(
x,
z,
leaning,
rotatingAroundRoot,
toLeft,
toRight
);
} }
deleteRebalance(node, parent) { deleteRebalance(node, parent) {

View file

@ -19,10 +19,11 @@ class Queue {
class RankedTree { class RankedTree {
constructor() { constructor() {
this.root = null; this.root = null;
this.recorder = null;
} }
_getUnwrappedGraph() { _getUnwrappedGraph() {
let result = ""; let result = [];
let q = new Queue(); let q = new Queue();
q.enqueue(this.root); q.enqueue(this.root);
@ -34,24 +35,45 @@ class RankedTree {
continue; continue;
} }
result += `"${node.toString()}" [label="${node.value}, ${node.rank}"];\n`; result.push(
`\t"${node.toString()}" [label="${node.value}, ${node.rank}"];`
);
[node.left, node.right].filter(child => child).forEach(child => { [node.left, node.right]
.filter((child) => child)
.forEach((child) => {
edges.push([node, child]); edges.push([node, child]);
q.enqueue(child); q.enqueue(child);
}); });
} }
edges.forEach(vertices => { result.push("");
edges.forEach((vertices) => {
let [u, v] = vertices; let [u, v] = vertices;
result += `"${u.toString()}" -> "${v.toString()}" [label="${nodeDifference(v)}"]\n`; result.push(
`\t"${u.toString()}" -> "${v.toString()}" [label="${nodeDifference(
v
)}"]`
);
}); });
return result; return result;
} }
toDot() {
return ["digraph {", ...this._getUnwrappedGraph(), "}"];
}
toString() { toString() {
return `digraph {\n${this._getUnwrappedGraph()}}\n`; return this.toDot().join("\n");
}
record() {
if (!this.recorder) {
return;
}
this.recorder.record(this);
} }
rank() { rank() {
@ -71,6 +93,7 @@ class RankedTree {
if (!this.root) { if (!this.root) {
this.root = insertedNode; this.root = insertedNode;
this.record();
return; return;
} }
@ -86,6 +109,7 @@ class RankedTree {
} else { } else {
parent.right = insertedNode; parent.right = insertedNode;
} }
this.record();
this.insertRebalance(insertedNode); this.insertRebalance(insertedNode);
} }
@ -134,6 +158,8 @@ class RankedTree {
node.left.parent = successor; node.left.parent = successor;
} }
this.record();
return [y, parent]; return [y, parent];
} }

View file

@ -1,541 +0,0 @@
var id = 1;
var rootNode = {
id: id++,
key: null,
rank: -1,
parent: null,
children: []
};
var changeRoot = false;
// Find tree's height
var findHeight = function (node) {
if (node.data === null || !node) return 0;
else {
var left = node.children[0] ? findHeight(node.children[0]) : 0;
var right = node.children[1] ? findHeight(node.children[1]) : 0;
return 1 + ((left > right) ? left : right);
}
};
var getHeight = function (node) {
if (node === null) {
return -1;
}
return node.rank;
}
// Binary Search Tree rotation
var rotateLeft = function (node, callback) {
var parent = node.parent,
leftChild = node.children[0],
rightChild = node.children[1];
if (rightChild.children.length === 0) {
rightChild.children.push({ id: id++, key: null, rank: -1, parent: rightChild, children: [] });
rightChild.children.push({ id: id++, key: null, rank: -1, parent: rightChild, children: [] });
}
rightChild.children[0].parent = node;
node.children[1] = rightChild.children[0];
node.parent = rightChild;
rightChild.children[0] = node;
rightChild.parent = parent;
if (parent === null) { // Root node
rootNode = rightChild;
gLinks.selectAll('path').filter(function (d) { // Update root node
return d.data.id === node.parent.id;
}).datum(d3.hierarchy(node).descendants()[0]);
changeRoot = true;
} else if (node === parent.children[0]) { // Left child of parent
parent.children[0] = rightChild;
changeRoot = false;
} else if (node === parent.children[1]) { // Right child of parent
parent.children[1] = rightChild;
changeRoot = false;
}
if (node.children.length !== 0 && node.children[0].key === null && node.children[1].key === null) node.children = [];
setTimeout(function () {
if (callback instanceof Function) {
callback();
}
}, duration);
return rightChild;
};
var rotateRight = function (node, callback) {
var parent = node.parent,
leftChild = node.children[0],
rightChild = node.children[1];
if (leftChild.children.length === 0) {
leftChild.children.push({ id: id++, key: null, rank: -1, parent: leftChild, children: [] });
leftChild.children.push({ id: id++, key: null, rank: -1, parent: leftChild, children: [] });
}
if (parent === null) { // Root node
leftChild.children[1].parent = node;
node.children[0] = leftChild.children[1];
node.parent = leftChild;
leftChild.children[1] = node;
leftChild.parent = parent;
rootNode = leftChild;
gLinks.selectAll('path').filter(function (d) { // Update root node's link
return d.data.id === node.parent.id;
}).datum(d3.hierarchy(node).descendants()[0]);
changeRoot = true;
} else if (node === parent.children[0]) { // Left child of parent
leftChild.children[1].parent = node;
node.children[0] = leftChild.children[1];
node.parent = leftChild;
leftChild.children[1] = node;
leftChild.parent = parent;
parent.children[0] = leftChild;
changeRoot = false;
} else if (node === parent.children[1]) { // Left child of parent
leftChild.children[1].parent = node;
node.children[0] = leftChild.children[1];
node.parent = leftChild;
leftChild.children[1] = node;
leftChild.parent = parent;
parent.children[1] = leftChild;
changeRoot = false;
}
if (node.children.length !== 0 && node.children[0].key === null && node.children[1].key === null) node.children = [];
setTimeout(function () {
if (callback instanceof Function) {
callback();
}
}, duration);
return leftChild;
};
// Node highlight for better visualization
var highlight = function (node) {
var hlNode = gNodes.selectAll('circle').filter(function (d) {
return d.data.id === node.id;
});
hlNode.transition()
.duration(duration / 3)
.style('stroke', '#e74c3c')
.style('stroke-width', '3.5px');
};
var removeHighlight = function (node) {
var hlNode = gNodes.selectAll('circle').filter(function (d) {
return d.data.id === node.id;
});
hlNode.transition()
.duration(duration / 3)
.style('stroke', '#3498db')
.style('stroke-width', '2.5px');
};
// AVL Tree balancing
var balance = function (node, callback) {
highlight(node);
var hLeft = node.children[0] ? findHeight(node.children[0]) : 0;
var hRight = node.children[1] ? findHeight(node.children[1]) : 0;
var hl, hr,
defer = 0.5;
if (hLeft - hRight >= 2) { // Left unbalance
var leftChild = node.children[0];
hl = leftChild.children[0] ? findHeight(leftChild.children[0]) : 0;
hr = leftChild.children[1] ? findHeight(leftChild.children[1]) : 0;
if (hl >= hr) { // Left of left
rotateRight(node, updateTree);
defer = 1;
} else { // Right of left
defer = 3;
rotateLeft(leftChild, function () {
updateTree();
setTimeout(function () {
rotateRight(node, updateTree);
}, duration);
});
}
} else if (hRight - hLeft >= 2) { // Right unbalance
rotated = false;
isChanged = true;
var rightChild = node.children[1];
hl = rightChild.children[0] ? findHeight(rightChild.children[0]) : 0;
hr = rightChild.children[1] ? findHeight(rightChild.children[1]) : 0;
if (hr >= hl) { // Right of right
rotateLeft(node, updateTree);
defer = 1;
} else { // Left of right
defer = 3;
rotateRight(rightChild, function () {
updateTree();
setTimeout(function () {
rotateLeft(node, updateTree);
}, duration);
});
}
}
if (!node.parent) {
node.rank = 69;
}
setTimeout(function () {
removeHighlight(node);
if (!node.parent) { // End balancing
if (callback instanceof Function) callback();
}
else balance(node.parent, callback);
}, duration * defer);
};
var rank = function (node) {
if (node === null) {
return -1;
}
return node.rank;
};
var difference = function (node, parent) {
return rank(parent) - rank(node);
};
var differences = function (node) {
const nodeRank = rank(node);
const leftRank = node ? rank(node.children[0]) : -1;
const rightRank = node ? rank(node.children[1]) : -1;
return [nodeRank - leftRank, nodeRank - rightRank];
};
var fix0Child = function (x, y, z, rotateL, rotateR) {
var newRoot = x.parent;
if (!y || difference(y, y.parent) == 2) {
newRoot = rotateR(z, updateTree);
z.rank--;
} else if (difference(y, y.parent) == 1) {
rotateL(x, function () {
updateTree();
setTimeout(function () {
newRoot = rotateR(z, updateTree);
}, duration);
});
y.rank++;
x.rank--;
z.rank--;
}
return newRoot;
};
var insertFixup = function (x, callback) {
highlight(x);
let [lDiff, rDiff] = differences(x.parent);
while (x.parent && ((lDiff == 0 && rDiff == 1) || (lDiff == 1 && rDiff == 0))) {
console.log(`Node with key: ${x.parent.key} is ${lDiff},${rDiff}-node`);
x.parent.rank++;
removeHighlight(x);
x = x.parent;
highlight(x);
[lDiff, rDiff] = differences(x.parent);
}
console.log(`Rank of node with key=${x.key}: ${x.rank}`);
console.log(`Parent node is ${lDiff},${rDiff}-node`);
if (x.parent === null) {
console.log("exiting");
setTimeout(function () {
removeHighlight(x);
if (callback instanceof Function) {
callback();
}
}, duration * 3);
return;
}
const rotatingAroundRoot = x.parent.parent === null;
var newRoot = x.parent;
var rankDifference = difference(x, x.parent);
console.log(x);
console.log(`Before fix 0-child: ${rankDifference}`);
if (rankDifference != 0) {
setTimeout(function () {
removeHighlight(x);
if (callback instanceof Function) {
callback();
}
}, duration * 3);
return;
}
if (rankDifference == 0 && x.parent.children[0] === x) {
console.log("Fixing 0-child on left");
newRoot = fix0Child(
x, x.children[1], x.parent, rotateLeft, rotateRight
);
} else if (rankDifference == 0 && x.parent.children[1] === x) {
console.log("Fixing 0-child on right");
newRoot = fix0Child(
x, x.children[0], x.parent, rotateRight, rotateLeft
);
}
// if (rotatingAroundRoot) {
// rootNode = newRoot;
// }
setTimeout(function () {
removeHighlight(x);
if (callback instanceof Function) {
callback();
}
}, duration * 3);
};
// Tree insertion
var insert = function (n, callback) {
console.log('Insert', n);
if (!n || !Number.isInteger(n)) return;
if (!rootNode.key) {
rootNode.key = n;
rootNode.rank++;
updateTree();
callback();
return;
}
var walker = rootNode,
newNode;
while (!newNode) {
if (n <= walker.key) {
if (walker.children.length === 0) { // No child
walker.children.push({ id: id++, key: n, rank: 0, parent: walker, children: [] }); // Left child
walker.children.push({ id: id++, key: null, rank: -1, parent: walker, children: [] }); // Empty right child
newNode = walker.children[0];
} else if (walker.children[0].key === null) { // Already have right child, left child is empty
walker.children[0].key = n;
walker.children[0].rank++;
newNode = walker.children[0];
} else { // Move left
walker = walker.children[0];
}
} else {
if (walker.children.length === 0) { // No child
walker.children.push({ id: id++, key: null, rank: -1, parent: walker, children: [] }); // Empty left child
walker.children.push({ id: id++, key: n, rank: 0, parent: walker, children: [] }); // Right child
newNode = walker.children[1];
} else if (walker.children[1].key === null) { // Already have left child, right child is empty
walker.children[1].key = n;
walker.children[1].rank++;
newNode = walker.children[1];
} else { // Move left
walker = walker.children[1];
}
}
}
updateTree();
setTimeout(function () {
insertFixup(newNode, callback);
//callback();
}, duration);
};
// Tree deletion
var deleteTree = function (n, callback) {
if (!rootNode.key) return false;
var walker = rootNode,
nodeDelete = null, // Node to be deleted
nodeReplace = null, // Node to replace deleted node
nodeBalance = null, // After deleting, perform balance on this node to root
parent;
// Find node
if (n === walker.key) { // Deleting root
nodeDelete = walker;
if (nodeDelete.children.length === 0) nodeDelete.key = null; // Tree only has root node
else {
if (nodeDelete.children[0].key === null) { // Root does not have left subtree
rootNode = rootNode.children[1]; // Right subtree becomes new tree
rootNode.parent = null;
gLinks.selectAll('path').filter(function (d) {
return d.data.id === rootNode.id;
}).remove();
} else {
nodeReplace = nodeDelete.children[0];
// In-order predecessor, largest child of left subtree
while (nodeReplace) {
if (!nodeReplace.children[1] || !nodeReplace.children[1].key) break;
nodeReplace = nodeReplace.children[1];
}
parent = nodeReplace.parent;
nodeBalance = parent; // Will start balacing from this node
if (parent.children[0] === nodeReplace) {
if (nodeReplace.children[0]) {
nodeReplace.children[0].parent = parent;
parent.children[0] = nodeReplace.children[0]
} else {
parent.children[0] = { id: id++, key: null, parent: parent, children: [] };
}
}
else if (parent.children[1] === nodeReplace) {
if (nodeReplace.children[0]) {
nodeReplace.children[0].parent = parent;
parent.children[1] = nodeReplace.children[0]
} else {
parent.children[1] = { id: id++, key: null, parent: parent, children: [] };
}
}
// After moving, if nodeReplace's parent has 2 empty children
if (parent.children.length !== 0 && parent.children[0].key === null && parent.children[1].key === null) parent.children = [];
// Add 2 subtrees of old root to new root
if (nodeDelete.children[0]) {
nodeDelete.children[0].parent = nodeReplace;
nodeReplace.children[0] = nodeDelete.children[0];
}
if (nodeDelete.children[1]) {
nodeDelete.children[1].parent = nodeReplace;
nodeReplace.children[1] = nodeDelete.children[1];
}
// Replace deleted root node by largest node of left subtree (in-order predecessor)
nodeReplace.parent = null;
rootNode = nodeReplace;
gLinks.selectAll('path').filter(function (d) {
return d.data.id === nodeReplace.id;
}).remove();
}
}
updateTree();
setTimeout(function () {
if (nodeBalance) balance(nodeBalance, callback);
else if (callback instanceof Function) callback();
}, duration);
return true;
}
// Finding node
while (walker.key) {
if (n < walker.key) walker = walker.children[0]; // Move left
else if (n > walker.key) walker = walker.children[1]; // Move right
else if (n === walker.key) {
nodeDelete = walker;
break;
}
}
if (!nodeDelete) return false;
// Deletion
if (nodeDelete.children.length === 0) { // Node to be deleted is leaf node
parent = nodeDelete.parent;
nodeBalance = parent; // Will start balacing from this node
if (parent.children[0] === nodeDelete) { // Remove left child
parent.children[0] = { id: id++, key: null, parent: parent, children: [] }; // Empty child
}
else if (parent.children[1] === nodeDelete) { // Remove right child
parent.children[1] = { id: id++, key: null, parent: parent, children: [] };
}
if (parent.children.length !== 0 && parent.children[0].key === null && parent.children[1].key === null) parent.children = [];
} else { // Node to be deleted is internal node
nodeReplace = nodeDelete.children[0].key ? nodeDelete.children[0] : null;
// In-order predecessor, largest child of left subtree
while (nodeReplace) {
if (!nodeReplace.children[1] || !nodeReplace.children[1].key) break;
nodeReplace = nodeReplace.children[1];
}
if (!nodeReplace) { // No left child, right child of nodeDelete replace its position
parent = nodeDelete.parent;
nodeBalance = parent; // Will start balacing from this node
nodeDelete.children[1].parent = parent;
if (parent.children[0] === nodeDelete) parent.children[0] = nodeDelete.children[1]; // Left child of parent
else if (parent.children[1] === nodeDelete) parent.children[1] = nodeDelete.children[1]; // Right child of parent
} else {
// Update nodeReplace's parent
parent = nodeReplace.parent;
nodeBalance = parent; // Will start balacing from this node
if (parent.children[0] === nodeReplace) {
if (nodeReplace.children[0]) {
nodeReplace.children[0].parent = parent;
parent.children[0] = nodeReplace.children[0]
} else {
parent.children[0] = { id: id++, key: null, parent: parent, children: [] };
}
}
else if (parent.children[1] === nodeReplace) {
if (nodeReplace.children[0]) {
nodeReplace.children[0].parent = parent;
parent.children[1] = nodeReplace.children[0]
} else {
parent.children[1] = { id: id++, key: null, parent: parent, children: [] };
}
}
// After moving, if nodeReplace's parent has 2 empty children
if (parent.children.length !== 0 && parent.children[0].key === null && parent.children[1].key === null) parent.children = [];
// Replace deleted node by largest node of left subtree (in-order predecessor)
parent = nodeDelete.parent;
nodeReplace.parent = parent;
if (parent.children[0] === nodeDelete) parent.children[0] = nodeReplace; // Left child of parent
else if (parent.children[1] === nodeDelete) parent.children[1] = nodeReplace; // Right child of parent
if (nodeDelete.children[0]) {
nodeDelete.children[0].parent = nodeReplace;
nodeReplace.children[0] = nodeDelete.children[0];
}
if (nodeDelete.children[1]) {
nodeDelete.children[1].parent = nodeReplace;
nodeReplace.children[1] = nodeDelete.children[1];
}
}
}
updateTree();
setTimeout(function () {
if (nodeBalance) balance(nodeBalance, callback);
else if (callback instanceof Function) callback();
}, duration);
return true;
};

View file

@ -1,159 +0,0 @@
/*************Binary Search Tree Visualization using D3JS *************/
const NODE_SIZE = 40;
var duration = 400;
var tree = d3.tree().separation(function () { return 3 * NODE_SIZE; });
var svg = d3.select('svg'),
g = svg.append('g').attr('transform', `translate(${3 * NODE_SIZE},${3 * NODE_SIZE})`);
var gLinks = g.append('g'),
gNodes = g.append('g');
svg.attr('width', '1920')
.attr('height', '1080');
var oldPos = {};
var updateTree = function () {
var root = d3.hierarchy(rootNode);
var newTreeSize = [root.descendants().length * 3 * NODE_SIZE, ((root.height + 1) * 2 - 1) * 30];
if (tree.size()[0] !== newTreeSize[0] || tree.size()[1] !== newTreeSize[1]) {
tree.size(newTreeSize);
center = pos = svg.attr('width') / 2 - tree.size()[0] / 2;
}
tree(root);
var nodes = root.descendants().filter(function (d) {
return d.data.key !== null;
});
var link = gLinks.selectAll('path')
.data(nodes, function (d) { return d.data.id; });
link.exit().remove();
link.transition() // Update new position of old links
.duration(duration)
.attrTween('d', function (d) {
var oldDraw = d3.select(this).attr('d');
if (oldDraw) {
oldDraw = oldDraw.match(/(M.*)(L.*)/);
var oldMoveto = oldMoveto = oldDraw[1].slice(1).split(',').map(Number),
oldLineto = oldDraw[2].slice(1).split(',').map(Number);
// If root is changed, reverse to correctly animated if rotate left
if (changeRoot && oldMoveto[1] === 0) { // Old root node
oldMoveto = oldDraw[2].slice(1).split(',').map(Number);
oldLineto = oldDraw[1].slice(1).split(',').map(Number);
changeRoot = false;
}
if ((oldLineto !== [d.x, d.y]) && (oldMoveto !== [d.parent.x, d.parent.y])) {
/*console.log(d.data.key, oldMoveto, oldLineto);
console.log(d.data.key, [d.parent.x, d.parent.y], [d.x, d.y]);*/
var interpolatorMX = d3.interpolateNumber(oldMoveto[0], d.parent.x);
var interpolatorMY = d3.interpolateNumber(oldMoveto[1], d.parent.y);
var interpolatorLX = d3.interpolateNumber(oldLineto[0], d.x);
var interpolatorLY = d3.interpolateNumber(oldLineto[1], d.y);
return function (t) {
return 'M' + interpolatorMX(t) + ',' + interpolatorMY(t) + 'L' + interpolatorLX(t) + ',' + interpolatorLY(t);
};
}
}
});
link.enter().append('path') // Add new element for new data
.attr('class', 'link')
.transition()
.duration(duration)
.attrTween('d', function (d) {
if (d.parent) {
var parentOldPos = oldPos[d.parent.data.id.toString()];
var interpolatorMX = d3.interpolateNumber(parentOldPos[0], d.parent.x);
var interpolatorMY = d3.interpolateNumber(parentOldPos[1], d.parent.y);
var interpolatorLX = d3.interpolateNumber(parentOldPos[0], d.x);
var interpolatorLY = d3.interpolateNumber(parentOldPos[1], d.y);
return function (t) {
return 'M' + interpolatorMX(t) + ',' + interpolatorMY(t) + 'L' + interpolatorLX(t) + ',' + interpolatorLY(t);
};
}
else {
d3.select(this).remove();
}
});
var node = gNodes.selectAll('g')
.data(nodes, function (d) { return d.data.id; });
node.exit().remove();
node.transition()
.duration(duration)
.attr('transform', function (d) {
setTimeout(function () { // Finish transition, update old position of this node
oldPos[d.data.id.toString()] = [d.x, d.y];
}, duration);
return 'translate(' + d.x + ',' + d.y + ')';
});
var newNode = node.enter().append('g')
.attr('transform', function (d) {
if (!d.parent) return 'translate(' + d.x + ',' + (d.y - 30) + ')';
else return 'translate(' + oldPos[d.parent.data.id.toString()][0] + ',' + (oldPos[d.parent.data.id.toString()][1] - 30) + ')';
})
.attr('class', 'node');
newNode.transition()
.duration(duration)
.attr('transform', function (d) {
oldPos[d.data.id.toString()] = [d.x, d.y];
return 'translate(' + d.x + ',' + d.y + ')';
});
newNode.append('circle')
.attr('r', NODE_SIZE);
newNode.append('text')
.attr('class', 'text')
.attr('text-anchor', 'middle')
.attr('dy', 5)
.text(function (d) { return `${d.data.key} (r=${d.data.rank})`; });
};
var handleInsert = function (event) {
var num = document.getElementById('insertInput').value;
if (num) {
document.getElementById('insertInput').value = '';
d3.selectAll('#insertTree input').each(function () { // Disable insert
d3.select(this).attr('disabled', '')
});
insert(parseInt(num), function () {
d3.selectAll('#insertTree input').each(function () { // Enable insert
d3.select(this).attr('disabled', null);
});
});
}
return false;
};
var handleDelete = function (event) {
var num = document.getElementById('deleteInput').value;
if (num && rootNode.key !== null) { // Tree is not empty
document.getElementById('deleteInput').value = '';
d3.selectAll('#deleteTree input').each(function () { // Disable insert
d3.select(this).attr('disabled', '')
});
deleteTree(parseInt(num), function () {
d3.selectAll('#deleteTree input').each(function () { // Enable insert
d3.select(this).attr('disabled', null);
});
});
}
return false;
};

93
visualization.js Normal file
View file

@ -0,0 +1,93 @@
class Recorder {
constructor(graph) {
this.graph = graph;
this.rendered = -1;
this.rendering = false;
this.states = new Array();
this.unblockRendering = ((recorder) => () => {
recorder.rendering = false;
})(this);
this.initGraph();
}
initGraph() {
this.graph.transition(() =>
d3
.transition("main")
.ease(d3.easeLinear)
.on("end", this.unblockRendering)
.delay(500)
.duration(1500)
);
}
render() {
if (
this.rendering ||
!this.states ||
this.rendered == this.states.length - 1
) {
return;
}
this.rendering = true;
this.rendered++;
this.graph.renderDot(this.states[this.rendered]);
}
previous() {
throw "not implemented!";
}
next() {
throw "not implemented!";
}
record(tree) {
// TODO: adjust join if necessary
this.states.push(tree.toDot().join(""));
this.render();
}
}
let recorder = new Recorder(d3.select("#graph").graphviz());
let tree = new AVLTree();
tree.recorder = recorder;
function insertCallback() {
let number = document.getElementById("insertInput").value;
if (number === "") {
return false;
}
let value = parseInt(number);
tree.insert(value);
document.getElementById("insertInput").value = "";
return false;
}
function deleteCallback() {
let number = document.getElementById("deleteInput").value;
if (number === "") {
return false;
}
let value = parseInt(number);
tree.delete(value);
document.getElementById("deleteInput").value = "";
return false;
}
function render() {
recorder.render();
setTimeout(render);
}
render();