Created
January 11, 2015 19:13
-
-
Save kmxz/8a10690effc5656f8e7e to your computer and use it in GitHub Desktop.
Naive function plotter
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<html> | |
<head> | |
<link rel="stylesheet" type="text/css" href="http://bootswatch.com/paper/bootstrap.min.css" /> | |
<style type="text/css"> | |
.template { | |
display: none; | |
} | |
</style> | |
<script type="text/javascript"> | |
var add, addListeners, conf, getColor, getGray, init, initconfAndSetListeners, mathWrapX, maxOfArray, minOfArray, randShift, reAssign, reRender, toArray; | |
toArray = function(input) { | |
return Array.prototype.slice.call(input); | |
}; | |
conf = {}; | |
randShift = Math.random(); | |
mathWrapX = function(exp) { | |
return new Function("x", "with(Math) { return (" + exp + ");}"); | |
}; | |
getColor = function(angle) { | |
return "hsl(" + ((angle + randShift % 1) * 360) + ", 80%, 50%)"; | |
}; | |
getGray = function(shade) { | |
return "hsl(" + randShift + ", 0%, " + 100 * shade + "%)"; | |
}; | |
reAssign = function() { | |
var i, te, valid; | |
valid = toArray(document.querySelectorAll(".entry")).filter(function(te) { | |
if (!te.querySelector("input").value.trim().length) { | |
te.parentNode.removeChild(te); | |
return false; | |
} | |
return true; | |
}); | |
for (i = 0; i < valid.length; i++) { | |
te = valid[i]; | |
te.querySelector(".input-group-addon").style.color = getColor(i / valid.length); | |
} | |
add(); | |
console.log("Reassigned..."); | |
}; | |
maxOfArray = function(arr) { | |
return arr.reduce((function(a, b) { | |
return Math.max(a, b); | |
}), Number.NEGATIVE_INFINITY); | |
}; | |
minOfArray = function(arr) { | |
return arr.reduce((function(a, b) { | |
return Math.min(a, b); | |
}), Number.POSITIVE_INFINITY); | |
}; | |
reRender = function() { | |
var allEntries, allFunc, canvas, ctx, i, i2x, j, labelPos, labelText, lastError, max, min, oldCanvas, padding, realHeight, realWidth, xMarks, xs, y2j, yMarks, ys; | |
lastError = null; | |
padding = conf["padding"]; | |
canvas = document.createElement("canvas"); | |
canvas.width = conf["width"]; | |
canvas.height = conf["height"]; | |
realWidth = conf["width"] - padding * 2; | |
realHeight = conf["height"] - padding * 2; | |
allEntries = toArray(document.querySelectorAll(".entry")); | |
allEntries.pop(); | |
allFunc = allEntries.map(function(te) { | |
return mathWrapX(te.querySelector("input").value); | |
}); | |
ctx = canvas.getContext("2d"); | |
xs = []; | |
i2x = function(i) { | |
return i * (conf["max"] - conf["min"]) / realWidth + conf["min"]; | |
}; | |
for (i = 0; i < realWidth; i++) { | |
xs.push(i2x(i)); | |
} | |
ys = xs.map(function(x) { | |
return allFunc.map(function(func) { | |
var y; | |
y = 0; | |
try { | |
y = func(x); | |
} catch (_) { | |
lastError = "Parse Error!"; | |
} | |
if (isNaN(y) || !isFinite(y)) { | |
lastError = "Illegal return value " + y; | |
} | |
return y; | |
}); | |
}); | |
max = maxOfArray(ys.map(maxOfArray)); | |
min = minOfArray(ys.map(minOfArray)); | |
y2j = function(y) { | |
return realHeight * (max - y) / (max - min) + padding; | |
}; | |
ctx.font = "normal normal 12px sans-serif"; | |
ctx.fillStyle = getGray(0.5); | |
ctx.textBaseline = "bottom"; | |
ctx.textAlign = "right"; | |
ctx.strokeStyle = getGray(0.9); | |
yMarks = Math.floor(realHeight / padding / 1.5); | |
for (i = 0; i <= yMarks; i++) { | |
labelText = (i / yMarks) * (max - min) + min; | |
labelPos = y2j(labelText); | |
ctx.fillText(labelText.toPrecision(3), padding, labelPos, padding); | |
ctx.beginPath(); | |
ctx.moveTo(padding, labelPos); | |
ctx.lineTo(realWidth + padding, labelPos); | |
ctx.stroke(); | |
} | |
ctx.textBaseline = "top"; | |
ctx.textAlign = "left"; | |
xMarks = Math.floor(realWidth / padding / 1.5); | |
for (i = 0; i <= xMarks; i++) { | |
labelPos = i / xMarks * realWidth; | |
labelText = i2x(labelPos); | |
labelPos += padding; | |
ctx.fillText(labelText.toPrecision(3), labelPos, padding + realHeight, padding); | |
ctx.beginPath(); | |
ctx.moveTo(labelPos, padding + realHeight); | |
ctx.lineTo(labelPos, padding); | |
ctx.stroke(); | |
} | |
for (j = 0; j < allFunc.length; j++) { | |
ctx.strokeStyle = getColor(j / allFunc.length); | |
ctx.beginPath(); | |
ctx.moveTo(padding, y2j(ys[0][j])); | |
for (i = 1; i < realWidth; i++) { | |
ctx.lineTo(i + padding, y2j(ys[i][j])); | |
} | |
ctx.stroke(); | |
} | |
if (lastError) { | |
ctx.font = "normal normal 24px sans-serif"; | |
ctx.fillStyle = "#000"; | |
ctx.textBaseline = "middle"; | |
ctx.textAlign = "center"; | |
ctx.fillText(lastError, conf["width"] / 2, conf["height"] / 2); | |
} | |
oldCanvas = document.querySelector("canvas"); | |
oldCanvas.parentNode.replaceChild(canvas, oldCanvas); | |
console.log("Rerendered..."); | |
}; | |
addListeners = function(container) { | |
input = container.querySelector("input"); | |
input.innerHTML = ''; | |
input.addEventListener("keyup", reAssign); | |
input.addEventListener("change", reRender); | |
}; | |
add = function() { | |
var cloned, e; | |
e = document.querySelector(".template"); | |
cloned = e.cloneNode(true); | |
cloned.classList.remove('template'); | |
cloned.classList.add('entry'); | |
addListeners(cloned); | |
e.parentNode.appendChild(cloned); | |
return cloned; | |
}; | |
initconfAndSetListeners = function(key) { | |
var el; | |
el = document.getElementById(key); | |
conf[key] = parseFloat(el.value); | |
el.addEventListener("change", function() { | |
var evaled; | |
evaled = parseFloat(el.value); | |
if (!isNaN(evaled)) { | |
conf[key] = evaled; | |
reRender(); | |
} else { | |
el.value = conf[key]; | |
} | |
}); | |
}; | |
init = function() { | |
add().querySelector('input').value = 'x * sin(x)'; | |
add().querySelector('input').value = 'x > 2 * PI ? x * cos(x) : x'; | |
initconfAndSetListeners("width"); | |
initconfAndSetListeners("height"); | |
initconfAndSetListeners("min"); | |
initconfAndSetListeners("max"); | |
initconfAndSetListeners("padding"); | |
reAssign(); | |
reRender(); | |
document.querySelector(".container").style.display = "block"; | |
console.log("Initialized..."); | |
}; | |
document.addEventListener("DOMContentLoaded", init); | |
</script> | |
<script type="text/javascript" src="http://coffeescript.org/extras/coffee-script.js"></script> | |
</head> | |
<body> | |
<div class="container" style="display: none"> | |
<div class="page-header"> | |
<h1>Naive function plotter</h1> | |
</div> | |
<div class="thumbnail"> | |
<canvas></canvas> | |
</div> | |
<div class="panel panel-default"> | |
<div class="panel-heading">Range</div> | |
<div class="panel-body"> | |
<div class="input-group"> | |
<span class="input-group-addon">minimum: </span> | |
<input type="text" class="form-control" value="0" id="min" /> | |
</div> | |
<div class="input-group"> | |
<span class="input-group-addon">maximum: </span> | |
<input type="text" class="form-control" value="10" id="max" /> | |
</div> | |
<div class="input-group"> | |
<span class="input-group-addon">width: </span> | |
<input type="text" class="form-control" value="640" id="width" /> | |
</div> | |
<div class="input-group"> | |
<span class="input-group-addon">height: </span> | |
<input type="text" class="form-control" value="480" id="height" /> | |
</div> | |
<div class="input-group"> | |
<span class="input-group-addon">padding: </span> | |
<input type="text" class="form-control" value="40" id="padding" /> | |
</div> | |
</div> | |
<div class="panel-footer">Dimensions include paddings.</div> | |
</div> | |
<div class="panel panel-default"> | |
<div class="panel-heading">Entries</div> | |
<div class="panel-body"> | |
<div class="input-group template"> | |
<span class="input-group-addon">function of x: </span> | |
<input type="text" class="form-control" /> | |
</div> | |
</div> | |
</div> | |
</div> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment