|
var S = 'ATGATGTAAGATGTTACATGAAAAC'; |
|
var stringSizeLimit = 25; |
|
var charSize = 20; |
|
var jobQueue = new Array(); |
|
var worker; |
|
|
|
var suffixArray = new Array(); |
|
for (var i = 0; i < S.length; ++i) suffixArray.push(i); |
|
|
|
var canvas = d3.select('#visualization'); |
|
var suffixArrayDiv = new SuffixArrayDiv(canvas, S); |
|
var suffixSorter = new SuffixSorter(S); |
|
|
|
var controls = canvas |
|
.append('div') |
|
.attr('id', 'controls'); |
|
var stringChanger = controls.append('fieldset'); |
|
stringChanger.append('label') |
|
.text('String: ') |
|
.attr('for', 'string-input'); |
|
stringChanger.append('input') |
|
.attr('id', 'string-input') |
|
.attr('value', S); |
|
stringChanger.append('br') |
|
stringChanger.append('button') |
|
.attr('type', 'button') |
|
.text('Change String') |
|
.on('click', function() { |
|
var newS = document.getElementById('string-input').value; |
|
if (newS.length < 1) { |
|
stringChanger.select('#error-message') |
|
.text('String must not be empty.'); |
|
} else if (newS.length > stringSizeLimit) { |
|
stringChanger.select('#error-message') |
|
.text('String must be at most ' + stringSizeLimit + ' characters.'); |
|
} else { |
|
stringChanger.select('#error-message').text(''); |
|
S = newS; |
|
suffixArray = new Array(); |
|
for (var i = 0; i < S.length; ++i) suffixArray.push(i); |
|
suffixArrayDiv.destroy(); |
|
suffixArrayDiv = new SuffixArrayDiv(canvas, S); |
|
suffixSorter.reset(S); |
|
setCharactersSorted(0); |
|
} |
|
}); |
|
stringChanger.append('br') |
|
stringChanger.append('span') |
|
.attr('id', 'error-message'); |
|
|
|
var sorter = controls.append('div').attr('id', 'sorter'); |
|
sorter.append('button') |
|
.attr('type', 'button') |
|
.text('Sort') |
|
.on('click', function() { |
|
quicksort(suffixArray, 0, S.length, suffixSorter.compare.bind(suffixSorter), |
|
function(i, j) { |
|
submitJob(suffixArrayDiv.swapColumns, suffixArrayDiv, [i, j]); |
|
}); |
|
suffixSorter.updateRank(suffixArray); |
|
var previousCharactersSorted = suffixSorter.charactersSorted; |
|
var charactersSorted = suffixSorter.incrementCharactersSorted(); |
|
for (var i = previousCharactersSorted; i < charactersSorted; ++i) { |
|
submitJob(suffixArrayDiv.colorRow, suffixArrayDiv, [i, '#e41a1c']); |
|
} |
|
setCharactersSorted(charactersSorted); |
|
}); |
|
sorter.append('span').text('Characters sorted: '); |
|
sorter.append('span').attr('id', 'characters-sorted').text(0); |
|
|
|
|
|
|
|
|
|
|
|
function SuffixSorter(S) { |
|
this.rank = new Array(); |
|
this.charactersSorted = 0; |
|
this.S = S; |
|
for (var i = 0; i < S.length; ++i) this.rank.push(0); |
|
|
|
this.compare = function(a, b) { |
|
if (this.charactersSorted === 0) return this.S[a] < this.S[b]; |
|
if (this.rank[a] !== this.rank[b]) return this.rank[a] < this.rank[b]; |
|
if (a + this.charactersSorted === this.S.length) return true; |
|
if (b + this.charactersSorted === this.S.length) return false; |
|
return this.rank[a + this.charactersSorted] < this.rank[b + this.charactersSorted]; |
|
} |
|
|
|
this.updateRank = function(arr) { |
|
var newRank = new Array(arr.length); |
|
var currentRank = 0; |
|
newRank[arr[0]] = currentRank; |
|
for (var i = 1; i < arr.length; ++i) { |
|
if (this.compare(arr[i-1], arr[i])) ++currentRank; |
|
newRank[arr[i]] = currentRank; |
|
} |
|
this.rank = newRank; |
|
} |
|
|
|
this.incrementCharactersSorted = function() { |
|
if (this.charactersSorted === 0) { |
|
this.charactersSorted = 1; |
|
} else { |
|
this.charactersSorted *= 2; |
|
} |
|
this.charactersSorted = Math.min(this.charactersSorted, S.length); |
|
return this.charactersSorted; |
|
} |
|
|
|
this.reset = function(S) { |
|
this.rank = new Array(S.length); |
|
for (var i = 0; i < S.length; ++i) this.rank.push(0); |
|
this.charactersSorted = 0; |
|
this.S = S; |
|
} |
|
} |
|
|
|
function setCharactersSorted(charactersSorted) { |
|
sorter.select('#characters-sorted') |
|
.transition().duration(1000).tween('text', function() { |
|
var interpolater = d3.interpolateRound(parseInt(this.textContent), charactersSorted); |
|
return function(t) { |
|
this.textContent = interpolater(t); |
|
} |
|
}); |
|
} |
|
|
|
function submitJob(f, thisArg, args) { |
|
jobQueue.push([f, thisArg, args]); |
|
startWorker(); |
|
} |
|
|
|
function startWorker() { |
|
if (worker === undefined) { |
|
worker = setInterval(function() { |
|
if (jobQueue.length) { |
|
var job = jobQueue.shift(); |
|
job[0].apply(job[1], job[2]); |
|
} else { |
|
clearInterval(worker); |
|
worker = undefined; |
|
} |
|
}, 250); |
|
} |
|
} |
|
|
|
function swap(arr, i, j) { |
|
var tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; |
|
} |
|
|
|
function quicksort(arr, i, j, comparator, swapTrigger) { |
|
if (j - i <= 1) return; |
|
// median of 3 method to select pivot |
|
var mid = Math.floor((i + j)/2); |
|
if (comparator(arr[i], arr[mid]) && comparator(arr[mid], arr[j - 1])) { |
|
swap(arr, mid, j - 1); |
|
if (swapTrigger) swapTrigger(mid, j - 1); |
|
} else if (comparator(arr[mid], arr[i]) && comparator(arr[i], arr[j - 1])) { |
|
swap(arr, i, j - 1); |
|
if (swapTrigger) swapTrigger(i, j - 1); |
|
} |
|
var pivot = j - 1; |
|
|
|
var cursor = i; |
|
for (var k = i; k < j - 1; ++k) { |
|
if (comparator(arr[k], arr[pivot])) { |
|
if (cursor != k) { |
|
swap(arr, k, cursor); |
|
if (swapTrigger) swapTrigger(k, cursor); |
|
} |
|
++cursor; |
|
} |
|
} |
|
if (pivot != cursor) { |
|
swap(arr, pivot, cursor); |
|
if (swapTrigger) swapTrigger(pivot, cursor); |
|
} |
|
quicksort(arr, i, cursor, comparator, swapTrigger); |
|
quicksort(arr, cursor + 1, j, comparator, swapTrigger); |
|
} |
|
|
|
function SuffixArrayDiv(parent, S) { |
|
this.parent = parent; |
|
this.suffixArrayDiv = parent.insert('div', ':first-child') |
|
.attr('class', 'suffix-array-div') |
|
.style('position', 'absolute') |
|
.style('left', 2*(960 - S.length*charSize)/3 + 'px'); |
|
this.suffixDivs = new Array(); var suffixDivs = this.suffixDivs; |
|
|
|
this.suffixArrayDiv.selectAll('.suffix') |
|
.data(S).enter() |
|
.append('div') |
|
.attr('class', 'suffix') |
|
.style('left', function(d, i) { return charSize*i + 'px'; }) |
|
.style('opacity', 0) |
|
.each(function(d, i) { |
|
suffixDivs.push(d3.select(this)); |
|
var suffixSvg = d3.select(this).append('svg') |
|
.attr('width', charSize) |
|
.attr('height', S.length*charSize); |
|
suffixSvg.selectAll('.char') |
|
.data(S.substring(i)) |
|
.enter() |
|
.append('text') |
|
.attr('class', function(d, i) { |
|
return 'char ' + 'row' + i; |
|
}) |
|
.attr('x', charSize/2) |
|
.attr('y', function(dd, j) { return (j+1)*charSize; }) |
|
.text(function(d) { return d; }); |
|
}) |
|
.transition().duration(1000) |
|
.style('opacity', 1); |
|
|
|
this.colorRow = function(i, color) { |
|
var chars = this.suffixArrayDiv.selectAll('.row' + i); |
|
chars.transition().duration(1000).attr('fill', color); |
|
} |
|
|
|
this.swapColumns = function(i, j) { |
|
suffixDivs[i] |
|
.transition() |
|
.styleTween('left', function(d, idx, initial) { |
|
var end = j*charSize + 'px'; |
|
return d3.interpolateString(initial, end); |
|
}); |
|
suffixDivs[j].transition() |
|
.styleTween('left', function(d, idx, initial) { |
|
var end = i*charSize + 'px'; |
|
return d3.interpolateString(initial, end); |
|
}); |
|
var tmp = suffixDivs[i]; |
|
suffixDivs[i] = suffixDivs[j]; |
|
suffixDivs[j] = tmp; |
|
} |
|
|
|
this.destroy = function() { |
|
this.suffixArrayDiv.transition().duration(1000).style('opacity', 0).remove(); |
|
} |
|
} |
|
|