Visualizer improvements. Sort inheritance pattern built.

master
ben-burlingham 10 years ago
parent 83c367cdc5
commit 78078f1a65
  1. 17
      .eslintrc
  2. 6
      Gruntfile.js
  3. 2
      grunt/aliases.yaml
  4. 18
      grunt/concurrent.js
  5. 8
      grunt/eslint.js
  6. 18
      grunt/watch.js
  7. 151
      index.html
  8. 53
      js/mergesort.js
  9. 67
      js/quicksort.js
  10. 79
      js/sorter.js
  11. 150
      js/visualizer.js
  12. 6
      package.json
  13. 4
      readme.md

@ -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'

@ -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);
};

@ -1,2 +0,0 @@
default:
- 'concurrent:watch'

@ -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'
],
}
};

@ -1,8 +0,0 @@
/**
* ESLINT
* ======
* Does not support 'subtasks' (like :all).
*/
module.exports = {
target: ['js/*.js']
};

@ -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
},
},
};

@ -1,56 +1,137 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>D3</title> <title>D3</title>
<script type='text/javascript' src='vendor/d3/d3.min.js'></script> <link rel="stylesheet" href="vendor/fontawesome/css/font-awesome.css">
<style type='text/css'> <style type='text/css'>
html, body { html, body {
font-family:sans-serif;
margin:0; margin:0;
} }
.quicksort-random { svg {
display:block; border:10px solid dodgerblue;
height:60px; width:940px;
margin:0 auto; }
width:1198px;
.sorter {
border:1px solid #e7e7e7;
margin:20px auto;
padding:10px;
width:960px;
}
.slider {
}
.top {
background:#f88;
}
.bottom {
background:#4e0;
height:100px;
}
button {
border:1px solid #333;
background:#888;
padding:3px;
}
button.fa {
cursor:pointer;
font-size:12px;
height:20px;
line-height:14px;
margin-right:5px;
transition:background 0.2s ease;
width:20px;
}
button:hover {
background:#aaa;
} }
</style> </style>
</head> </head>
<body> <body>
<h1>Quicksort discussion</h1>
<div class="sorter"></div>
<h1>Mergesort discussion</h1>
<div class="sorter"></div>
<!-- <h1>Shellsort discussion</h1>
<div class="sorter"></div>
<h1>Heapsort discussion</h1>
<div class="sorter"></div>
<h1>Bubblesort discussion</h1>
<div class="sorter"></div>
<h1>Radixsort discussion</h1>
<div class="sorter"></div>
-->
<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.js'></script>
<script type='text/javascript' src='js/sorter.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>
<script type='text/javascript'> <script type='text/javascript'>
var data = []; // var QS = new Quicksort();
// QS.addInstruction();
// TODO namespace this
(function populate() { var MS = new Mergesort();
var len = 50;
var r, g, b; var data = MS.generate(15);
for (var i = 0; i < len; i++) { var a = []
data.push({ data.forEach(function(obj0) {
val: Math.floor(i * 255 / len), a.push(obj0.value);
fill: `rgb(0, 0, ${Math.floor(i * 255 / len)})`, })
fade: 'rgb(220, 220, 220)', console.log('DATA: ' + a.join(','));
});
}; var shuffled = MS.shuffle(data);
}()); var b = []
shuffled.forEach(function(obj0) {
var S = new Sorter(); b.push(obj0.value);
S.shuffle(data); });
console.log('SHUFFLED: ' + b.join(','));
var V = new Visualizer();
V.init(data); var ordered = Object.create(shuffled);
V.addMarker('quicksort-left-marker'); MS.sort(ordered, 0, ordered.length - 1);
V.moveMarker('quicksort-left-marker', 2); var c = []
ordered.forEach(function(obj0) {
var temp = Object.create(data); c.push(obj0.value);
S.quicksort(temp, 0, data.length - 1); });
temp = null; console.log('ORDERED: ' + c.join(','));
setTimeout(V.followInstruction.bind(V, S.instructions, 0), 500);
// Wrap anonymous function to avoid polluting global namespace.
// (function() {
// var elements = document.querySelectorAll('.sorter');
// for (key in elements) {
// if (elements.hasOwnProperty(key)) {
// new Visualizer(elements[key]);
// }
// }
// })();
</script> </script>
</body> </body>
</html> </html>

@ -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;
};

@ -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);
};

