From 78078f1a654f1c4645585858f2928e2f79481101 Mon Sep 17 00:00:00 2001 From: ben-burlingham Date: Sun, 4 Oct 2015 10:10:17 -0700 Subject: [PATCH] Visualizer improvements. Sort inheritance pattern built. --- .eslintrc | 17 ----- Gruntfile.js | 6 -- grunt/aliases.yaml | 2 - grunt/concurrent.js | 18 ------ grunt/eslint.js | 8 --- grunt/watch.js | 18 ------ index.html | 151 ++++++++++++++++++++++++++++++++++---------- js/mergesort.js | 53 ++++++++++++++++ js/quicksort.js | 67 ++++++++++++++++++++ js/sorter.js | 79 +++++++---------------- js/visualizer.js | 150 +++++++++++++++++++++++++++++++++++++------ package.json | 6 +- readme.md | 4 ++ 13 files changed, 398 insertions(+), 181 deletions(-) delete mode 100644 .eslintrc delete mode 100644 Gruntfile.js delete mode 100644 grunt/aliases.yaml delete mode 100644 grunt/concurrent.js delete mode 100644 grunt/eslint.js delete mode 100644 grunt/watch.js create mode 100644 js/mergesort.js create mode 100644 js/quicksort.js create mode 100644 readme.md diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 296027c..0000000 --- a/.eslintrc +++ /dev/null @@ -1,17 +0,0 @@ -rules: - indent: - - 2 - - 4 - quotes: - - 2 - - single - linebreak-style: - - 2 - - unix - semi: - - 2 - - always -env: - es6: true - browser: true -extends: 'eslint:recommended' diff --git a/Gruntfile.js b/Gruntfile.js deleted file mode 100644 index 6be93b9..0000000 --- a/Gruntfile.js +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Debugging: try running "npm cache clear" if things aren't working as expected. - */ -module.exports = function(grunt) { - require('load-grunt-config')(grunt); -}; diff --git a/grunt/aliases.yaml b/grunt/aliases.yaml deleted file mode 100644 index 5f71f4f..0000000 --- a/grunt/aliases.yaml +++ /dev/null @@ -1,2 +0,0 @@ -default: - - 'concurrent:watch' diff --git a/grunt/concurrent.js b/grunt/concurrent.js deleted file mode 100644 index 7177d2c..0000000 --- a/grunt/concurrent.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * CONCURRENT - * ========== - * Runs several multiple blocking tasks at once (like "watch"). - * http://stackoverflow.com/questions/17585385/how-to-run-two-grunt-watch-tasks-simultaneously - */ - -module.exports = { - options: { - logConcurrentOutput: true, - }, - watch: { - tasks: [ - //'nodemon', - 'watch' - ], - } -}; diff --git a/grunt/eslint.js b/grunt/eslint.js deleted file mode 100644 index ba98c43..0000000 --- a/grunt/eslint.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * ESLINT - * ====== - * Does not support 'subtasks' (like :all). - */ -module.exports = { - target: ['js/*.js'] -}; diff --git a/grunt/watch.js b/grunt/watch.js deleted file mode 100644 index 3b66750..0000000 --- a/grunt/watch.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * WATCH - * ===== - * Process for watching changes to files in real time. - */ -module.exports = { - js: { - files: [ - 'js/**/*.js' - ], - tasks: [ - 'eslint' - ], - options: { - livereload: true - }, - }, -}; diff --git a/index.html b/index.html index 459cf23..075835e 100644 --- a/index.html +++ b/index.html @@ -1,56 +1,137 @@ - + D3 - + + +

Quicksort discussion

+ +
+ + +

Mergesort discussion

