| 1 |
(function () { |
| 2 |
'use strict'; |
| 3 |
|
| 4 |
/* ---- Typography CSS-var helper ---- */ |
| 5 |
var _typoKeys = {family:'font-family',weight:'font-weight',style:'font-style',decoration:'text-decoration',transform:'text-transform'}; |
| 6 |
var _respKeys = {sizeDesktop:'font-size-d',sizeTablet:'font-size-t',sizeMobile:'font-size-m',lineHeightDesktop:'line-height-d',lineHeightTablet:'line-height-t',lineHeightMobile:'line-height-m',letterSpacingDesktop:'letter-spacing-d',letterSpacingTablet:'letter-spacing-t',letterSpacingMobile:'letter-spacing-m',wordSpacingDesktop:'word-spacing-d',wordSpacingTablet:'word-spacing-t',wordSpacingMobile:'word-spacing-m'}; |
| 7 |
function typoCssVarsForEl(el, obj, prefix) { |
| 8 |
if (!obj || typeof obj !== 'object') return; |
| 9 |
var k; |
| 10 |
for (k in _typoKeys) { if (obj[k]) el.style.setProperty(prefix + _typoKeys[k], obj[k]); } |
| 11 |
for (k in _respKeys) { if (obj[k] !== undefined && obj[k] !== '') el.style.setProperty(prefix + _respKeys[k], obj[k]); } |
| 12 |
} |
| 13 |
|
| 14 |
/* ---- Math helpers ---- */ |
| 15 |
function parseNums(raw) { |
| 16 |
var parts = raw.trim().split(/[\s,;]+/); |
| 17 |
var nums = []; |
| 18 |
for (var i = 0; i < parts.length; i++) { |
| 19 |
var p = parts[i].trim(); |
| 20 |
if (p === '') continue; |
| 21 |
var n = parseFloat(p); |
| 22 |
if (isNaN(n)) throw new Error('Invalid number: "' + p + '"'); |
| 23 |
nums.push(n); |
| 24 |
} |
| 25 |
if (nums.length < 2) throw new Error('Enter at least 2 numbers.'); |
| 26 |
return nums; |
| 27 |
} |
| 28 |
|
| 29 |
function mean(arr) { return arr.reduce(function (a, b) { return a + b; }, 0) / arr.length; } |
| 30 |
|
| 31 |
function median(sorted) { |
| 32 |
var n = sorted.length; |
| 33 |
return n % 2 === 0 ? (sorted[n / 2 - 1] + sorted[n / 2]) / 2 : sorted[Math.floor(n / 2)]; |
| 34 |
} |
| 35 |
|
| 36 |
function mode(arr) { |
| 37 |
var freq = {}; |
| 38 |
arr.forEach(function (n) { freq[n] = (freq[n] || 0) + 1; }); |
| 39 |
var maxF = Math.max.apply(null, Object.values(freq)); |
| 40 |
if (maxF === 1) return { values: [], label: 'No mode' }; |
| 41 |
var modes = Object.keys(freq).filter(function (k) { return freq[k] === maxF; }) |
| 42 |
.map(Number).sort(function (a, b) { return a - b; }); |
| 43 |
return { values: modes, label: modes.join(', '), freq: maxF }; |
| 44 |
} |
| 45 |
|
| 46 |
function stdDev(arr, mn) { |
| 47 |
var variance = arr.reduce(function (s, v) { return s + Math.pow(v - mn, 2); }, 0) / arr.length; |
| 48 |
return { variance: variance, stdDev: Math.sqrt(variance) }; |
| 49 |
} |
| 50 |
|
| 51 |
function quartiles(sorted) { |
| 52 |
var n = sorted.length; |
| 53 |
var lo = sorted.slice(0, Math.floor(n / 2)); |
| 54 |
var hi = n % 2 === 0 ? sorted.slice(n / 2) : sorted.slice(Math.floor(n / 2) + 1); |
| 55 |
return { q1: median(lo), q3: median(hi) }; |
| 56 |
} |
| 57 |
|
| 58 |
function skewness(arr, mn, sd) { |
| 59 |
if (sd === 0) return 0; |
| 60 |
var n = arr.length; |
| 61 |
var s = arr.reduce(function (a, v) { return a + Math.pow((v - mn) / sd, 3); }, 0); |
| 62 |
return (n / ((n - 1) * (n - 2))) * s; |
| 63 |
} |
| 64 |
|
| 65 |
function histogram(arr, bins) { |
| 66 |
var mn = Math.min.apply(null, arr); |
| 67 |
var mx = Math.max.apply(null, arr); |
| 68 |
var rng = mx - mn; |
| 69 |
if (rng === 0) { return [{ label: mn.toString(), count: arr.length, lo: mn, hi: mn }]; } |
| 70 |
bins = bins || Math.max(4, Math.min(12, Math.ceil(Math.sqrt(arr.length)))); |
| 71 |
var w = rng / bins; |
| 72 |
var buckets = []; |
| 73 |
for (var i = 0; i < bins; i++) { |
| 74 |
var lo = mn + i * w; |
| 75 |
var hi = lo + w; |
| 76 |
var count = arr.filter(function (v) { return v >= lo && (i === bins - 1 ? v <= hi : v < hi); }).length; |
| 77 |
buckets.push({ lo: lo, hi: hi, count: count }); |
| 78 |
} |
| 79 |
return buckets; |
| 80 |
} |
| 81 |
|
| 82 |
/* ---- DOM build ---- */ |
| 83 |
function initApp(app) { |
| 84 |
var opts; |
| 85 |
try { opts = JSON.parse(app.getAttribute('data-opts') || '{}'); } catch (e) { return; } |
| 86 |
|
| 87 |
var accent = opts.accentColor || '#6c3fb5'; |
| 88 |
var meanC = opts.meanColor || '#6c3fb5'; |
| 89 |
var medianC = opts.medianColor || '#0ea5e9'; |
| 90 |
var modeC = opts.modeColor || '#10b981'; |
| 91 |
var stdC = opts.stdDevColor || '#f59e0b'; |
| 92 |
var barC = opts.barColor || accent; |
| 93 |
var cardR = (opts.cardRadius || 16) + 'px'; |
| 94 |
var statR = (opts.statRadius || 12) + 'px'; |
| 95 |
var inpR = (opts.inputRadius || 8) + 'px'; |
| 96 |
var maxW = (opts.maxWidth || 680) + 'px'; |
| 97 |
var dp = opts.decimalPlaces !== undefined ? opts.decimalPlaces : 4; |
| 98 |
var lblClr = opts.labelColor || '#374151'; |
| 99 |
|
| 100 |
app.style.paddingTop = (opts.paddingTop || 60) + 'px'; |
| 101 |
app.style.paddingBottom = (opts.paddingBottom || 60) + 'px'; |
| 102 |
if (opts.sectionBg) app.style.background = opts.sectionBg; |
| 103 |
|
| 104 |
/* Typography CSS vars */ |
| 105 |
if (opts.titleSize) app.style.setProperty('--bkstc-tt-sz', opts.titleSize + 'px'); |
| 106 |
if (opts.titleFontWeight) app.style.setProperty('--bkstc-tt-wt', opts.titleFontWeight); |
| 107 |
if (opts.titleLineHeight !== undefined) app.style.setProperty('--bkstc-tt-lh', opts.titleLineHeight); |
| 108 |
if (opts.subtitleFontSize) app.style.setProperty('--bkstc-st-sz', opts.subtitleFontSize + 'px'); |
| 109 |
if (opts.subtitleLineHeight !== undefined) app.style.setProperty('--bkstc-st-lh', opts.subtitleLineHeight); |
| 110 |
typoCssVarsForEl(app, opts.titleTypo, '--bkstc-tt-'); |
| 111 |
typoCssVarsForEl(app, opts.subtitleTypo, '--bkstc-st-'); |
| 112 |
|
| 113 |
/* Card */ |
| 114 |
var card = document.createElement('div'); |
| 115 |
card.className = 'bkbg-stc-card'; |
| 116 |
Object.assign(card.style, { background: opts.cardBg || '#fff', borderRadius: cardR, maxWidth: maxW }); |
| 117 |
app.appendChild(card); |
| 118 |
|
| 119 |
if (opts.showTitle && opts.title) { |
| 120 |
var ttl = document.createElement('div'); ttl.className = 'bkbg-stc-title'; |
| 121 |
ttl.textContent = opts.title; |
| 122 |
if (opts.titleColor) ttl.style.color = opts.titleColor; |
| 123 |
card.appendChild(ttl); |
| 124 |
} |
| 125 |
if (opts.showSubtitle && opts.subtitle) { |
| 126 |
var sub = document.createElement('div'); sub.className = 'bkbg-stc-subtitle'; |
| 127 |
sub.textContent = opts.subtitle; |
| 128 |
if (opts.subtitleColor) sub.style.color = opts.subtitleColor; |
| 129 |
card.appendChild(sub); |
| 130 |
} |
| 131 |
|
| 132 |
/* Input */ |
| 133 |
var lbl = document.createElement('label'); lbl.className = 'bkbg-stc-lbl'; lbl.textContent = 'Dataset'; |
| 134 |
if (lblClr) lbl.style.color = lblClr; card.appendChild(lbl); |
| 135 |
var ta = document.createElement('textarea'); ta.className = 'bkbg-stc-textarea'; |
| 136 |
ta.value = opts.defaultData || '4, 8, 15, 16, 23, 42'; |
| 137 |
ta.placeholder = 'e.g. 4, 8, 15, 16, 23, 42'; |
| 138 |
ta.style.borderRadius = inpR; ta.style.background = opts.inputBg || '#f9fafb'; |
| 139 |
ta.style.borderColor = opts.inputBorder || '#d1d5db'; |
| 140 |
card.appendChild(ta); |
| 141 |
|
| 142 |
/* Buttons row */ |
| 143 |
var row = document.createElement('div'); row.className = 'bkbg-stc-row'; card.appendChild(row); |
| 144 |
var btn = document.createElement('button'); btn.className = 'bkbg-stc-calc-btn'; |
| 145 |
btn.textContent = '⚡ Calculate'; btn.style.background = accent; row.appendChild(btn); |
| 146 |
var clr = document.createElement('button'); clr.className = 'bkbg-stc-clear-btn'; |
| 147 |
clr.textContent = 'Clear'; row.appendChild(clr); |
| 148 |
var errEl = document.createElement('div'); errEl.className = 'bkbg-stc-error'; card.appendChild(errEl); |
| 149 |
|
| 150 |
/* Results area */ |
| 151 |
var resultsArea = document.createElement('div'); card.appendChild(resultsArea); |
| 152 |
|
| 153 |
function fmt(n) { return parseFloat(n.toFixed(dp)).toString(); } |
| 154 |
|
| 155 |
function STAT_CARDS(mn, med, mod, sd, v, rng, knt, min, max) { |
| 156 |
return [ |
| 157 |
{ label: 'Mean', val: fmt(mn), color: meanC }, |
| 158 |
{ label: 'Median', val: fmt(med), color: medianC }, |
| 159 |
{ label: 'Mode', val: mod.label,color: modeC }, |
| 160 |
{ label: 'Std Dev', val: fmt(sd), color: stdC }, |
| 161 |
{ label: 'Variance', val: fmt(v), color: stdC }, |
| 162 |
{ label: 'Range', val: fmt(rng), color: accent }, |
| 163 |
{ label: 'Count', val: knt, color: accent }, |
| 164 |
{ label: 'Min', val: fmt(min), color: '#6b7280' }, |
| 165 |
{ label: 'Max', val: fmt(max), color: '#6b7280' } |
| 166 |
]; |
| 167 |
} |
| 168 |
|
| 169 |
function render(nums) { |
| 170 |
resultsArea.innerHTML = ''; |
| 171 |
var sorted = nums.slice().sort(function (a, b) { return a - b; }); |
| 172 |
var mn = mean(nums); |
| 173 |
var med = median(sorted); |
| 174 |
var mod = mode(nums); |
| 175 |
var sv = stdDev(nums, mn); |
| 176 |
var sd = sv.stdDev; var variance = sv.variance; |
| 177 |
var rng = sorted[sorted.length - 1] - sorted[0]; |
| 178 |
var qs = quartiles(sorted); |
| 179 |
var iqr = qs.q3 - qs.q1; |
| 180 |
var skew = skewness(nums, mn, sd); |
| 181 |
var min = sorted[0]; var max = sorted[sorted.length - 1]; |
| 182 |
|
| 183 |
/* ---- Stat grid ---- */ |
| 184 |
var grid = document.createElement('div'); grid.className = 'bkbg-stc-grid'; |
| 185 |
STAT_CARDS(mn, med, mod, sd, variance, rng, nums.length, min, max).forEach(function (s) { |
| 186 |
var sc = document.createElement('div'); sc.className = 'bkbg-stc-stat-card'; |
| 187 |
sc.style.background = opts.statCardBg || '#fafafa'; |
| 188 |
sc.style.borderTopColor = s.color; |
| 189 |
sc.style.borderRadius = statR; |
| 190 |
var sl = document.createElement('div'); sl.className = 'bkbg-stc-stat-lbl'; sl.textContent = s.label; |
| 191 |
var sv2 = document.createElement('div'); sv2.className = 'bkbg-stc-stat-val'; sv2.textContent = s.val; sv2.style.color = s.color; |
| 192 |
sc.appendChild(sl); sc.appendChild(sv2); grid.appendChild(sc); |
| 193 |
}); |
| 194 |
/* Extra single-row stats */ |
| 195 |
[ |
| 196 |
{ label: 'Q1', val: fmt(qs.q1) }, |
| 197 |
{ label: 'Q3', val: fmt(qs.q3) }, |
| 198 |
{ label: 'IQR', val: fmt(iqr) }, |
| 199 |
{ label: 'Skewness', val: fmt(skew) } |
| 200 |
].forEach(function (s) { |
| 201 |
var sc = document.createElement('div'); sc.className = 'bkbg-stc-stat-card'; |
| 202 |
sc.style.background = opts.statCardBg || '#fafafa'; sc.style.borderTopColor = '#9ca3af'; |
| 203 |
sc.style.borderRadius = statR; |
| 204 |
var sl = document.createElement('div'); sl.className = 'bkbg-stc-stat-lbl'; sl.textContent = s.label; |
| 205 |
var sv2 = document.createElement('div'); sv2.className = 'bkbg-stc-stat-val'; sv2.textContent = s.val; sv2.style.color = '#6b7280'; |
| 206 |
sc.appendChild(sl); sc.appendChild(sv2); grid.appendChild(sc); |
| 207 |
}); |
| 208 |
resultsArea.appendChild(grid); |
| 209 |
|
| 210 |
/* ---- Histogram ---- */ |
| 211 |
if (opts.showHistogram !== false) { |
| 212 |
var sep1 = document.createElement('div'); sep1.className = 'bkbg-stc-sep'; resultsArea.appendChild(sep1); |
| 213 |
var ht = document.createElement('div'); ht.className = 'bkbg-stc-section-title'; ht.textContent = 'Frequency Histogram'; resultsArea.appendChild(ht); |
| 214 |
var buckets = histogram(nums); |
| 215 |
var maxCt = Math.max.apply(null, buckets.map(function (b) { return b.count; })); |
| 216 |
var hWrap = document.createElement('div'); hWrap.className = 'bkbg-stc-histogram'; resultsArea.appendChild(hWrap); |
| 217 |
buckets.forEach(function (b) { |
| 218 |
var col = document.createElement('div'); col.className = 'bkbg-stc-hist-col'; |
| 219 |
var bar = document.createElement('div'); bar.className = 'bkbg-stc-hist-bar'; |
| 220 |
var h = maxCt > 0 ? Math.max(4, (b.count / maxCt) * 90) : 4; |
| 221 |
bar.style.height = h + 'px'; bar.style.background = barC; bar.title = 'Count: ' + b.count; |
| 222 |
var lbl2 = document.createElement('div'); lbl2.className = 'bkbg-stc-hist-lbl'; |
| 223 |
lbl2.textContent = parseFloat(b.lo.toFixed(2)); |
| 224 |
col.appendChild(bar); col.appendChild(lbl2); hWrap.appendChild(col); |
| 225 |
}); |
| 226 |
} |
| 227 |
|
| 228 |
/* ---- Box plot ---- */ |
| 229 |
if (opts.showBoxPlot !== false && max > min) { |
| 230 |
var sep2 = document.createElement('div'); sep2.className = 'bkbg-stc-sep'; resultsArea.appendChild(sep2); |
| 231 |
var bt2 = document.createElement('div'); bt2.className = 'bkbg-stc-section-title'; bt2.textContent = 'Box Plot'; resultsArea.appendChild(bt2); |
| 232 |
var bpw = document.createElement('div'); bpw.className = 'bkbg-stc-boxplot-wrap'; |
| 233 |
var bpt = document.createElement('div'); bpt.className = 'bkbg-stc-boxplot-track'; |
| 234 |
var span = max - min; |
| 235 |
var q1pct = ((qs.q1 - min) / span) * 100; |
| 236 |
var q3pct = ((qs.q3 - min) / span) * 100; |
| 237 |
var medpct = ((med - min) / span) * 100; |
| 238 |
var iqrEl = document.createElement('div'); iqrEl.className = 'bkbg-stc-boxplot-iqr'; |
| 239 |
iqrEl.style.left = q1pct + '%'; iqrEl.style.width = (q3pct - q1pct) + '%'; |
| 240 |
iqrEl.style.background = barC + '66'; |
| 241 |
var medEl = document.createElement('div'); medEl.className = 'bkbg-stc-boxplot-median'; |
| 242 |
medEl.style.left = medpct + '%'; medEl.style.background = medianC; |
| 243 |
bpt.appendChild(iqrEl); bpt.appendChild(medEl); |
| 244 |
var bpl = document.createElement('div'); bpl.className = 'bkbg-stc-boxplot-labels'; |
| 245 |
[ |
| 246 |
{ label: 'Min ' + fmt(min), left: '0%' }, |
| 247 |
{ label: 'Q1 ' + fmt(qs.q1), left: q1pct + '%' }, |
| 248 |
{ label: 'Med ' + fmt(med), left: medpct + '%' }, |
| 249 |
{ label: 'Q3 ' + fmt(qs.q3), left: q3pct + '%' }, |
| 250 |
{ label: 'Max ' + fmt(max), left: '100%' } |
| 251 |
].forEach(function (p) { |
| 252 |
var s = document.createElement('span'); s.textContent = p.label; bpl.appendChild(s); |
| 253 |
}); |
| 254 |
bpw.appendChild(bpt); bpw.appendChild(bpl); resultsArea.appendChild(bpw); |
| 255 |
} |
| 256 |
|
| 257 |
/* ---- Sorted list ---- */ |
| 258 |
if (opts.showSortedList !== false) { |
| 259 |
var sep3 = document.createElement('div'); sep3.className = 'bkbg-stc-sep'; resultsArea.appendChild(sep3); |
| 260 |
var st = document.createElement('div'); st.className = 'bkbg-stc-section-title'; st.textContent = 'Sorted Values'; resultsArea.appendChild(st); |
| 261 |
var sw = document.createElement('div'); sw.className = 'bkbg-stc-sorted'; |
| 262 |
var lowerFence = qs.q1 - 1.5 * iqr; |
| 263 |
var upperFence = qs.q3 + 1.5 * iqr; |
| 264 |
sorted.forEach(function (n) { |
| 265 |
var pill = document.createElement('span'); pill.className = 'bkbg-stc-pill'; |
| 266 |
var isMode = mod.values.indexOf(n) !== -1; |
| 267 |
var isOut = n < lowerFence || n > upperFence; |
| 268 |
if (isMode) { pill.classList.add('bkbg-stc-pill-mode'); pill.style.background = modeC; } |
| 269 |
else if (isOut) { pill.classList.add('bkbg-stc-pill-outlier'); } |
| 270 |
pill.textContent = fmt(n); if (isMode) pill.title = 'Mode'; if (isOut) pill.title = 'Outlier'; |
| 271 |
sw.appendChild(pill); |
| 272 |
}); |
| 273 |
resultsArea.appendChild(sw); |
| 274 |
var legend = document.createElement('div'); legend.style.cssText = 'font-size:11px;color:#9ca3af;margin-top:8px;display:flex;gap:16px;flex-wrap:wrap;'; |
| 275 |
[{ bg: modeC, label: '● Mode' }, { bg: '#fee2e2', label: '● Outlier (IQR×1.5)' }].forEach(function (l) { |
| 276 |
var s = document.createElement('span'); s.style.color = l.bg; s.textContent = l.label; legend.appendChild(s); |
| 277 |
}); |
| 278 |
resultsArea.appendChild(legend); |
| 279 |
} |
| 280 |
} |
| 281 |
|
| 282 |
/* Trigger initial calculation */ |
| 283 |
function calculate() { |
| 284 |
errEl.textContent = ''; |
| 285 |
try { |
| 286 |
var nums = parseNums(ta.value); |
| 287 |
render(nums); |
| 288 |
} catch (e) { |
| 289 |
resultsArea.innerHTML = ''; |
| 290 |
errEl.textContent = '❗ ' + e.message; |
| 291 |
} |
| 292 |
} |
| 293 |
|
| 294 |
btn.addEventListener('click', calculate); |
| 295 |
clr.addEventListener('click', function () { ta.value = ''; resultsArea.innerHTML = ''; errEl.textContent = ''; }); |
| 296 |
ta.addEventListener('keydown', function (e) { if (e.key === 'Enter' && e.ctrlKey) { e.preventDefault(); calculate(); } }); |
| 297 |
|
| 298 |
calculate(); /* auto-run on load */ |
| 299 |
} |
| 300 |
|
| 301 |
document.querySelectorAll('.bkbg-stc-app').forEach(initApp); |
| 302 |
})(); |
| 303 |
|