@ -2,78 +2,47 @@
* *
*/ */
var Sorter = function() {}; var Sorter = function() {};
Sorter.prototype.instructions = [];
// NOTE fisher-yates, http://bost.ocks.org/mike/algorithms/ // NOTE fisher-yates, http://bost.ocks.org/mike/algorithms/
/** /**
* * Returns copy of an array shuffled using Fisher-Yates.
*/ */
Sorter.prototype.shuffle = function(arr) { Sorter.prototype.shuffle = function(arr) {
var n = arr.length, t, i; var result = Object.create(arr);
var n = result.length, t, i;
while (n) { while (n) {
i = Math.random() * n-- | 0; // 0 ≤ i < n i = Math.random() * n-- | 0; // 0 ≤ i < n
t = arr[n]; t = result[n];
arr[n] = arr[i]; result[n] = result[i];
arr[i] = t; result[i] = t;
} }
return arr; return result;
}; };
/** /**
* *
*/ */
Sorter.prototype.addInstruction = function(operation, left, right, pivot) { Sorter.prototype.generate = function(n) {
this.instructions.push({ var arr = [];
operation: operation, for (var i = 0; i < n; i++) {
left: left, arr.push({
right: right, value: Math.floor(i * 255 / n),
pivot: pivot });
}); };
return arr;
}; };
// NOTE adds to an instruction set
/** /**
* *
*/ */
Sorter.prototype.quicksort = function(arr, start, end) { Sorter.prototype.sort = function(instruction) {
if (end - start <= 0) { throw new Error('Sorter.sort() method override required.');
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); *
*/
Sorter.prototype.addInstruction = function(instruction) {
throw new Error('Sorter.addInstruction() method override required.');
}; };

@ -1,37 +1,69 @@
/** /**
* *
*/ */
var Visualizer = function() {}; function Visualizer(parent) {
var top = document.createElement('div');
top.className = 'top';
parent.appendChild(top);
Visualizer.spacerW = 5; var bottom = document.createElement('div');
Visualizer.itemW = 14; bottom.className = 'bottom';
Visualizer.itemH = 50; parent.appendChild(bottom);
Visualizer.itemY = 20;
Visualizer.svg = null; var range = this.initRange();
Visualizer.groups = null; parent.querySelector('.bottom').appendChild(range);
Visualizer.instructions = null;
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. // A swap on the dataset will not take effect until after transition is complete, so custom index is required.
var n = 0; var n = 0;
for (i in data) { for (i in shuffled) {
data[i].index = n++; shuffled[i].index = n++;
} }
this.svg = d3.select('body').append('svg') var svg = d3.select(this.parent.querySelector('.top')).append('svg');
.attr('class', 'quicksort-random');
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})`); .attr('transform', `translate(0, ${Visualizer.itemY})`);
this.groups.append('rect') this.groups.append('rect')
.attr('height', Visualizer.itemH) .attr('height', Visualizer.itemH)
.attr('width', Visualizer.itemW) .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) this.groups.transition(500)
.attr('transform', function doTransform(d, i) { .attr('transform', function doTransform(d, i) {
@ -39,7 +71,7 @@ Visualizer.prototype.init = function(data) {
}); });
this.groups.append('text') 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('fill', '#aaa')
.attr('font-size', 10) .attr('font-size', 10)
.attr('font-family', 'sans-serif') .attr('font-family', 'sans-serif')
@ -47,9 +79,16 @@ Visualizer.prototype.init = function(data) {
return `rotate(90 0,0), translate(5, -3)`; 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('left-marker'); L
// this.svg.append('sometext').id('right-marker'); R // this.svg.append('sometext').id('right-marker'); R
// this.svg.append('sometext').id('pivot-marker'); triangle or arrow // 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 Two way binding here between dataset and function parameter?
// NOTE swapping will not reorder index and i parameter will be off // NOTE swapping will not reorder index and i parameter will be off
// NOTE discuss chained transitions: http://bl.ocks.org/mbostock/1125997 // 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 this.groups
.transition().duration(400) .transition().duration(400)
.attr('transform', function doTransform(d) { .attr('transform', function doTransform(d) {
@ -77,14 +115,14 @@ Visualizer.prototype.swap = function(indexA, indexB) {
/** /**
* *
*/ * /
Visualizer.prototype.moveMarker = function(id, toIndex) { Visualizer.prototype.moveMarker = function(id, toIndex) {
this.svg.select('#' + id).attr('x', toIndex * 22); this.svg.select('#' + id).attr('x', toIndex * 22);
}; };
/** /**
* *
*/ * /
Visualizer.prototype.addMarker = function(id) { Visualizer.prototype.addMarker = function(id) {
// this.svg.append('text') // this.svg.append('text')
// .attr('id', id) // .attr('id', id)
@ -156,3 +194,77 @@ Visualizer.prototype.followInstruction = function(instructions, index) {
this.followInstruction(instructions, index + 1); 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;
};

@ -1,7 +1,7 @@
{ {
"name": "ben.burlingham", "name": "d3-sort-visualization",
"version": "0.0.0", "version": "1.0.0",
"description": "", "description": "Sorting visualizations using the D3 library.",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"

@ -0,0 +1,4 @@
### Installation
1. Run `bower install` in root directory.
1. Open `index.html` in a browser.
Loading…
Cancel
Save