diff --git a/.gitignore b/.gitignore index 91be243..5d860c0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ vendor node_modules +.todo diff --git a/index.html b/index.html index 7ec1c55..893b53a 100644 --- a/index.html +++ b/index.html @@ -19,162 +19,45 @@ + + + diff --git a/js/sorter.js b/js/sorter.js new file mode 100644 index 0000000..1d80404 --- /dev/null +++ b/js/sorter.js @@ -0,0 +1,79 @@ +/** + * + */ +var Sorter = function() {}; +Sorter.prototype.instructions = []; + +// NOTE fisher-yates, http://bost.ocks.org/mike/algorithms/ +/** + * + */ +Sorter.prototype.shuffle = function(arr) { + var n = arr.length, t, i; + while (n) { + i = Math.random() * n-- | 0; // 0 ≤ i < n + t = arr[n]; + arr[n] = arr[i]; + arr[i] = t; + } + return arr; +}; + +/** + * + */ +Sorter.prototype.addInstruction = function(operation, left, right, pivot) { + this.instructions.push({ + operation: operation, + left: left, + right: right, + pivot: pivot + }); +}; + +// NOTE adds to an instruction set +/** + * + */ +Sorter.prototype.quicksort = function(arr, start, end) { + if (end - start <= 0) { + this.addInstruction('single', start, end, null); + return this.visualizer; + } + + var left = start; + var right = end; + + var pivot = Math.floor((right + left) / 2); + var pivotval = arr[pivot].val; + var tmp; + + this.addInstruction('init', left, right, pivot); + + while (left <= right) { + while (arr[left].val < pivotval) { + left++; + this.addInstruction('inc-left', left, right, pivot); + } + + while (arr[right].val > pivotval) { + right--; + this.addInstruction('dec-right', left, right, pivot); + } + + if (left <= right) { + tmp = arr[left]; + arr[left] = arr[right]; + arr[right] = tmp; + + this.addInstruction('swap', left, right, pivot); + + left++; + right--; + this.addInstruction('swap-inc-dec', left, right, pivot); + } + } + + this.quicksort(arr, start, right); + this.quicksort(arr, left, end); +}; diff --git a/js/visualizer.js b/js/visualizer.js new file mode 100644 index 0000000..dd0c86b --- /dev/null +++ b/js/visualizer.js @@ -0,0 +1,178 @@ +/** + * + */ +var Visualizer = function() { + this.itemW = 20; + this.itemH = 50; + + this.svg = null; + this.instructions = null; +}; +Visualizer.prototype.svg = null; +Visualizer.prototype.instructions = null; + +/** + * + */ +Visualizer.prototype.init = function(data) { + // A swap on the dataset will not take effect until after transition is complete, so custom index is required. + var n = 0; + for (i in data) { + data[i].index = n++; + } + + this.svg = d3.select('body').append('svg') + .attr('class', 'quicksort-random'); + + this.svg.selectAll('rect').data(data).enter() + .append('rect') + .attr('height', function h(d) { return d.h; }) + .attr('width', function w(d) { return d.w; }) + .attr('fill', function fill(d) { return d.fill; }) + .attr('y', function y(d) { return d.y; }) + .attr('x', function x(d) { return d.x; }) + .append('text') + .text(function t(d) { return d.val; }) + .attr('fill', '#000') + .attr('x', function x(d) { return d.x; }) + .transition(500) + .attr('x', function expand(d, i) { + data[i].x = i * (d.w + 2); + return data[i].x; + }); + + // this.svg.append('sometext').id('left-marker'); L + // this.svg.append('sometext').id('right-marker'); R + // this.svg.append('sometext').id('pivot-marker'); triangle or arrow +}; + +/** + * + */ +Visualizer.prototype.swap = function(indexA, indexB) { + // Move up + // NOTE Two way binding here between dataset and function parameter? + // NOTE swapping will not reorder index and i parameter will be off + this.svg.selectAll('rect') + // .transition() + // .duration(100) + // .attr('y', function ya(d) { + // if (d.index === indexA || d.index === indexB) { + // d.y = 5; + // } + + // return d.y; // NOTE d[i].y has been modified too. But, the bound value in the dataset + // // will not be updated until after the chain is completed, so it has to be explicitly defined here. + // }) + + // Switch places + // NOTE discuss chained transitions: http://bl.ocks.org/mbostock/1125997 + // TODO now that elements aren't cached, is there a need for .index? + .transition() + .duration(400) + .attr('x', function xa(d) { + if (d.index === indexA) { + d.index = indexB; + d.x = d.index * (d.w + 2); + } + else if (d.index === indexB) { + d.index = indexA; + d.x = d.index * (d.w + 2); + } + + return d.x; + }) + + // Move down + // .transition() + // .duration(100) + // .attr('y', function ya(d) { + // if (d.index === indexA || d.index === indexB) { + // d.y = 20; + // } + + // return d.y; + // }); +}; + +/** + * + */ +Visualizer.prototype.moveMarker = function(id, toIndex) { + this.svg.select('#' + id).attr('x', toIndex * 22); +}; + +/** + * + */ +Visualizer.prototype.addMarker = function(id) { + this.svg.append('text') + .attr('id', id) + .attr('x', 0) + .attr('y', 10) + .attr('width', 20) + .attr('height', 20) + .attr('fill', '#333') + .attr('font-size', 20) + .attr('alignment-baseline', 'middle') + .attr('text-anchor', 'middle') + .attr('transform', 'rotate(90 10,10)') + .text('P'); +}; + +/** + * + */ +Visualizer.prototype.fade = function(startIndex, endIndex) { + console.log(`fading from ${startIndex} to ${endIndex}`) + + this.svg.selectAll('rect') + // NOTE this replaces the fill function reference for each rectangle - key point! + .attr('fill', function fill(d) { + if (d.index >= startIndex && d.index <= endIndex) { + console.log(`${d.index} to ${d.fade}`) + return d.fade; + } + return d.fill; + }); +}; + +/** + * + */ +Visualizer.prototype.unfade = function() { + this.svg.selectAll('rect') + .attr('fill', function fill(d) { + console.log(`unfade ${d.index}`) + return d.fill; + }) +}; + +/** + * + */ +Visualizer.prototype.followInstruction = function(instructions, index) { + if (index >= instructions.length || index > 25) { + return; + } + + var i = instructions[index]; + console.log(i); + + if (i.operation === 'swap' && i.left < i.right) { + this.swap(i.left, i.right); + setTimeout(this.followInstruction.bind(this, instructions, index + 1), 400); + } + else if (i.operation === 'init') { + this.unfade(); + this.fade(0, i.left - 1); + this.fade(i.right + 1, 1000) + this.followInstruction(instructions, index + 1); + } + else if (i.operation === 'swap') { + this.followInstruction(instructions, index + 1); + } + else { + this.followInstruction(instructions, index + 1); + } +};