Created
July 31, 2026 01:59
-
-
Save yeiichi/67aa8ee202e09a35eeb63564caf4bf05 to your computer and use it in GitHub Desktop.
Generate random values from a normal distribution and display the histogram
This file contains hidden or 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
| <!doctype html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>Gaussian Histogram Generator</title> | |
| <style> | |
| :root { | |
| color-scheme: light; | |
| --bg: #f6f7f9; | |
| --panel: #ffffff; | |
| --text: #1c2430; | |
| --muted: #647184; | |
| --line: #d8dde6; | |
| --accent: #227c70; | |
| --accent-dark: #175f56; | |
| --bar: #4477aa; | |
| --bar-hi: #d4663f; | |
| --shadow: 0 14px 34px rgba(27, 36, 48, 0.08); | |
| } | |
| * { | |
| box-sizing: border-box; | |
| } | |
| body { | |
| margin: 0; | |
| min-height: 100vh; | |
| background: var(--bg); | |
| color: var(--text); | |
| font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; | |
| } | |
| main { | |
| width: min(1180px, calc(100vw - 32px)); | |
| margin: 0 auto; | |
| padding: 32px 0; | |
| } | |
| header { | |
| display: flex; | |
| justify-content: space-between; | |
| gap: 24px; | |
| align-items: flex-end; | |
| margin-bottom: 22px; | |
| } | |
| h1 { | |
| margin: 0; | |
| font-size: 32px; | |
| line-height: 1.1; | |
| font-weight: 760; | |
| } | |
| .subtitle { | |
| margin: 8px 0 0; | |
| color: var(--muted); | |
| font-size: 15px; | |
| } | |
| .layout { | |
| display: grid; | |
| grid-template-columns: 320px minmax(0, 1fr); | |
| gap: 18px; | |
| align-items: start; | |
| } | |
| .panel { | |
| background: var(--panel); | |
| border: 1px solid var(--line); | |
| border-radius: 8px; | |
| box-shadow: var(--shadow); | |
| } | |
| .controls { | |
| padding: 18px; | |
| } | |
| .field { | |
| display: grid; | |
| gap: 7px; | |
| margin-bottom: 16px; | |
| } | |
| label { | |
| color: #334155; | |
| font-size: 13px; | |
| font-weight: 700; | |
| } | |
| input { | |
| width: 100%; | |
| height: 42px; | |
| border: 1px solid var(--line); | |
| border-radius: 7px; | |
| padding: 0 12px; | |
| color: var(--text); | |
| font: inherit; | |
| background: #fbfcfd; | |
| } | |
| input:focus { | |
| border-color: var(--accent); | |
| outline: 3px solid rgba(34, 124, 112, 0.18); | |
| } | |
| button { | |
| width: 100%; | |
| height: 44px; | |
| border: 0; | |
| border-radius: 7px; | |
| color: #fff; | |
| background: var(--accent); | |
| font: inherit; | |
| font-weight: 760; | |
| cursor: pointer; | |
| } | |
| button:hover { | |
| background: var(--accent-dark); | |
| } | |
| .stats { | |
| display: grid; | |
| grid-template-columns: repeat(2, 1fr); | |
| gap: 10px; | |
| margin-top: 18px; | |
| } | |
| .stat { | |
| border: 1px solid var(--line); | |
| border-radius: 7px; | |
| padding: 10px; | |
| min-width: 0; | |
| } | |
| .stat span { | |
| display: block; | |
| color: var(--muted); | |
| font-size: 12px; | |
| font-weight: 700; | |
| } | |
| .stat strong { | |
| display: block; | |
| margin-top: 4px; | |
| overflow-wrap: anywhere; | |
| font-size: 18px; | |
| } | |
| .chart-panel { | |
| padding: 16px; | |
| } | |
| .chart-top { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| gap: 12px; | |
| margin-bottom: 10px; | |
| color: var(--muted); | |
| font-size: 13px; | |
| font-weight: 700; | |
| } | |
| canvas { | |
| display: block; | |
| width: 100%; | |
| height: 520px; | |
| border: 1px solid var(--line); | |
| border-radius: 8px; | |
| background: #fff; | |
| } | |
| .error { | |
| min-height: 20px; | |
| margin-top: 12px; | |
| color: #a23a2a; | |
| font-size: 13px; | |
| font-weight: 700; | |
| } | |
| @media (max-width: 820px) { | |
| main { | |
| width: min(100vw - 22px, 680px); | |
| padding: 20px 0; | |
| } | |
| header { | |
| display: block; | |
| } | |
| h1 { | |
| font-size: 27px; | |
| } | |
| .layout { | |
| grid-template-columns: 1fr; | |
| } | |
| canvas { | |
| height: 390px; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <main> | |
| <header> | |
| <div> | |
| <h1>Gaussian Histogram Generator</h1> | |
| <p class="subtitle">Generate random values from a normal distribution and display the histogram.</p> | |
| </div> | |
| </header> | |
| <section class="layout"> | |
| <form class="panel controls" id="controls"> | |
| <div class="field"> | |
| <label for="trials">Trials n</label> | |
| <input id="trials" type="number" min="0" step="1" value="1000" required> | |
| </div> | |
| <div class="field"> | |
| <label for="mu">Mu m</label> | |
| <input id="mu" type="number" step="any" value="0" required> | |
| </div> | |
| <div class="field"> | |
| <label for="sigma">Sigma s</label> | |
| <input id="sigma" type="number" min="0" step="any" value="1" required> | |
| </div> | |
| <button type="submit">Generate</button> | |
| <div class="error" id="error" aria-live="polite"></div> | |
| <div class="stats" aria-label="Distribution summary"> | |
| <div class="stat"> | |
| <span>Count</span> | |
| <strong id="count">0</strong> | |
| </div> | |
| <div class="stat"> | |
| <span>Mean</span> | |
| <strong id="mean">0</strong> | |
| </div> | |
| <div class="stat"> | |
| <span>Std dev</span> | |
| <strong id="stddev">0</strong> | |
| </div> | |
| <div class="stat"> | |
| <span>Bins</span> | |
| <strong id="bins">0</strong> | |
| </div> | |
| </div> | |
| </form> | |
| <section class="panel chart-panel"> | |
| <div class="chart-top"> | |
| <span id="range">Range: -</span> | |
| <span id="peak">Peak: -</span> | |
| </div> | |
| <canvas id="histogram" width="1100" height="620" aria-label="Gaussian histogram"></canvas> | |
| </section> | |
| </section> | |
| </main> | |
| <script> | |
| const form = document.querySelector("#controls"); | |
| const canvas = document.querySelector("#histogram"); | |
| const ctx = canvas.getContext("2d"); | |
| const fields = { | |
| n: document.querySelector("#trials"), | |
| m: document.querySelector("#mu"), | |
| s: document.querySelector("#sigma"), | |
| error: document.querySelector("#error"), | |
| count: document.querySelector("#count"), | |
| mean: document.querySelector("#mean"), | |
| stddev: document.querySelector("#stddev"), | |
| bins: document.querySelector("#bins"), | |
| range: document.querySelector("#range"), | |
| peak: document.querySelector("#peak"), | |
| }; | |
| let currentValues = []; | |
| function gaussianRandom(mu, sigma) { | |
| let u = 0; | |
| let v = 0; | |
| while (u === 0) u = Math.random(); | |
| while (v === 0) v = Math.random(); | |
| const z = Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v); | |
| return mu + z * sigma; | |
| } | |
| function generateGaussianDistribution(n, m, s) { | |
| if (!Number.isInteger(n) || n < 0) { | |
| throw new Error("n must be a whole number greater than or equal to 0."); | |
| } | |
| if (n > 1000000) { | |
| throw new Error("n must be 1,000,000 or less."); | |
| } | |
| if (!Number.isFinite(m)) { | |
| throw new Error("mu must be a valid number."); | |
| } | |
| if (!Number.isFinite(s) || s < 0) { | |
| throw new Error("sigma must be greater than or equal to 0."); | |
| } | |
| return Array.from({ length: n }, () => gaussianRandom(m, s)); | |
| } | |
| function formatNumber(value) { | |
| if (!Number.isFinite(value)) return "-"; | |
| return Math.abs(value) >= 1000 || Math.abs(value) < 0.001 && value !== 0 | |
| ? value.toExponential(3) | |
| : value.toFixed(3); | |
| } | |
| function summarize(values) { | |
| if (values.length === 0) { | |
| return { mean: 0, stddev: 0 }; | |
| } | |
| const mean = values.reduce((sum, value) => sum + value, 0) / values.length; | |
| const variance = values.reduce((sum, value) => sum + (value - mean) ** 2, 0) / values.length; | |
| return { mean, stddev: Math.sqrt(variance) }; | |
| } | |
| function makeBins(values) { | |
| if (values.length === 0) { | |
| return { bins: [], min: 0, max: 0, maxCount: 0 }; | |
| } | |
| let min = values[0]; | |
| let max = values[0]; | |
| for (const value of values) { | |
| if (value < min) min = value; | |
| if (value > max) max = value; | |
| } | |
| const binCount = Math.max(1, Math.min(70, Math.round(Math.sqrt(values.length)))); | |
| const bins = Array.from({ length: binCount }, (_, index) => ({ | |
| index, | |
| count: 0, | |
| start: min, | |
| end: max, | |
| })); | |
| const span = max - min; | |
| if (span === 0) { | |
| bins[0].count = values.length; | |
| return { bins: [bins[0]], min, max, maxCount: values.length }; | |
| } | |
| for (let index = 0; index < binCount; index += 1) { | |
| const start = min + span * index / binCount; | |
| const end = min + span * (index + 1) / binCount; | |
| bins[index].start = start; | |
| bins[index].end = end; | |
| } | |
| for (const value of values) { | |
| const index = Math.min(binCount - 1, Math.floor((value - min) / span * binCount)); | |
| bins[index].count += 1; | |
| } | |
| return { bins, min, max, maxCount: Math.max(...bins.map((bin) => bin.count)) }; | |
| } | |
| function drawHistogram(values) { | |
| const dpr = window.devicePixelRatio || 1; | |
| const rect = canvas.getBoundingClientRect(); | |
| canvas.width = Math.round(rect.width * dpr); | |
| canvas.height = Math.round(rect.height * dpr); | |
| ctx.setTransform(dpr, 0, 0, dpr, 0, 0); | |
| const width = rect.width; | |
| const height = rect.height; | |
| const margin = { top: 34, right: 24, bottom: 54, left: 58 }; | |
| const plotWidth = width - margin.left - margin.right; | |
| const plotHeight = height - margin.top - margin.bottom; | |
| const histogram = makeBins(values); | |
| ctx.clearRect(0, 0, width, height); | |
| ctx.fillStyle = "#ffffff"; | |
| ctx.fillRect(0, 0, width, height); | |
| ctx.strokeStyle = "#d8dde6"; | |
| ctx.lineWidth = 1; | |
| for (let i = 0; i <= 4; i += 1) { | |
| const y = margin.top + plotHeight * i / 4; | |
| ctx.beginPath(); | |
| ctx.moveTo(margin.left, y); | |
| ctx.lineTo(width - margin.right, y); | |
| ctx.stroke(); | |
| } | |
| ctx.strokeStyle = "#1c2430"; | |
| ctx.beginPath(); | |
| ctx.moveTo(margin.left, margin.top); | |
| ctx.lineTo(margin.left, margin.top + plotHeight); | |
| ctx.lineTo(width - margin.right, margin.top + plotHeight); | |
| ctx.stroke(); | |
| if (histogram.bins.length === 0) { | |
| ctx.fillStyle = "#647184"; | |
| ctx.font = "16px system-ui, sans-serif"; | |
| ctx.textAlign = "center"; | |
| ctx.fillText("Enter values and generate a histogram", width / 2, height / 2); | |
| fields.range.textContent = "Range: -"; | |
| fields.peak.textContent = "Peak: -"; | |
| fields.bins.textContent = "0"; | |
| return; | |
| } | |
| const barWidth = plotWidth / histogram.bins.length; | |
| histogram.bins.forEach((bin, index) => { | |
| const barHeight = histogram.maxCount === 0 ? 0 : bin.count / histogram.maxCount * plotHeight; | |
| const x = margin.left + index * barWidth + 1; | |
| const y = margin.top + plotHeight - barHeight; | |
| ctx.fillStyle = index === Math.floor(histogram.bins.length / 2) ? "#d4663f" : "#4477aa"; | |
| ctx.fillRect(x, y, Math.max(1, barWidth - 2), barHeight); | |
| }); | |
| ctx.fillStyle = "#647184"; | |
| ctx.font = "12px system-ui, sans-serif"; | |
| ctx.textAlign = "right"; | |
| ctx.fillText(String(histogram.maxCount), margin.left - 8, margin.top + 4); | |
| ctx.fillText("0", margin.left - 8, margin.top + plotHeight + 4); | |
| ctx.textAlign = "center"; | |
| ctx.fillText(formatNumber(histogram.min), margin.left, height - 20); | |
| ctx.fillText(formatNumber(histogram.max), width - margin.right, height - 20); | |
| fields.range.textContent = `Range: ${formatNumber(histogram.min)} to ${formatNumber(histogram.max)}`; | |
| fields.peak.textContent = `Peak: ${histogram.maxCount}`; | |
| fields.bins.textContent = String(histogram.bins.length); | |
| } | |
| function update() { | |
| fields.error.textContent = ""; | |
| try { | |
| const n = Number(fields.n.value); | |
| const m = Number(fields.m.value); | |
| const s = Number(fields.s.value); | |
| const values = generateGaussianDistribution(n, m, s); | |
| currentValues = values; | |
| const summary = summarize(values); | |
| fields.count.textContent = String(values.length); | |
| fields.mean.textContent = formatNumber(summary.mean); | |
| fields.stddev.textContent = formatNumber(summary.stddev); | |
| drawHistogram(values); | |
| } catch (error) { | |
| fields.error.textContent = error.message; | |
| } | |
| } | |
| form.addEventListener("submit", (event) => { | |
| event.preventDefault(); | |
| update(); | |
| }); | |
| window.addEventListener("resize", () => drawHistogram(currentValues)); | |
| update(); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment