Large architectural update for extra memory visualization.

master
ben-burlingham 10 years ago
parent f2891359ab
commit b224562970
  1. 8
      css/style.css
  2. 4
      index.html
  3. 146
      js/itemgroup.js
  4. 166
      js/mergesort.js
  5. 58
      js/sorter.js
  6. 122
      js/visualizer-actions.js
  7. 62
      js/visualizer-dom.js
  8. 60
      js/visualizer.js

@ -25,7 +25,7 @@ html, body {
.sorter {
background:#f4f4f4;
border:1px solid #bbb;
height:270px;
height:260px;
margin:20px auto;
padding:10px;
position:relative;
@ -33,7 +33,7 @@ html, body {
}
.sorter-sidebar {
height:230px;
height:220px;
position:absolute;
right:10px;
top:10px;
@ -43,7 +43,7 @@ html, body {
.sorter-svg {
background:#fff;
border:1px solid #ccc;
height:200px;
height:190px;
left:10px;
position:absolute;
top:10px;
@ -63,7 +63,7 @@ html, body {
height:40px;
left:10px;
position:absolute;
top:220px;
top:210px;
width:770px;
}

@ -81,8 +81,8 @@
<script type='text/javascript' src='vendor/d3/d3.min.js'></script>
<script type='text/javascript' src='js/visualizer.js'></script>
<script type='text/javascript' src='js/visualizer-actions.js'></script>
<script type='text/javascript' src='js/visualizer-inits.js'></script>
<script type='text/javascript' src='js/itemgroup.js'></script>
<script type='text/javascript' src='js/visualizer-dom.js'></script>
<script type='text/javascript' src='js/sorter.js'></script>
<script type='text/javascript' src='js/quicksort.js'></script>
<script type='text/javascript' src='js/mergesort.js'></script>

@ -0,0 +1,146 @@
/**
*
*/
var Itemgroup = {
/**
*
*/
items: function items(group, delay, n) {
var g;
var all = [];
// Items start with no background by default.
for (var i = 0; i < n; i++) {
g = group.append('g')
.attr('class', `i${i}`)
.attr('transform', function transform(d) {
return `translate(${i * (Visualizer.itemW + Visualizer.spacerW) + Visualizer.padding}, ${Visualizer.padding})`;
});;
g.append('rect')
.attr('class', 'item')
.attr('height', Visualizer.itemH)
.attr('width', Visualizer.itemW)
.attr('fill', 'transparent');
// Item labels
g.append('text')
.attr('fill', '#aaa')
.attr('font-size', 10)
.attr('font-family', 'sans-serif')
.attr('transform', function transform(d) {
return `rotate(90 0,0), translate(5, -3)`;
});
all.push(g);
};
return all;
},
/**
*
*/
swap: function swap(group, delay, indexA, indexB) {
var x, a, b;
var len = group.selectAll('g')[0].length;
// NOTE pitfalls in the swapping problem.
// NOTE Two way binding here between dataset and function parameter?
// NOTE swapping will not reorder index and i parameter will be off
// NOTE discuss chained transitions: http://bl.ocks.org/mbostock/1125997
// Animate a node swap which will be quietly undone after completion.
group.selectAll('g').transition().duration(delay - 100)
.attr('transform', function transform(d, i) {
x = i * (Visualizer.itemW + Visualizer.spacerW) + Visualizer.padding;
if (i === indexA) {
x = indexB * (Visualizer.itemW + Visualizer.spacerW) + Visualizer.padding;
a = d3.select(this).select('text').text();
}
else if (i === indexB) {
x = indexA * (Visualizer.itemW + Visualizer.spacerW) + Visualizer.padding;
b = d3.select(this).select('text').text();
}
return `translate(${x}, ${Visualizer.padding})`;
})
.each('end', function(d, i) {
if (i !== len - 1) {
return;
}
console.log(`indexA: ${indexA}, ${a}, indexB: ${indexB}, ${b}`);
// Undo the animation by restoring the original positions and swapping the values.
group.selectAll('g')
.attr('transform', function transform(d, i) {
x = i * (Visualizer.itemW + Visualizer.spacerW) + Visualizer.padding;
return `translate(${x}, ${Visualizer.padding})`;
})
.each(function(d, i) {
if (i === indexA) {
d3.select(this).select('text').text(b);
}
else if (i === indexB) {
d3.select(this).select('text').text(a);
}
});
console.warn(`indexA: ${indexA}, ${a}, indexB: ${indexB}, ${b}`);
});
},
/**
*
*/
background: function background(group, delay, start, end, color) {
group.selectAll('g').each(function(d, i) {
if (i >= start && i <= end) {
d3.select(this).select('rect').attr('fill', color);
}
});
},
/**
*
*/
foreground: function foreground(group, delay, start, end, color) {
group.selectAll('g').each(function(d, i) {
if (i >= start && i <= end) {
d3.select(this).select('text').attr('fill', color);
}
});
},
/**
*
*/
text: function text(group, delay, which, text) {
// NOTE http://stackoverflow.com/questions/28390754/get-one-element-from-d3js-selection-by-index
group.selectAll('g')
.filter(function filter(d, i) { return i === which; })
.select('text').text(text);
},
/**
*
*/
opacity: function opacity(group, delay, start, end, opacity) {
group.selectAll('g').each(function(d, i) {
if (i >= start && i <= end) {
d3.select(this).attr('opacity', opacity);
}
});
},
/**
* Message updates.
*/
message: function message(group, delay, which, msg) {
var msg = msg || '&nbsp;';
this.parent.querySelector(`.message:nth-child(${which})`).innerHTML = msg;
},
};

@ -1,96 +1,120 @@
/**
*
*/
var MergeSort = function(VisualizerInstance) {
var MergeSort = function() {
//===== Inits.
this.V = VisualizerInstance;
this.actions = [];
this.comparisons = 0;
//===== Action management.
//
this.initSort = function(arr, start, end) {
this.V
.instruct(this.V.unhighlight, 0)
.instruct(this.V.unfade, 0)
.instruct(this.V.fade, 0, -1, start - 1)
.instruct(this.V.fade, 0, end + 1, arr.length)
.instruct(this.V.message, 0, 1, `Comparisons: ${this.comparisons}`)
.instruct(this.V.message, 0, 2, '')
.instruct(this.V.message, 0, 3, '')
.instruct(this.V.message, 0, 4, '')
.instruct(this.V.message, 0, 5, '');
this.reset = function(arr, start, end) {
var len = arr.length;
this
.instruct(Itemgroup.background, 0, 0, '#f00', 0, len)
.instruct(Itemgroup.opacity, 0, 0, 0, len, 1)
.instruct(Itemgroup.opacity, 0, 0, -1, start - 1, 0.2)
.instruct(Itemgroup.opacity, 0, 0, end + 1, len, 0.2)
.instruct(Itemgroup.message, 0, 0, 1, `Comparisons: ${this.comparisons}`)
.instruct(Itemgroup.message, 0, 0, 2, '')
.instruct(Itemgroup.message, 0, 0, 3, '')
.instruct(Itemgroup.message, 0, 0, 4, '')
.instruct(Itemgroup.message, 0, 0, 5, '');
};
//
this.splitSingle = function(index) {
this.V.instruct(this.V.message, 100, 2, `Single element [${index}]`);
this
.instruct(Itemgroup.message, 0, 100, 2, `Single element [${index}]`);
};
//
this.preSort = function(start, mid, end) {
this.V
.instruct(this.V.message, 0, 2, `Sorting [${start}] - [${end}]`)
.instruct(this.V.message, 0, 3, 'Slicing and recursing:')
.instruct(this.V.message, 100, 4, `[${start}]-[${mid}] and [${mid + 1}]-[${end}]`);
this
.instruct(Itemgroup.message, 0, 0, 2, `Sorting [${start}] - [${end}]`)
.instruct(Itemgroup.message, 0, 0, 3, 'Slicing and recursing:')
.instruct(Itemgroup.message, 0, 100, 4, `[${start}]-[${mid}] and [${mid + 1}]-[${end}]`);
};
//
this.preMerge = function(arr1, arr2, start, mid, end) {
var i, j, x, y, v;
var len1 = arr1.length;
var len2 = arr2.length;
for (var i = 0; i < len1; i++) {
x = Visualizer.padding + (i + start) * (Visualizer.itemW + Visualizer.spacerW);
y = Visualizer.padding * 2 + Visualizer.itemH;
v = arr1[i].value;
this.V.instruct(this.V.item, 0, 'secondary', x, y, v, '#05350D')
}
for (var j = 0; j < len2; j++) {
x = Visualizer.padding + (j + len1 + start) * (Visualizer.itemW + Visualizer.spacerW);
y = Visualizer.padding * 2 + Visualizer.itemH;
v = arr2[j].value;
this.V.instruct(this.V.item, 0, 'secondary', x, y, v, '#028E2D')
}
this.V
// .instruct(this.V.fade, 0, 0, arr.length)
.instruct(this.V.message, 0, 2, ``)
.instruct(this.V.message, 0, 3, 'Merging slices:')
.instruct(this.V.message, 0, 4, `[${start}]-[${mid}] and [${mid + 1}]-[${end}]`)
.instruct(this.V.removeTertiary, 0);
this.preMerge = function(start, mid, end, len) {
this
.instruct(Itemgroup.message, 0, 0, 2, ``)
.instruct(Itemgroup.message, 0, 0, 3, 'Merging slices:')
.instruct(Itemgroup.message, 0, 0, 4, `[${start}]-[${mid}] and [${mid + 1}]-[${end}]`)
.instruct(Itemgroup.opacity, 1, 0, 0, len, 0)
.instruct(Itemgroup.opacity, 1, 0, start, end, 1)
.instruct(Itemgroup.opacity, 2, 0, 0, len, 0)
};
//
this.postMerge = function() {
this.V.instruct(this.V.removeSecondary, 0);
this.postMerge = function(len) {
this
.instruct(Itemgroup.opacity, 1, 0, 0, len, 0)
};
//
this.midMerge = function(index, value, message) {
var x = Visualizer.padding + index * (Visualizer.itemW + Visualizer.spacerW);
var y = Visualizer.padding * 3 + Visualizer.itemH * 2
this.V
.instruct(this.V.item, 0, 'tertiary', x, y, value, '#8E5500')
.instruct(this.V.message, 0, 4, message)
.instruct(this.V.message, 100, 5, `Pushing ${value} to sub-result.`);
this
.instruct(Itemgroup.opacity, 1, 0, index, index, 1)
.instruct(Itemgroup.text, 1, 0, index, value)
.instruct(Itemgroup.message, 0, 100, 5, `Pushing ${value} to sub-result.`);
};
//
this.updateComparisons = function() {
this.V.instruct(this.V.message, 0, 1, `Comparisons: ${this.comparisons}`);
this.instruct(Itemgroup.message, 0, 0, 1, `Comparisons: ${this.comparisons}`);
};
};
MergeSort.prototype = Object.create(Sorter.prototype);
/**
*
*/
MergeSort.prototype.init = function() {
var len = this.shuffled.length;
this
.instruct(Itemgroup.items, 0, 0, len)
.instruct(Itemgroup.items, 1, 0, len)
.instruct(Itemgroup.items, 2, 0, len)
for (var i = 0; i < len; i++) {
this.instruct(Itemgroup.text, 0, 0, i, this.shuffled[i]);
}
this
.instruct(Itemgroup.foreground, 0, 0, 0, len, Visualizer.fg0)
.instruct(Itemgroup.background, 0, 0, 0, len, Visualizer.bg0)
.instruct(Itemgroup.opacity, 1, 0, 0, 0, len)
.instruct(Itemgroup.foreground, 1, 0, 0, len, Visualizer.fg1)
.instruct(Itemgroup.background, 1, 0, 0, len, Visualizer.bg1)
.instruct(Itemgroup.opacity, 2, 0, 0, 0, len)
.instruct(Itemgroup.foreground, 2, 0, 0, len, Visualizer.fg2)
.instruct(Itemgroup.background, 2, 100, 0, len, Visualizer.bg2)
this
.instruct(Itemgroup.swap, 0, 1000, 0, 1)
.instruct(Itemgroup.swap, 0, 1000, 1, 2)
.instruct(Itemgroup.swap, 0, 1000, 2, 3)
.instruct(Itemgroup.swap, 0, 1000, 3, 2)
.instruct(Itemgroup.swap, 0, 1000, 2, 1)
.instruct(Itemgroup.swap, 0, 1000, 1, 0)
};
/**
*
*/
MergeSort.prototype.sort = function(arr, start, end) {
this.initSort(arr, start, end);
this.reset(arr, start, end);
if (arr.length === 0) {
return arr;
@ -98,7 +122,7 @@ MergeSort.prototype.sort = function(arr, start, end) {
if (start === end) {
this.splitSingle(start);
return new Array(arr[start]);
return [arr[start]];
}
@ -108,10 +132,10 @@ MergeSort.prototype.sort = function(arr, start, end) {
var arr1 = this.sort(arr, start, mid);
var arr2 = this.sort(arr, mid + 1, end);
this.preMerge(arr1, arr2, start, mid, end);
this.preMerge(start, mid, end, arr.length);
var result = this.merge(arr1, arr2);
this.postMerge(arr.length);
this.postMerge();
return result;
};
@ -120,28 +144,28 @@ MergeSort.prototype.sort = function(arr, start, end) {
*/
MergeSort.prototype.merge = function(arr1, arr2) {
var result = [];
var e;
var n;
while (arr1.length > 0 || arr2.length > 0) {
if (arr1.length === 0) {
e = arr2.shift();
result.push(e);
this.midMerge(result.length, e.value, 'One element left to merge.');
n = arr2.shift();
result.push(n);
this.midMerge(result.length, n, 'One element left to merge.');
}
else if (arr2.length === 0) {
e = arr1.shift();
result.push(e);
this.midMerge(result.length, e.value, 'One element left to merge.');
n = arr1.shift();
result.push(n);
this.midMerge(result.length, n, 'One element left to merge.');
}
else if (arr1[0].value <= arr2[0].value) {
e = arr1.shift()
result.push(e);
this.midMerge(result.length, e.value, `${e.value} <= ${arr2[0].value}`);
else if (arr1[0] <= arr2[0]) {
n = arr1.shift()
result.push(n);
this.midMerge(result.length, n, `${n} <= ${arr2[0]}`);
}
else {
e = arr2.shift();
result.push(e);
this.midMerge(result.length, e.value, `${arr1[0].value} > ${e.value}`);
n = arr2.shift();
result.push(n);
this.midMerge(result.length, n, `${arr1[0]} > ${n}`);
}
this.comparisons++;

@ -1,7 +1,14 @@
/**
*
*/
var Sorter = function() {};
var Sorter = function() {
this.data = [];
this.shuffled = [];
this.ordered = [];
this.actions = [];
this.comparisons = [];
};
// NOTE fisher-yates, http://bost.ocks.org/mike/algorithms/
/**
@ -22,40 +29,49 @@ Sorter.prototype.shuffle = function(arr) {
/**
*
*/
Sorter.prototype.generate = function(n) {
var arr = [];
var v;
for (var i = 0; i < n; i++) {
v = Math.floor(i * 255 / n);
arr.push({
value: v,
fill: `rgb(0, 0, ${v})`
});
};
Sorter.prototype.swap = function(arr, i, j) {
var tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
};
return arr;
/**
*
*/
Sorter.prototype.sort = function() {
throw new Error('Sorter.sort() method override required.');
};
/**
*
*/
Sorter.prototype.swap = function(arr, i, j) {
// console.info(`swapping ${arr[i].value} and ${arr[j].value}`)
var tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
Sorter.prototype.init = function() {
throw new Error('Sorter.init() method override required.');
};
/**
*
*/
Sorter.prototype.sort = function(instruction) {
throw new Error('Sorter.sort() method override required.');
Sorter.prototype.instruct = function() {
this.actions.push(arguments);
return this;
};
/**
*
*/
Sorter.prototype.addInstruction = function(instruction) {
throw new Error('Sorter.addInstruction() method override required.');
Sorter.prototype.generate = function(n) {
this.data = [];
var upper = Math.floor(Math.random() * 300 + n);
for (var i = 0; i < n; i++) {
this.data.push(Math.floor(i * upper / n));
};
this.shuffled = this.shuffle(this.data);
this.ordered = this.shuffled.slice();
this.init();
// this.sort(this.ordered, 0, this.ordered.length - 1);
return this.actions;
};

@ -1,122 +0,0 @@
/**
*
*/
Visualizer.prototype.swap = function(delay, indexA, indexB) {
// NOTE Two way binding here between dataset and function parameter?
// NOTE swapping will not reorder index and i parameter will be off
// NOTE discuss chained transitions: http://bl.ocks.org/mbostock/1125997
this.groups
.transition().duration(delay)
.attr('transform', function doTransform(d) {
if (d.index === indexA) {
d.index = indexB;
}
else if (d.index === indexB) {
d.index = indexA;
}
return `translate(${Visualizer.calculateX(d.index)}, ${Visualizer.itemY})`;
});
};
/**
* Highlights a range of indices with a color. End index and color optional.
*/
Visualizer.prototype.highlight = function(delay, startIndex, endIndex, color) {
if (endIndex === undefined) {
endIndex = startIndex;
}
if (color === undefined) {
color = 'orangered';
}
this.groups.each(function(d, i) {
if (d.index >= startIndex && d.index <= endIndex) {
d3.select(this).select('rect').attr('fill', color);
}
});
};
/**
* Un-highlights an index.
*/
Visualizer.prototype.unhighlight = function() {
// this.svg.selectAll('.item').attr('fill', function(d) { return d.fill; });
this.svg.selectAll('.item').attr('fill', '#1A45AC');
};
/**
* Greys out an item.
*/
Visualizer.prototype.fade = function(delay, startIndex, endIndex) {
this.groups.each(function(d) {
if (d.index >= startIndex && d.index <= endIndex) {
d3.select(this).style('opacity', '0.2');
}
});
};
/**
* Restores all items to un-greyed state.
*/
Visualizer.prototype.unfade = function() {
this.groups.each(function(d) {
d3.select(this).style('opacity', 1);
});
};
/**
* Message updates.
*/
Visualizer.prototype.message = function(delay, which, msg) {
var msg = msg || '&nbsp;';
this.parent.querySelector(`.message:nth-child(${which})`).innerHTML = msg;
};
/**
*
*/
Visualizer.prototype.item = function(delay, classname, x, y, text, color) {
var g = this.svg.append('g')
.attr('class', classname)
.attr('transform', `translate(${x}, ${y})`);
g.append('rect').attr('width', Visualizer.itemW)
.attr('height', Visualizer.itemH)
.attr('fill', color);
g.append('text').text(text)
.attr('fill', '#aaa')
.attr('font-size', 10)
.attr('font-family', 'sans-serif')
.attr('transform', function doTransform(d) {
return `rotate(90 0,0), translate(5, -3)`;
});
};
/**
*
*/
Visualizer.prototype.removeSecondary = function() {
this.svg.selectAll('.secondary').remove();
};
/**
*
*/
Visualizer.prototype.removeTertiary = function() {
this.svg.selectAll('.tertiary').remove();
};
/**
*
*/
Visualizer.prototype.remove = function(delay, id) {
this.svg.select(`#${id}`).remove();
};
/**
*
*/
// Visualizer.prototype.move = function()

@ -3,51 +3,23 @@
/**
*
*/
Visualizer.prototype.initItems = function(n) {
var data = this.sorter.generate(n);
var shuffled = this.sorter.shuffle(data);
var ordered = Object.create(shuffled);
ordered = this.sorter.sort(ordered, 0, ordered.length - 1);
// A swap on the dataset will not take effect until after transition is complete, so custom index is required.
var n = 0;
for (var i in shuffled) {
shuffled[i].index = n++;
}
if (this.svg !== undefined) {
this.svg.remove();
}
this.actionIndex = 0;
this.svg = d3.select(this.parent).append('svg')
.attr('class', 'sorter-svg');
// Items
this.groups = this.svg.selectAll('g').data(shuffled).enter().insert('g')
.attr('transform', `translate(0, ${Visualizer.padding})`);
this.groups.append('rect')
.attr('class', 'item')
.attr('height', Visualizer.itemH)
.attr('width', Visualizer.itemW)
// .attr('fill', function doFill(d) { return d.fill; });
.attr('fill', '#1A45AC');
this.groups.transition(500)
.attr('transform', function doTransform(d, i) {
return `translate(${i * (Visualizer.itemW + Visualizer.spacerW) + Visualizer.padding}, ${Visualizer.padding})`;
});
// Item labels
this.groups.append('text')
.text(function t(d) { return d.value; })
.attr('fill', '#aaa')
.attr('font-size', 10)
.attr('font-family', 'sans-serif')
.attr('transform', function doTransform(d) {
return `rotate(90 0,0), translate(5, -3)`;
});
Visualizer.prototype.initSvg = function() {
var svg = d3.select(this.parent).append('svg').attr('class', 'sorter-svg');
var groups = [];
groups.push(svg.append('g')
.attr('transform', `translate(0, 0)`)
);
groups.push(svg.append('g')
.attr('transform', `translate(0, ${Visualizer.padding + Visualizer.itemH})`)
);
groups.push(svg.append('g')
.attr('transform', `translate(0, ${Visualizer.padding * 2 + Visualizer.itemH * 2})`)
);
return groups;
};
/**

@ -3,9 +3,11 @@
*/
function Visualizer(parent) {
this.actions = [];
this.actionIndex = 0;
this.parent = parent;
this.sorter = null;
this.paused = true;
this.groups = this.initSvg();
var sorterSidebarContainer = document.createElement('div');
sorterSidebarContainer.className = 'sorter-sidebar';
@ -25,60 +27,51 @@ function Visualizer(parent) {
switch(parent.attributes['data-algorithm'].value) {
case 'quick':
this.sorter = new QuickSort(this);
this.sorter = new QuickSort();
break;
case 'merge':
this.sorter = new MergeSort(this);
this.sorter = new MergeSort();
break;
case 'selection':
this.sorter = new SelectionSort(this);
this.sorter = new SelectionSort();
break;
case 'bubble':
this.sorter = new BubbleSort(this);
this.sorter = new BubbleSort();
break;
case 'insertion':
this.sorter = new InsertionSort(this);
this.sorter = new InsertionSort();
break;
case 'shell':
this.sorter = new ShellSort(this);
this.sorter = new ShellSort();
break;
case 'radix':
this.sorter = new RadixSort(this);
this.sorter = new RadixSort();
break;
default:
throw new Error('Unrecognized sort type.');
}
this.initItems(10);
this.actions = this.sorter.generate(10);
};
// Static properties (mutable)
// Static properties (global, mutable)
Visualizer.spacerW = 5;
Visualizer.itemW = 14;
Visualizer.itemH = 50;
Visualizer.padding = 10;
/**
* Static.
*/
Visualizer.calculateX = function(index) {
return Visualizer.spacerW + index * (Visualizer.itemW + Visualizer.spacerW)
};
/**
*
*/
Visualizer.prototype.instruct = function() {
this.actions.push(arguments);
return this;
};
Visualizer.bg0 = '#284A8F';
Visualizer.bg1 = '#C25C49';
Visualizer.bg2 = '#CCCC53';
Visualizer.fg0 = '#e7e7e7';
Visualizer.fg1 = '#e7e7e7';
Visualizer.fg2 = '#000000';
/**
* Instructions contain a string with the name of a function in this object which is called to perform an action.
@ -89,30 +82,33 @@ Visualizer.prototype.go = function() {
}
var obj = this.actions[this.actionIndex];
var instruction = new Array();
var action = new Array();
for (var key in obj) {
if (obj[key].hasOwnProperty) {
instruction.push(obj[key]);
action.push(obj[key]);
}
}
var delay = instruction[1];
var args = instruction.slice(1);
action[1] = this.groups[action[1]];
var delay = action[2];
var args = action.slice(1);
action[0].apply(this, args);
// TODO add tabs for best/worst cases
// TODO add links to stats
// TODO fix init slider
// TODO heap sort
// TODO extra memory
// TODO width and height updates
// TODO width update
// TODO disable next button if no further actions and during action
// NOTE functional programming discussion
// NOTE interesting (anti?)pattern here.
// NOTE use of call() vs apply() (apply only delivered first array item as string)
// if (typeof operation === 'function') {
// operation.call(this, instruction);
instruction[0].apply(this, args);
// operation.call(this, action);
if (delay === 0) {
this.actionIndex++;

Loading…
Cancel
Save