|
<!DOCTYPE html> |
|
<meta charset="utf-8"> |
|
|
|
<html> |
|
|
|
<head> |
|
<title>Building a Normal Distribution Histogram</title> |
|
|
|
<script src="https://use.fontawesome.com/35aa61260c.js"></script> |
|
<script src="http://d3js.org/d3.v4.min.js"></script> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jstat/1.7.0/jstat.js"></script> |
|
<script src="https://unpkg.com/simple-statistics@4.1.0/dist/simple-statistics.min.js"></script> |
|
|
|
<style> |
|
body { |
|
/*background-color: rgba(245,243,242,1);*/ |
|
font-family: "Helvetica", sans-serif; |
|
margin: 8px; |
|
} |
|
|
|
svg { |
|
background-color: white; |
|
/*border: solid 1px rgba(208,199,198,1);*/ |
|
} |
|
|
|
.axis { |
|
font: 10px sans-serif; |
|
} |
|
|
|
.axis path, |
|
.axis line { |
|
fill: none; |
|
stroke: #000; |
|
shape-rendering: crispEdges; |
|
} |
|
|
|
text.axis.label { |
|
fill: #000; |
|
} |
|
|
|
.bar { |
|
fill: rgba(30, 80, 230, 1); |
|
shape-rendering: crispEdges; |
|
opacity: 0.9; |
|
} |
|
|
|
text.bar.label { |
|
fill: white; |
|
text-anchor: middle; |
|
} |
|
|
|
.interactive { |
|
transition: all 0.2s ease 0s; |
|
user-select: none; |
|
cursor: pointer; |
|
} |
|
|
|
.interactive:hover { |
|
fill: #888888; |
|
} |
|
|
|
.btn { |
|
font-family: 'FontAwesome'; |
|
font-size: 15px; |
|
text-anchor: end; |
|
letter-spacing: 10px |
|
} |
|
</style> |
|
|
|
<body> |
|
|
|
<script> |
|
(function () { |
|
|
|
var margin = { |
|
top: 20, |
|
right: 20, |
|
bottom: 40, |
|
left: 40 |
|
}, |
|
width = 950 - margin.left - margin.right, |
|
height = 500 - margin.top - margin.bottom; |
|
|
|
|
|
////////////////////////// |
|
// Generate data // |
|
////////////////////////// |
|
|
|
var normalMean = 0, |
|
normalStdDev = 3; |
|
|
|
function update_xValues() { |
|
xSteps = normalStdDev * 4 * 2; // We want to generate 4 standard deviations to each side of the mean |
|
xDomain = [Math.floor(normalMean - xSteps / 2), Math.ceil(normalMean + xSteps / 2)]; |
|
} |
|
update_xValues(); |
|
|
|
|
|
var autoIterate = false, |
|
iteration = 0, |
|
i_step = 1, |
|
numDataPoints = 100, |
|
numDataPointsMax = 100000; //For memory purposes when scrolling |
|
|
|
var updateDelay = 500; |
|
var transitionDuration = updateDelay * 0.8; |
|
|
|
// Data |
|
function update_dataPoints(adjust) { |
|
let rand = function () { |
|
var a = d3.randomNormal(normalMean, normalStdDev); |
|
return Math.round(a()); |
|
} |
|
if (adjust == true) { |
|
var new_length = numDataPoints - dataPoints.length; |
|
if (new_length > 0) { |
|
// Add additional points |
|
let additional = d3.range(new_length).map(rand); |
|
return dataPoints.concat(additional); |
|
} else if (new_length <= 0) { |
|
// Reduce points |
|
iteration = Math.min(iteration, numDataPoints); //Iteration should not be larger than total |
|
return dataPoints.slice(0, numDataPoints); |
|
} |
|
} else { |
|
// New set of data points: |
|
return d3.range(numDataPoints).map(rand); |
|
} |
|
} |
|
var dataPoints = update_dataPoints(); |
|
|
|
// Initial histogram |
|
function update_dataHist(data) { |
|
return d3.histogram() |
|
.domain(xDomain) |
|
.thresholds(21) |
|
(data); |
|
} |
|
var dataHist = update_dataHist(dataPoints); |
|
|
|
|
|
////////////////////////////////// |
|
// Build the barchart // |
|
////////////////////////////////// |
|
|
|
// Draw SVG |
|
var svg = d3.select("body").append("svg") |
|
.attr("width", width + margin.left + margin.right) |
|
.attr("height", height + margin.top + margin.bottom) |
|
.append("g") |
|
.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); |
|
|
|
// X scale (Continous) step generator for generating even spaced ticks |
|
// Neccesary for continous precision of descriptives |
|
var xStep = d3.scaleLinear() |
|
.domain(xDomain); |
|
|
|
// X scale (Ordinal) |
|
var x = d3.scaleBand() |
|
.domain(d3.ticks(xDomain[0], xDomain[1], 21)) |
|
.rangeRound([0, width]) |
|
.paddingInner(0.15); |
|
|
|
// Recalibrate the Continous X scale to fit the Ordinal X scale endpoints |
|
xStep.range([(x(xDomain[0]) + (x.bandwidth() / 2)), (x(xDomain[1]) + (x.bandwidth() / 2))]); |
|
|
|
// X axis |
|
var xAxis = d3.axisBottom(x); |
|
|
|
var xAxisGroup = svg.append("g") |
|
.attr("class", "x axis") |
|
.attr("transform", "translate(0," + height + ")") |
|
.call(xAxis); |
|
|
|
// Y scale |
|
var y = d3.scaleLinear() |
|
.range([height, 0]) |
|
.domain([0, d3.max(dataHist, function (d) { |
|
return Math.ceil(d.length / 10) * 10 |
|
})]); |
|
|
|
// Y axis |
|
var yAxis = d3.axisLeft() |
|
.scale(y); |
|
|
|
var yAxisGroup = svg.append("g") |
|
.attr("class", "y axis") |
|
.call(yAxis); |
|
|
|
// Y Label |
|
yAxisGroup |
|
.call(yAxis) |
|
.append("text") |
|
.attr("class", "axis label") |
|
.attr("transform", "rotate(-90)") |
|
.attr("y", 6) |
|
.attr("dy", ".71em") |
|
.style("text-anchor", "end") |
|
.text("Frequency"); |
|
|
|
|
|
////////////////////////////////////////// |
|
// Bars and labels within the plot area // |
|
////////////////////////////////////////// |
|
|
|
var barGroup = svg.append("g") |
|
.attr("class", "bars") |
|
|
|
var barGroupEntries = barGroup.selectAll("g") |
|
.data(dataHist); |
|
|
|
var barGroupEnter = barGroupEntries.enter() |
|
.append("g") |
|
.attr("class", "entry"); |
|
|
|
// Draw initial bars |
|
var barRects = barGroupEnter.append("rect") |
|
.attr("class", "bar") |
|
.attr("stroke", "1") |
|
.attr("width", x.bandwidth()) |
|
.attr("transform", function (d) { |
|
return "translate(" + x(d.x0) + |
|
"," + y(0) + ")"; |
|
}) |
|
.attr("height", function (d) { |
|
return height - y(0); |
|
}); |
|
|
|
// Add bar labels |
|
var barLabels = barGroupEnter.append("text") |
|
.attr("class", "bar label") |
|
.text(function (d) { |
|
return ""; |
|
}) // Start out with blank labels |
|
.attr("text-anchor", "middle") |
|
.attr("transform", function (d) { |
|
return "translate(" + (x(d.x0) + x.bandwidth() / 2) + |
|
"," + (y(0) + 16) + ")"; |
|
}); |
|
|
|
|
|
//////////////////////////////////////////// |
|
// Descriptives: // |
|
// Mean, median, standard deviation, etc. // |
|
//////////////////////////////////////////// |
|
|
|
var desciptiveData = []; |
|
// Colors for each descriptive line and label |
|
var colors10 = d3.scaleOrdinal(d3.schemeCategory10); |
|
|
|
function update_descriptiveData(data) { |
|
|
|
// See js statistic libraries benchmarks here: |
|
// https://github.com/simple-statistics/simple-statistics/tree/master/benchmarks |
|
var desciptiveFunctions = [{ |
|
id: 1, |
|
type: "central_tendency", |
|
name: "Median", |
|
fun: ss.median |
|
}, // 20.1 times faster than d3.median |
|
{ |
|
id: 2, |
|
type: "central_tendency", |
|
name: "Mean", |
|
fun: jStat.mean |
|
}, // 1.6 times faster than d3.mean |
|
{ |
|
id: 3, |
|
type: "spread", |
|
name: "SD", |
|
fun: x => { |
|
return jStat.stdev(x, true); |
|
} |
|
}, // 6.67 times faster than d3.deviation |
|
{ |
|
id: 4, |
|
type: "spread", |
|
name: "SEM", |
|
fun: x => { |
|
return jStat.stdev(x, true) / Math.sqrt(x.length); |
|
} |
|
}, |
|
{ |
|
id: 5, |
|
type: "spread", |
|
name: "CI", |
|
fun: x => { |
|
// For the CI we centralize the value which we later adjust with the mean: |
|
return jStat.tci(0, 0.05, x)[1]; |
|
} |
|
} // See documentation: http://jstat.github.io/all.html#jStat.tci |
|
]; |
|
// ES2015 anonymous function(arg) mapped over each function returns named dict |
|
desciptiveData = desciptiveFunctions.map(arg => ({ |
|
id: arg.id, |
|
type: arg.type, |
|
name: arg.name, |
|
val: arg.fun(data) |
|
})); |
|
} |
|
// Set the standard (not neccesary) |
|
desciptiveData = [{ |
|
id: 1, |
|
type: "central_tendency", |
|
name: "Median", |
|
val: normalMean |
|
}, |
|
{ |
|
id: 2, |
|
type: "central_tendency", |
|
name: "Mean", |
|
val: normalMean |
|
}, |
|
{ |
|
id: 3, |
|
type: "spread", |
|
name: "SD", |
|
val: normalStdDev |
|
}, |
|
{ |
|
id: 4, |
|
type: "spread", |
|
name: "SEM", |
|
val: normalStdDev |
|
}, |
|
{ |
|
id: 5, |
|
type: "spread", |
|
name: "CI", |
|
val: normalStdDev * 1.96 |
|
} // Temp 95% CI |
|
]; |
|
|
|
// We insert instead of append to make it appear behind the bars. |
|
var descGroup = svg.insert("g", ":first-child") |
|
.attr("class", "descriptives") |
|
.selectAll("g") |
|
.data(desciptiveData) |
|
.enter().append("g") |
|
.attr("class", "entry") |
|
.attr("id", function (d) { |
|
return d.name; |
|
}) |
|
.style("opacity", 0); |
|
|
|
// Add descriptive lines |
|
var descLines = descGroup.filter(function (d) { |
|
return d.type == "central_tendency"; |
|
}).append("line") |
|
.attr("stroke", function (d) { |
|
return colors10(d.id); |
|
}) |
|
.attr('x1', function (d) { |
|
return xStep(d.val); |
|
}) // Start all central tendencies at the mean, initially 0 |
|
.attr('x2', function (d) { |
|
return xStep(d.val); |
|
}) |
|
.attr('y1', 0) |
|
.attr('y2', height); |
|
|
|
// Add descriptive spreads |
|
var descAreas = descGroup.filter(function (d) { |
|
return d.type == "spread"; |
|
}).append("rect") |
|
.attr("fill", function (d) { |
|
return colors10(d.id); |
|
}) |
|
.attr("opacity", 0.1) |
|
.attr('x', function (d) { |
|
return xStep(normalMean - d.val); |
|
}) // Start all central tendencies at the mean, initially 0 |
|
.attr('width', function (d) { |
|
return ((xStep(d.val) - xStep(normalMean)) * 2); |
|
}) |
|
.attr('y', 0) |
|
.attr('height', height); |
|
|
|
// Add labels for each descriptives |
|
var descLabels = descGroup.append("text") |
|
.attr("class", "line label") |
|
.style("fill", function (d) { |
|
return colors10(d.id); |
|
}) |
|
.style("font-size", "150%") |
|
.attr("text-anchor", "end") |
|
.attr("dy", "-2px") |
|
.text(function (d) { |
|
return d.name; |
|
}) |
|
.attr("transform", function (d) { |
|
return "translate(" + xStep(d.val) + "," + 0 + ") rotate(-90)"; |
|
}) |
|
.on('mouseover', function (d) { |
|
d3.select(".descriptives #" + d.name) |
|
.transition() |
|
.style("opacity", visible ? 0 : 1) |
|
}) |
|
.on('mouseout', function (d) { |
|
d3.select(this).style({ |
|
opacity: '0.0', |
|
}) |
|
d3.select("text").style({ |
|
opacity: '0.0' |
|
}); |
|
}); |
|
|
|
|
|
/////////////////////////////////////////////// |
|
// Upper right controls and legend |
|
/////////////////////////////////////////////// |
|
var controls = svg.append("g") |
|
.attr("class", "controls"); |
|
|
|
// Add descriptives legend |
|
var legend = controls.append("g") |
|
.attr("class", "legend") |
|
.attr("text-anchor", "end") |
|
.selectAll("g") |
|
.data(desciptiveData) |
|
.enter().append("g") |
|
.attr("transform", function (d, i) { |
|
return "translate(0," + (40 + i * 20) + ")"; |
|
}); |
|
|
|
legend.append("text") |
|
.attr("class", "btnToggleDescriptive interactive") |
|
.attr("x", width) |
|
.attr("y", 14) |
|
.style("text-anchor", "end") |
|
.style('font-family', 'FontAwesome') |
|
.style('font-size', '15px') |
|
.text("\uf0c8") |
|
.on("click", function (d, i) { |
|
// Toggle on/off descriptive |
|
var visible = d3.select(this).classed("visible"); |
|
d3.select(this) |
|
.classed("visible", !visible) |
|
.text(visible ? "\uf0c8" : "\uf14a"); // Unchecked box |
|
d3.select(".descriptives #" + d.name) |
|
.transition() |
|
.style("opacity", visible ? 0 : 1) |
|
}); |
|
|
|
legend.append("text") |
|
.attr("x", width - 18) |
|
.attr("y", 9.5) |
|
.attr("dy", "0.32em") |
|
.text(function (d) { |
|
return d.name; |
|
}) |
|
.style("color", function (d) { |
|
return colors10(d.id); |
|
}); |
|
|
|
|
|
///////////////////////////// |
|
// Controls top right area // |
|
///////////////////////////// |
|
|
|
// Draw walker number group, iteration, and total |
|
var walkNum = controls.append("text") |
|
.attr("class", "walknum") |
|
.style("text-anchor", "end") |
|
.attr("x", width) |
|
.attr("y", 4); |
|
var walkNumCurrent = walkNum.append("tspan") |
|
.attr("class", "walkNumCurrent interactive") |
|
.text("0 / ") |
|
.on("wheel", scrollN); |
|
var walkNumTot = walkNum.append("tspan") |
|
.attr("class", "walkNumTotal interactive") |
|
.style("text-anchor", "end") |
|
.text("100") |
|
.on("wheel", scrollTot); |
|
|
|
|
|
|
|
// Draw font awesome control buttons |
|
// Play/Pause button |
|
var buttons = controls.append("text") |
|
.attr("x", width) |
|
.attr("y", 28) |
|
.attr("class", "buttons"); |
|
|
|
var toggleAutoButton = buttons.append("tspan") |
|
.attr("id", "btnToggleAuto") |
|
.attr("class", "interactive btn") |
|
.text("\uf04b") |
|
.on("click", function toggleAuto() { |
|
autoIterate = !autoIterate; |
|
if (autoIterate) { |
|
// Start auto iterate and display pause button |
|
toggleAutoButton.text("\uf04c"); |
|
updateBars(); |
|
} else { |
|
// Pause auto iterate and display play button |
|
clearTimeout(timer); // Stop ongoing timer |
|
toggleAutoButton.text("\uf04b"); |
|
} |
|
}); |
|
|
|
// Randomize button |
|
var randomizeButton = buttons.append("tspan") |
|
.attr("id", "btnRandomize") |
|
.attr("class", "interactive btn") |
|
.text("\uf074") |
|
.on("click", function () { |
|
update_variables(shuffle = true); |
|
}); |
|
// Refresh button |
|
var refreshButton = buttons.append("tspan") |
|
.attr("id", "btnRefresh") |
|
.attr("class", "interactive btn") |
|
.text("\uf021") |
|
.on("click", function () { |
|
update_variables(shuffle = true, restart = true); |
|
}); |
|
|
|
|
|
//////////////////////////////////////////////////////// |
|
// Functions for scroll control and auto-incrementing // |
|
//////////////////////////////////////////////////////// |
|
|
|
function scrollN() { |
|
d3.event.preventDefault(); |
|
var dy = d3.event.wheelDeltaY; |
|
var minor_step = Math.ceil(numDataPoints / 100); |
|
var major_step = Math.ceil(numDataPoints / 100) * 5; |
|
if (dy > 0) { |
|
if (d3.event.altKey === true) { |
|
iteration = Math.min(numDataPoints, iteration + major_step) |
|
} else { |
|
iteration = Math.min(numDataPoints, iteration + minor_step) |
|
} |
|
} else { |
|
if (d3.event.altKey === true) { |
|
iteration = Math.max(0, iteration - major_step) |
|
} else { |
|
iteration = Math.max(0, iteration - minor_step) |
|
} |
|
} |
|
updateBars() |
|
} |
|
|
|
function scrollTot() { |
|
d3.event.preventDefault(); |
|
var dy = d3.event.wheelDeltaY; |
|
var step = Math.pow(10, numDataPoints.toString().length - 1); |
|
if (dy > 0) { |
|
if (d3.event.altKey === true) { |
|
numDataPoints = step * 10; |
|
} else { |
|
numDataPoints += step; |
|
} |
|
} else { |
|
if (numDataPoints % step == 0) step = step / 10; //Minus from a number raised to 10 should decrement by 10^x-1 |
|
// Minimum n = 10 total sample |
|
if (d3.event.altKey === true) { |
|
numDataPoints = Math.max(10, step / 10) |
|
} else { |
|
numDataPoints = Math.max(10, numDataPoints - (step / 10)) |
|
} |
|
} |
|
// Check that the Total does is not more than max to prevent memory consumption memory |
|
numDataPoints = Math.min(numDataPoints, numDataPointsMax); |
|
dataPoints = update_dataPoints(true); |
|
update_variables(); |
|
} |
|
|
|
var timer; |
|
|
|
function updateBars() { |
|
// Stop any potential ongoing transition |
|
clearTimeout(timer); |
|
|
|
// Slice to current i-step |
|
var DataPointsSliced = dataPoints.slice(0, iteration); |
|
|
|
// Bin the data |
|
var dataHist = update_dataHist(DataPointsSliced); |
|
|
|
walkNumCurrent.text(iteration + " / "); |
|
walkNumTot.text(numDataPoints); |
|
|
|
// Standard enter + update pattern not working. Due to completely new histogram data, and not an update of recycled datums? |
|
//barGroupEnties = barGroup.selectAll("g") |
|
// .data(dataHist); |
|
|
|
barRects |
|
.data(dataHist) |
|
.transition() |
|
.duration(transitionDuration) |
|
.ease(d3.easeBounce) |
|
.attr("transform", function (d) { |
|
return "translate(" + x(d.x0) + |
|
"," + y(d.length) + ")"; |
|
}) |
|
.attr("height", function (d) { |
|
return height - y(d.length); |
|
}); |
|
|
|
//Remove surplus bars if bins have been reduced |
|
barGroupEnter.exit().remove() |
|
|
|
// Update bar labels |
|
barLabels |
|
.data(dataHist) |
|
.transition() |
|
.duration(transitionDuration) |
|
.ease(d3.easeBounce) |
|
.text(function (d) { |
|
if (y(d.length) < (height - 15)) { |
|
return d.length; |
|
}; |
|
}) |
|
.attr("transform", function (d) { |
|
return "translate(" + (x(d.x0) + x.bandwidth() / 2) + |
|
"," + (y(d.length) + 16) + ")"; |
|
}); |
|
|
|
// Update descriptives if iteration is 2 or more |
|
if (iteration > 1) { |
|
update_descriptiveData(DataPointsSliced); |
|
console.log("Descriptives:\n", desciptiveData, |
|
"\nData points:\n", DataPointsSliced, |
|
"\nData histogram:\n", dataHist); |
|
|
|
descGroup |
|
.data(desciptiveData) |
|
.select("line") |
|
.transition() |
|
.duration(transitionDuration) |
|
.ease(d3.easeBounce) |
|
.attr('x1', function (d) { |
|
return xStep(d.val); |
|
}) |
|
.attr('x2', function (d) { |
|
return xStep(d.val); |
|
}); |
|
|
|
descGroup |
|
.select("text") |
|
.transition() |
|
.duration(transitionDuration) |
|
.ease(d3.easeBounce) |
|
.attr("transform", function (d) { |
|
if (d.type == "spread") { |
|
var value = xStep(desciptiveData[1].val - d.val); |
|
} else { |
|
var value = xStep(d.val); |
|
} |
|
return "translate(" + value + "," + 0 + ") rotate(-90)"; |
|
}); |
|
|
|
descGroup |
|
.select("rect") |
|
.transition() |
|
.duration(transitionDuration) |
|
.ease(d3.easeBounce) |
|
.attr('x', function (d) { |
|
return xStep(desciptiveData[1].val - d.val); |
|
}) // Mean - spread value (lower bound) |
|
.attr('width', function (d) { |
|
return ((xStep(desciptiveData[1].val + d.val) - xStep(desciptiveData[1].val)) * |
|
2); |
|
}) // Spread value - mean (width to upper bound) |
|
.attr('y', 0) |
|
.attr('height', height); |
|
} |
|
|
|
// Iterate after timer has passed if autoIterate = true |
|
if (autoIterate) { |
|
//Do the update first, then wait. Immediate transitions when updateBars() is run. |
|
if (iteration + 1 <= numDataPoints) { |
|
timer = setTimeout(function () { |
|
iteration += i_step; |
|
updateBars(); |
|
}, updateDelay); |
|
} |
|
} |
|
} |
|
|
|
function update_variables(shuffle, restart) { |
|
// Update X-Axis |
|
update_xValues(); |
|
xStep.domain(xDomain); |
|
x.domain(d3.ticks(xDomain[0], xDomain[1], 21)); |
|
// Recalibrate the Continous X scale to fit the Ordinal X scale endpoints |
|
xStep.range([(x(xDomain[0]) + (x.bandwidth() / 2)), (x(xDomain[1]) + (x.bandwidth() / 2))]); |
|
xAxisGroup |
|
.transition() |
|
.duration(500) |
|
.ease(d3.easeLinear) |
|
.call(xAxis); |
|
|
|
// Update bars width in case the X-axis has changed tick spacing |
|
barRects.attr("width", x.bandwidth()); |
|
|
|
//TODO: Remove bars that are left-over from previous distributions |
|
//Change sigma to 2 after a larger sample to see why |
|
|
|
// Start the update iteration over again from scratch |
|
if (shuffle) { |
|
dataPoints = update_dataPoints(); |
|
} |
|
if (restart) { |
|
iteration = 0; |
|
} |
|
|
|
dataHist = update_dataHist(dataPoints); |
|
|
|
// Update Y-Axis |
|
y.domain([0, d3.max(dataHist, function (d) { |
|
return Math.ceil(d.length / 10) * 10 |
|
})]); |
|
yAxisGroup |
|
.transition() |
|
.duration(500) |
|
.ease(d3.easeLinear) |
|
.call(yAxis); |
|
|
|
// If the updateDelay has changed, the transition duration should change accordingly |
|
transitionDuration = updateDelay * 0.8; |
|
|
|
updateBars(); |
|
} |
|
|
|
|
|
///////////////////////////////////////////////////// |
|
// Add listener to check for variable input change // |
|
///////////////////////////////////////////////////// |
|
|
|
document.addEventListener('DOMContentLoaded', function () { |
|
document.querySelector('#variables').onchange = change_variable; |
|
}, true); |
|
|
|
function change_variable(event) { |
|
if (!event.target.value) return; |
|
console.log(event.target.id, " was ", eval(event.target.id)); |
|
eval(event.target.id + " = " + event.target.valueAsNumber); |
|
console.log(event.target.id, " is now ", eval(event.target.id)); |
|
eval(event.target.getAttribute('fun')); |
|
// Refresh view |
|
update_variables(); |
|
} |
|
|
|
})() |
|
</script> |
|
|
|
<div id="variables"> |
|
<label>Sample:</label> |
|
<input type="number" class="variable" id="numDataPoints" placeholder="n = 100" fun="dataPoints = update_dataPoints(true);"> |
|
<input type="number" class="variable" id="normalMean" placeholder="μ = 0"> |
|
<input type="number" class="variable" id="normalStdDev" placeholder="σ = 3"> |
|
</br> |
|
</br> |
|
<label>Speed:</label> |
|
<input type="number" class="variable" id="i_step" placeholder="1 / i"> |
|
<input type="number" class="variable" id="updateDelay" placeholder="500 ms"> |
|
</div> |
|
|
|
</body> |
|
|
|
</html> |