+ +
+ + + + + + + + diff --git a/js/mergesort.js b/js/mergesort.js new file mode 100644 index 0000000..83e2d6c --- /dev/null +++ b/js/mergesort.js @@ -0,0 +1,53 @@ +/** + * + */ +var Mergesort = function() { + this.instructions = []; +}; + +Mergesort.prototype = Object.create(Sorter.prototype); + +/** + * + */ +Mergesort.prototype.addInstruction = function(operation, left, right, mid) { + console.log({ + operation: operation, + left: left, + right: right, + mid: mid + }); + + this.instructions.push({ + operation: operation, + left: left, + right: right + }); +}; + +var count = 0; + +/** + * + */ +Mergesort.prototype.sort = function(arr, start, end) { + count++; + + if (count > 30) { + console.error('count exceeded'); + return; + } + + if (start >= end) { + this.addInstruction('single', start, end, null); + return; + } + + var mid = start + Math.floor((end - start) / 2); + // this.addInstruction('init', start, end, mid); + + + this.sort(arr, start, mid); + this.sort(arr, mid + 1, end); + return; +}; diff --git a/js/quicksort.js b/js/quicksort.js new file mode 100644 index 0000000..2ec869d --- /dev/null +++ b/js/quicksort.js @@ -0,0 +1,67 @@ +/** + * + */ +var Quicksort = function() { + this.instructions = []; +}; + +Quicksort.prototype = Object.create(Sorter.prototype); + +/** + * + */ +Quicksort.prototype.addInstruction = function(operation, left, right, pivot) { + this.instructions.push({ + operation: operation, + left: left, + right: right, + pivot: pivot + }); +}; + +// NOTE adds to an instruction set +/** + * + */ +Quicksort.prototype.sort = 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].value; + var tmp; + + this.addInstruction('init', left, right, pivot); + + while (left <= right) { + while (arr[left].value < pivotval) { + left++; + this.addInstruction('inc-left', left, right, pivot); + } + + while (arr[right].value > 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.sort(arr, start, right); + this.sort(arr, left, end); +}; diff --git a/js/sorter.js b/js/sorter.js index 1d80404..2277632 100644 --- a/js/sorter.js +++ b/js/sorter.js @@ -2,78 +2,47 @@ * */ var Sorter = function() {}; -Sorter.prototype.instructions = []; // NOTE fisher-yates, http://bost.ocks.org/mike/algorithms/ /** - * + * Returns copy of an array shuffled using Fisher-Yates. */ Sorter.prototype.shuffle = function(arr) { - var n = arr.length, t, i; + var result = Object.create(arr); + var n = result.length, t, i; while (n) { i = Math.random() * n-- | 0; // 0 ≤ i < n - t = arr[n]; - arr[n] = arr[i]; - arr[i] = t; + t = result[n]; + result[n] = result[i]; + result[i] = t; } - return arr; + return result; }; /** * */ -Sorter.prototype.addInstruction = function(operation, left, right, pivot) { - this.instructions.push({ - operation: operation, - left: left, - right: right, - pivot: pivot - }); +Sorter.prototype.generate = function(n) { + var arr = []; + for (var i = 0; i < n; i++) { + arr.push({ + value: Math.floor(i * 255 / n), + }); + }; + + return arr; }; -// 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); - } - } +Sorter.prototype.sort = function(instruction) { + throw new Error('Sorter.sort() method override required.'); +}; - this.quicksort(arr, start, right); - this.quicksort(arr, left, end); +/** + * + */ +Sorter.prototype.addInstruction = function(instruction) { + throw new Error('Sorter.addInstruction() method override required.'); }; diff --git a/js/visualizer.js b/js/visualizer.js index ddad908..c5743c9 100644 --- a/js/visualizer.js +++ b/js/visualizer.js @@ -1,37 +1,69 @@ /** * */ -var Visualizer = function() {}; +function Visualizer(parent) { + var top = document.createElement('div'); + top.className = 'top'; + parent.appendChild(top); -Visualizer.spacerW = 5; -Visualizer.itemW = 14; -Visualizer.itemH = 50; -Visualizer.itemY = 20; + var bottom = document.createElement('div'); + bottom.className = 'bottom'; + parent.appendChild(bottom); -Visualizer.svg = null; -Visualizer.groups = null; -Visualizer.instructions = null; + var range = this.initRange(); + parent.querySelector('.bottom').appendChild(range); + + var controls = this.initControls(); + parent.querySelector('.bottom').appendChild(controls); + + this.groups = null; + this.parent = parent; + this.init(10); + + // this.instructions = null; + // this.currentInstruction = 0; +}; + +// Public static properties (mutable) +Visualizer.spacerW = 5; +Visualizer.itemW = 14; +Visualizer.itemH = 50; +Visualizer.itemY = 20; /** * */ -Visualizer.prototype.init = function(data) { +Visualizer.prototype.init = function(n) { + // Sorter setup. + var QS = new Quicksort(); + var data = QS.generate(n); + var shuffled = QS.shuffle(data); + var ordered = Object.create(shuffled); + QS.sort(ordered, 0, ordered.length - 1); + + + var x = [] + ordered.forEach(function(obj0) { + x.push(obj0.value); + }) + + console.log(x.join(',')); + // 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++; + for (i in shuffled) { + shuffled[i].index = n++; } - this.svg = d3.select('body').append('svg') - .attr('class', 'quicksort-random'); + var svg = d3.select(this.parent.querySelector('.top')).append('svg'); - this.groups = this.svg.selectAll('g').data(data).enter().append('g') + this.groups = svg.selectAll('g').data(shuffled).enter().append('g') .attr('transform', `translate(0, ${Visualizer.itemY})`); this.groups.append('rect') .attr('height', Visualizer.itemH) .attr('width', Visualizer.itemW) - .attr('fill', function doFill(d) { return d.fill; }); + .attr('fill', function doFill(d) { return `rgb(0, 0, ${d.value})`; }); this.groups.transition(500) .attr('transform', function doTransform(d, i) { @@ -39,7 +71,7 @@ Visualizer.prototype.init = function(data) { }); this.groups.append('text') - .text(function t(d) { console.log(d.val); return d.val; }) + .text(function t(d) { return d.value; }) .attr('fill', '#aaa') .attr('font-size', 10) .attr('font-family', 'sans-serif') @@ -47,9 +79,16 @@ Visualizer.prototype.init = function(data) { return `rotate(90 0,0), translate(5, -3)`; }); + // V.addMarker('quicksort-left-marker'); + // V.moveMarker('quicksort-left-marker', 2); + // 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 + + + + setTimeout(this.followInstruction.bind(this, QS.instructions, 0), 500); }; /** @@ -60,7 +99,6 @@ Visualizer.prototype.swap = function(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 - // TODO now that elements aren't cached, is there a need for .index? this.groups .transition().duration(400) .attr('transform', function doTransform(d) { @@ -77,14 +115,14 @@ Visualizer.prototype.swap = function(indexA, indexB) { /** * - */ + * / 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) @@ -156,3 +194,77 @@ Visualizer.prototype.followInstruction = function(instructions, index) { this.followInstruction(instructions, index + 1); } }; + + +/** + * + */ +Visualizer.prototype.initRange = function(Visualizer) { + var container = document.createElement('div'); + container.className = 'range-container'; + + var label = document.createElement('div'); + label.innerHTML = "n: 40"; + + var range = document.createElement('input'); + range.setAttribute('type', 'range'); + range.setAttribute('value', 40); + range.setAttribute('min', 5); + range.setAttribute('max', 80); + range.addEventListener('change', this.rangeChange.bind(this)); + range.addEventListener('input', this.rangeInput.bind(null, label)); + + container.appendChild(range); + container.appendChild(label); + + return container; +}; + +/** + * + */ +Visualizer.prototype.rangeInput = function(label, event) { + label.innerHTML = 'Items: ' + event.target.value; +}; + +/** + * + */ +Visualizer.prototype.rangeChange = function(event) { + this.init(event.target.value); +}; + +/** + * + */ +Visualizer.prototype.initControls = function() { + var play = document.createElement('button'); + play.className = 'fa fa-play'; + play.title = 'Play' + play.addEventListener('click', onclick); + + var back = document.createElement('button'); + back.className = 'fa fa-step-backward'; + back.title = 'Step Backward'; + back.addEventListener('click', onclick); + + var forward = document.createElement('button'); + forward.className = 'fa fa-step-forward' + forward.title = 'Step Forward'; + forward.addEventListener('click', onclick); + + var reset = document.createElement('button'); + reset.className = 'fa fa-fast-backward'; + reset.title = 'Restart' + reset.addEventListener('click', onclick); + + var container = document.createElement('div'); + container.className = 'controls'; + + container.appendChild(reset); + container.appendChild(back); + container.appendChild(play); + container.appendChild(forward); + + return container; +}; diff --git a/package.json b/package.json index 4e9a545..e38ed54 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "ben.burlingham", - "version": "0.0.0", - "description": "", + "name": "d3-sort-visualization", + "version": "1.0.0", + "description": "Sorting visualizations using the D3 library.", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..2358df0 --- /dev/null +++ b/readme.md @@ -0,0 +1,4 @@ +### Installation + +1. Run `bower install` in root directory. +1. Open `index.html` in a browser.