Skip to content

Commit f1d4c3a

Browse files
committed
Add static notebook assets and polya vanilla JS
1 parent 52a08bd commit f1d4c3a

93 files changed

Lines changed: 118163 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
/**
2+
* Polya Urn – Vanilla D3 Visualizations
3+
*/
4+
(function () {
5+
'use strict';
6+
7+
/* ── shared constants ───────────────────────────────────────────────── */
8+
var URN_W = 650,
9+
URN_H = 260,
10+
DYN_W = 650,
11+
DYN_H = 250,
12+
acolor = 'Orange',
13+
bcolor = 'CornflowerBlue',
14+
text_color = '#222222',
15+
num_cols = 15,
16+
r = Math.floor(URN_W / 2 / num_cols / 2),
17+
line_offset = 2,
18+
txt_pad = 4,
19+
NUM_STEPS = 100,
20+
NUM_RUNS = 60;
21+
22+
/* ═══════════════════════════════════════════════════════════════════════
23+
Figure 1 – PolyaUrn (balls in an urn)
24+
═══════════════════════════════════════════════════════════════════════ */
25+
26+
function ballY(pos) {
27+
return URN_H - r - line_offset - Math.floor(pos / num_cols) * (line_offset + 2 * r);
28+
}
29+
30+
function drawBalls(svg, a, b) {
31+
svg.selectAll('circle.ball').remove();
32+
var i;
33+
for (i = 0; i < a; i++) {
34+
svg.append('circle').attr('class', 'ball')
35+
.attr('r', r)
36+
.attr('cx', URN_W / 2 - r - (i % num_cols) * 2 * r)
37+
.attr('cy', ballY(i))
38+
.style('stroke', 'black').style('stroke-width', 2)
39+
.style('fill', acolor);
40+
}
41+
for (i = 0; i < b; i++) {
42+
svg.append('circle').attr('class', 'ball')
43+
.attr('r', r)
44+
.attr('cx', URN_W / 2 + r + (i % num_cols) * 2 * r)
45+
.attr('cy', ballY(i))
46+
.style('stroke', 'black').style('stroke-width', 2)
47+
.style('fill', bcolor);
48+
}
49+
}
50+
51+
function initPolyaUrn(figureEl) {
52+
var container = figureEl.querySelector('.d3-component');
53+
if (!container) return;
54+
55+
container.innerHTML = '';
56+
57+
/* Read initial values from the idyll-dynamic spans */
58+
var dynSpans = figureEl.querySelectorAll('.idyll-dynamic');
59+
var a0 = parseInt((dynSpans[0] || {}).textContent || '1', 10);
60+
var b0 = parseInt((dynSpans[1] || {}).textContent || '2', 10);
61+
62+
var svg = d3.select(container).append('svg')
63+
.attr('viewBox', '0 0 ' + URN_W + ' ' + URN_H)
64+
.attr('overflow', 'visible') /* allow animated balls to enter from top */
65+
.style('width', '100%')
66+
.style('height', 'auto')
67+
.style('display', 'block');
68+
69+
svg.append('rect')
70+
.attr('width', URN_W).attr('height', URN_H).attr('fill', 'white')
71+
.style('stroke', '#ccc').style('stroke-width', 1);
72+
73+
drawBalls(svg, a0, b0);
74+
75+
/* ratio label */
76+
var lbl = svg.append('text')
77+
.attr('x', 2 * URN_W / 3).attr('y', 25)
78+
.text('Ratio b / (a + b) = ')
79+
.attr('font-family', 'sans-serif').attr('font-size', '16px')
80+
.attr('fill', text_color);
81+
var lb = lbl.node().getBBox();
82+
83+
var ratioTxt = svg.append('text').attr('class', 'polya-ratio')
84+
.attr('x', lb.x + lb.width + 5).attr('y', 25)
85+
.text(d3.format(',.2f')(b0 / (a0 + b0)))
86+
.attr('font-family', 'sans-serif').attr('font-size', '16px')
87+
.attr('fill', b0 > a0 ? bcolor : acolor);
88+
var rb = ratioTxt.node().getBBox();
89+
90+
svg.append('rect')
91+
.attr('x', lb.x - txt_pad).attr('y', lb.y - txt_pad)
92+
.attr('width', lb.width + rb.width + 5 + 2 * txt_pad)
93+
.attr('height', lb.height + 2 * txt_pad)
94+
.attr('fill', 'none').style('stroke', text_color).style('stroke-width', 1);
95+
96+
function updateRatio(a, b) {
97+
svg.select('.polya-ratio')
98+
.text(d3.format(',.2f')(b / (a + b)))
99+
.attr('fill', b > a ? bcolor : acolor);
100+
}
101+
102+
/* Simulate button – add listener directly, no cloneNode needed */
103+
var btn = figureEl.querySelector('button.simulate');
104+
if (btn) {
105+
btn.addEventListener('click', function () {
106+
var a = parseInt((dynSpans[0] || {}).textContent || '1', 10);
107+
var b = parseInt((dynSpans[1] || {}).textContent || '2', 10);
108+
109+
svg.selectAll('circle.ball').interrupt();
110+
drawBalls(svg, a, b);
111+
112+
var max_balls = (Math.floor(URN_H / (2 * r + line_offset)) - 1) * num_cols;
113+
var step = 0;
114+
while (a < max_balls && b < max_balls) {
115+
var color, sign, pos;
116+
if (Math.random() > b / (a + b)) {
117+
color = acolor; sign = -1; pos = a++;
118+
} else {
119+
color = bcolor; sign = 1; pos = b++;
120+
}
121+
step++;
122+
svg.append('circle').attr('class', 'ball')
123+
.attr('r', r)
124+
.attr('cx', URN_W / 2 + sign * (r + (pos % num_cols) * 2 * r))
125+
.attr('cy', r) /* start just inside the top edge */
126+
.style('stroke', 'black').style('stroke-width', 2)
127+
.style('fill', color)
128+
.transition().ease(d3.easeCubicOut).duration(400).delay(step * 8)
129+
.attr('cy', ballY(pos));
130+
}
131+
updateRatio(a, b);
132+
});
133+
}
134+
135+
/* draggable parameter spans */
136+
makeDraggable(dynSpans[0], 1, 12, function (val) {
137+
a0 = val;
138+
drawBalls(svg, a0, b0);
139+
updateRatio(a0, b0);
140+
});
141+
makeDraggable(dynSpans[1], 1, 12, function (val) {
142+
b0 = val;
143+
drawBalls(svg, a0, b0);
144+
updateRatio(a0, b0);
145+
});
146+
}
147+
148+
/* ═══════════════════════════════════════════════════════════════════════
149+
Figure 2 – PolyaDynamics (time-series)
150+
═══════════════════════════════════════════════════════════════════════ */
151+
152+
function drawBaseline(svg, a, b) {
153+
svg.selectAll('.dyn').remove();
154+
var y0 = DYN_H * (1 - b / (a + b));
155+
156+
svg.append('line').attr('class', 'dyn')
157+
.style('stroke', 'black').style('stroke-dasharray', '3,3').style('opacity', 0.8)
158+
.attr('x1', 0).attr('y1', y0)
159+
.attr('x2', (NUM_STEPS + 1) * 10).attr('y2', y0);
160+
161+
var hgap = 8, hwidth = 65, vgap = 20, vheight = 26;
162+
var y0_t = (b > a) ? y0 + vgap + vheight : y0;
163+
164+
svg.append('text').attr('class', 'dyn')
165+
.attr('x', DYN_W - hgap - hwidth + 5).attr('y', y0_t - vgap)
166+
.text('Y').attr('font-family', 'sans-serif').attr('font-size', '16px')
167+
.attr('fill', text_color)
168+
.append('tspan').text('0').style('font-size', '10px').attr('dx', '-.2em').attr('dy', '.7em');
169+
170+
svg.append('text').attr('class', 'dyn')
171+
.attr('x', DYN_W - hgap - hwidth + 15).attr('y', y0_t - vgap)
172+
.text('= ' + d3.format(',.2f')(b / (a + b)))
173+
.attr('font-family', 'sans-serif').attr('font-size', '16px').attr('fill', text_color);
174+
175+
svg.append('rect').attr('class', 'dyn')
176+
.attr('x', DYN_W - hwidth - hgap).attr('y', y0_t - vgap - 17)
177+
.attr('width', hwidth).attr('height', vheight)
178+
.attr('fill', 'none').style('stroke', text_color).style('stroke-width', 1);
179+
}
180+
181+
function initPolyaDynamics(figureEl) {
182+
var container = figureEl.querySelector('.d3-component');
183+
if (!container) return;
184+
185+
container.innerHTML = '';
186+
187+
var dynSpans = figureEl.querySelectorAll('.idyll-dynamic');
188+
var a0 = parseInt((dynSpans[0] || {}).textContent || '1', 10);
189+
var b0 = parseInt((dynSpans[1] || {}).textContent || '2', 10);
190+
191+
var svg = d3.select(container).append('svg')
192+
.attr('viewBox', '0 0 ' + DYN_W + ' ' + DYN_H)
193+
.attr('overflow', 'visible')
194+
.style('width', '100%')
195+
.style('height', 'auto')
196+
.style('display', 'block');
197+
198+
svg.append('rect')
199+
.attr('width', DYN_W).attr('height', DYN_H).attr('fill', 'white')
200+
.style('stroke', '#ccc').style('stroke-width', 1);
201+
202+
drawBaseline(svg, a0, b0);
203+
204+
var btn = figureEl.querySelector('button.simulate');
205+
if (btn) {
206+
btn.addEventListener('click', function () {
207+
var a = parseInt((dynSpans[0] || {}).textContent || '1', 10);
208+
var b = parseInt((dynSpans[1] || {}).textContent || '2', 10);
209+
210+
svg.selectAll('.dyn').interrupt();
211+
drawBaseline(svg, a, b);
212+
213+
var duration = 2200 / NUM_STEPS;
214+
var bvals = [];
215+
for (var k = 0; k < NUM_RUNS; k++) bvals.push(b);
216+
217+
for (var i = 0; i < NUM_STEPS; i++) {
218+
for (var j = 0; j < NUM_RUNS; j++) {
219+
var adrawn = Math.random() > bvals[j] / (a + b + i);
220+
if (!adrawn) bvals[j]++;
221+
222+
svg.append('line').attr('class', 'dyn')
223+
.style('stroke', 'PowderBlue').style('opacity', 0.35)
224+
.transition().duration(duration).delay(i * duration)
225+
.attr('x1', i * 10)
226+
.attr('y1', DYN_H * (1 - (adrawn ? bvals[j] : bvals[j] - 1) / (a + b + i)))
227+
.attr('x2', (i + 1) * 10)
228+
.attr('y2', DYN_H * (1 - bvals[j] / (b + a + i + 1)));
229+
}
230+
231+
(function (step, snapshot_b, a_val, b_val) {
232+
var sum = 0;
233+
for (var s = 0; s < snapshot_b.length; s++) sum += snapshot_b[s];
234+
var avg = sum / snapshot_b.length;
235+
svg.append('circle').attr('class', 'dyn')
236+
.transition().duration(duration).delay(step * duration)
237+
.attr('cx', step * 10)
238+
.attr('cy', DYN_H * (1 - avg / (b_val + a_val + step + 1)))
239+
.attr('r', 3).style('fill', 'Tomato');
240+
}(i, bvals.slice(), a, b));
241+
}
242+
});
243+
}
244+
245+
makeDraggable(dynSpans[0], 1, 12, function (val) {
246+
a0 = val; drawBaseline(svg, a0, b0);
247+
});
248+
makeDraggable(dynSpans[1], 1, 12, function (val) {
249+
b0 = val; drawBaseline(svg, a0, b0);
250+
});
251+
}
252+
253+
/* ═══════════════════════════════════════════════════════════════════════
254+
Draggable .idyll-dynamic spans
255+
═══════════════════════════════════════════════════════════════════════ */
256+
257+
function makeDraggable(el, min, max, onChange) {
258+
if (!el) return;
259+
el.style.cursor = 'ew-resize';
260+
el.style.userSelect = 'none';
261+
el.title = 'Drag left/right to change value';
262+
263+
el.addEventListener('mousedown', function (e) {
264+
var startX = e.clientX;
265+
var startVal = parseInt(el.textContent, 10);
266+
267+
function onMove(e2) {
268+
var delta = Math.round((e2.clientX - startX) / 20);
269+
var newVal = Math.max(min, Math.min(max, startVal + delta));
270+
if (parseInt(el.textContent, 10) !== newVal) {
271+
el.textContent = newVal;
272+
onChange(newVal);
273+
}
274+
}
275+
function onUp() {
276+
document.removeEventListener('mousemove', onMove);
277+
document.removeEventListener('mouseup', onUp);
278+
}
279+
document.addEventListener('mousemove', onMove);
280+
document.addEventListener('mouseup', onUp);
281+
e.preventDefault();
282+
});
283+
}
284+
285+
/* ═══════════════════════════════════════════════════════════════════════
286+
Bootstrap
287+
═══════════════════════════════════════════════════════════════════════ */
288+
289+
function bootstrap() {
290+
var root = document.getElementById('idyll-mount') || document;
291+
var figures = root.querySelectorAll('.figure');
292+
if (figures[0]) initPolyaUrn(figures[0]);
293+
if (figures[1]) initPolyaDynamics(figures[1]);
294+
}
295+
296+
if (document.readyState === 'loading') {
297+
document.addEventListener('DOMContentLoaded', bootstrap);
298+
} else {
299+
bootstrap();
300+
}
301+
302+
}());
6.89 KB
Loading
190 KB
Loading
62.3 KB
Loading
13.4 KB
Loading
10.1 KB
Loading
205 KB
Loading
9.89 KB
Loading

0 commit comments

Comments
 (0)