Änderungen von Dokument Interaktiv Behälter füllen

Zuletzt geändert von Holger Engels am 2025/12/18 12:53

Von Version 1.1
bearbeitet von Stefan Martin
am 2025/12/18 09:48
Änderungskommentar: Es gibt keinen Kommentar für diese Version
Auf Version 4.1
bearbeitet von Stefan Martin
am 2025/12/18 09:59
Änderungskommentar: Es gibt keinen Kommentar für diese Version

Zusammenfassung

Details

Seiteneigenschaften
Inhalt
... ... @@ -1,6 +1,6 @@
1 1  {{html clean="false"}}
2 2  <iframe
3 - src="$doc.getAttachmentURL('wahrscheinlichkeitsbaum.html')"
3 + src="$doc.getAttachmentURL('baumdiagramm.html')"
4 4   width="100%"
5 5   height="900px"
6 6   style="border:1px solid #ccc;">
baumdiagramm.html
Author
... ... @@ -1,0 +1,1 @@
1 +XWiki.smartin
Größe
... ... @@ -1,0 +1,1 @@
1 +24.3 KB
Inhalt
... ... @@ -1,0 +1,765 @@
1 +<!doctype html>
2 +<html lang="de">
3 +<head>
4 + <meta charset="utf-8" />
5 + <meta name="viewport" content="width=device-width, initial-scale=1" />
6 + <title>Interaktiver Wahrscheinlichkeitsbaum (mit/ohne Zurücklegen)</title>
7 + <style>
8 + :root { font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
9 + body { margin: 0; background: #0b0d12; color: #e9eefc; }
10 + header { padding: 14px 16px; border-bottom: 1px solid rgba(255,255,255,.08); }
11 + h1 { margin: 0; font-size: 16px; font-weight: 700; letter-spacing: .2px; }
12 + .wrap { display: grid; grid-template-columns: 380px 1fr; min-height: calc(100vh - 52px); }
13 + .panel {
14 + padding: 14px 16px;
15 + border-right: 1px solid rgba(255,255,255,.08);
16 + background: rgba(255,255,255,.03);
17 + }
18 + .row { margin: 14px 0; }
19 + .label { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 6px; gap: 10px; }
20 + .label b { font-size: 13px; }
21 + .label span { font-variant-numeric: tabular-nums; opacity: .9; }
22 + textarea {
23 + width: 100%;
24 + min-height: 88px;
25 + resize: vertical;
26 + border-radius: 12px;
27 + padding: 10px 12px;
28 + border: 1px solid rgba(255,255,255,.15);
29 + background: rgba(255,255,255,.05);
30 + color: inherit;
31 + outline: none;
32 + font-size: 13px;
33 + line-height: 1.35;
34 + }
35 + input[type="range"] { width: 100%; }
36 + .hint { font-size: 12px; line-height: 1.35; opacity: .85; }
37 + .stats { margin-top: 14px; padding: 10px 12px; border-radius: 12px; background: rgba(255,255,255,.06); }
38 + .stats div { display: flex; justify-content: space-between; margin: 6px 0; font-variant-numeric: tabular-nums; gap: 10px; }
39 + .btns { display: flex; gap: 8px; margin-top: 12px; flex-wrap: wrap; }
40 + button {
41 + border: 1px solid rgba(255,255,255,.15);
42 + background: rgba(255,255,255,.06);
43 + color: inherit;
44 + padding: 8px 10px;
45 + border-radius: 10px;
46 + cursor: pointer;
47 + font-size: 13px;
48 + }
49 + button:hover { background: rgba(255,255,255,.10); }
50 +
51 + .toggle {
52 + display: flex; align-items: center; justify-content: space-between;
53 + gap: 10px; padding: 10px 12px; border-radius: 12px;
54 + background: rgba(255,255,255,.06);
55 + border: 1px solid rgba(255,255,255,.10);
56 + }
57 + .toggle label { font-size: 13px; font-weight: 700; }
58 + .switch { position: relative; width: 52px; height: 30px; flex: 0 0 auto; }
59 + .switch input{
60 + position: absolute;
61 + inset: 0;
62 + width: 100%;
63 + height: 100%;
64 + opacity: 0;
65 + cursor: pointer;
66 + margin: 0;
67 + z-index: 2;
68 + }
69 + .slider{
70 + position: absolute; inset: 0;
71 + background: rgba(255,255,255,.10);
72 + border: 1px solid rgba(255,255,255,.14);
73 + transition: .2s; border-radius: 999px;
74 + z-index: 1;
75 + }
76 + .slider:before{
77 + position: absolute; content: "";
78 + height: 22px; width: 22px; left: 3px; top: 3px;
79 + background: rgba(233,238,252,.92);
80 + transition: .2s; border-radius: 999px;
81 + }
82 + .switch input:checked + .slider { background: rgba(120,255,180,.16); }
83 + .switch input:checked + .slider:before { transform: translateX(22px); }
84 +
85 + .warn {
86 + margin-top: 10px;
87 + padding: 10px 12px;
88 + border-radius: 12px;
89 + border: 1px solid rgba(255,200,120,.28);
90 + background: rgba(255,200,120,.10);
91 + color: rgba(255,235,200,.95);
92 + font-size: 12px;
93 + display: none;
94 + }
95 +
96 + .canvas { position: relative; overflow: hidden; }
97 + .tooltip {
98 + position: absolute;
99 + pointer-events: none;
100 + background: rgba(20, 24, 35, .92);
101 + border: 1px solid rgba(255,255,255,.12);
102 + padding: 8px 10px;
103 + border-radius: 10px;
104 + font-size: 12px;
105 + max-width: 420px;
106 + display: none;
107 + box-shadow: 0 14px 40px rgba(0,0,0,.35);
108 + }
109 + .tooltip .t { font-weight: 700; margin-bottom: 4px; }
110 + .tooltip code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
111 +
112 + svg {
113 + width: 100%;
114 + height: 100%;
115 + display: block;
116 + background: radial-gradient(900px 500px at 20% 10%, rgba(80,120,255,.10), transparent 60%);
117 + }
118 +
119 + /* Labels keep via CSS */
120 + .edgeLabel { fill: rgba(233,238,252,.88); font-size: 11px; }
121 + .nodeText { fill: rgba(233,238,252,.92); font-size: 11px; }
122 + .leafProb { fill: rgba(233,238,252,.92); font-size: 11px; opacity: .95; }
123 + </style>
124 +</head>
125 +<body>
126 + <header>
127 + <h1>Interaktiver Wahrscheinlichkeitsbaum (Brüche, mit/ohne Zurücklegen)</h1>
128 + </header>
129 +
130 + <div class="wrap">
131 + <aside class="panel">
132 + <div class="row">
133 + <div class="label">
134 + <b>Ergebnismenge (mit Elementarwahrscheinlichkeiten als Bruch)</b>
135 + <span id="kInfo"></span>
136 + </div>
137 + <textarea id="outcomes" spellcheck="false">rot (7/13), grün (1/13), gelb (3/13), weiß (2/13)</textarea>
138 + <div class="hint">
139 + Format: <code>name (a/b)</code>, getrennt mit Komma oder neuer Zeile.
140 + </div>
141 + <div class="warn" id="parseWarn"></div>
142 + </div>
143 +
144 + <div class="row">
145 + <div class="toggle" id="toggleBox">
146 + <div>
147 + <label id="modeLabel">Mit Zurücklegen</label>
148 + <div class="hint" style="margin-top:4px;">
149 + Ohne Zurücklegen: Wahrscheinlichkeiten ändern sich pro Stufe gemäß Restbestand.
150 + </div>
151 + </div>
152 + <div class="switch">
153 + <input id="without" type="checkbox" />
154 + <span class="slider"></span>
155 + </div>
156 + </div>
157 + </div>
158 +
159 + <div class="row">
160 + <div class="label">
161 + <b>Wiederholungen (Stufen/Tiefe)</b>
162 + <span id="nVal"></span>
163 + </div>
164 + <input id="n" type="range" min="1" max="10" step="1" value="3" />
165 + <div class="hint">Anzahl Ziehungen / Wiederholungen \(n\).</div>
166 + <div class="warn" id="depthWarn"></div>
167 + </div>
168 +
169 + <div class="stats">
170 + <div><span>Fälle (k)</span><span id="kVal"></span></div>
171 + <div><span>Blätter</span><span id="leafCount"></span></div>
172 + <div><span>Summe Start-Wahrscheinlichkeiten</span><span id="sumProb"></span></div>
173 + <div><span>Ohne Zurücklegen: Gesamtbestand</span><span id="totalMass"></span></div>
174 + </div>
175 +
176 + <div class="btns">
177 + <button id="fit">Ansicht zentrieren</button>
178 + <button id="reset">Zoom zurücksetzen</button>
179 + <button id="download">SVG speichern</button>
180 + <button id="rerender">Neu zeichnen</button>
181 + </div>
182 +
183 + <div class="row hint" style="margin-top:12px;">
184 + <b>Interaktion:</b> Ziehen = Pan, Mausrad = Zoom, Hover = Tooltip.
185 + </div>
186 + </aside>
187 +
188 + <main class="canvas" id="canvas">
189 + <div class="tooltip" id="tip">
190 + <div class="t" id="tipTitle"></div>
191 + <div id="tipBody"></div>
192 + </div>
193 +
194 + <svg id="svg" viewBox="0 0 1400 900" preserveAspectRatio="xMidYMid meet">
195 + <g id="viewport"></g>
196 + </svg>
197 + </main>
198 + </div>
199 +
200 +<script>
201 +(() => {
202 + // ========= Exact rational arithmetic (BigInt) =========
203 + const BI = (x) => BigInt(x);
204 +
205 + function gcd(a, b) {
206 + a = a < 0n ? -a : a;
207 + b = b < 0n ? -b : b;
208 + while (b !== 0n) { const t = a % b; a = b; b = t; }
209 + return a;
210 + }
211 + function lcm(a, b) {
212 + a = a < 0n ? -a : a;
213 + b = b < 0n ? -b : b;
214 + if (a === 0n || b === 0n) return 0n;
215 + return (a / gcd(a,b)) * b;
216 + }
217 + function normRat(num, den) {
218 + if (den === 0n) throw new Error("Nenner darf nicht 0 sein.");
219 + if (den < 0n) { num = -num; den = -den; }
220 + const g = gcd(num, den);
221 + return { num: num / g, den: den / g };
222 + }
223 + function addRat(a, b) { return normRat(a.num*b.den + b.num*a.den, a.den*b.den); }
224 + function mulRat(a, b) { return normRat(a.num*b.num, a.den*b.den); }
225 + function ratToString(r) { return `${r.num.toString()}/${r.den.toString()}`; }
226 + function ratToDecimal(r) {
227 + const n = Number(r.num), d = Number(r.den);
228 + if (!isFinite(n) || !isFinite(d) || d === 0) return "—";
229 + const x = n / d;
230 + if (x === 0) return "0";
231 + if (Math.abs(x) >= 0.001) return x.toFixed(6).replace(/0+$/,'').replace(/\.$/,'');
232 + return x.toExponential(3);
233 + }
234 +
235 + // ========= UI elements =========
236 + const svg = document.getElementById("svg");
237 + const viewport = document.getElementById("viewport");
238 + const canvas = document.getElementById("canvas");
239 +
240 + const outcomesEl = document.getElementById("outcomes");
241 + const withoutEl = document.getElementById("without");
242 + const modeLabel = document.getElementById("modeLabel");
243 +
244 + const nSlider = document.getElementById("n");
245 + const nVal = document.getElementById("nVal");
246 +
247 + const kInfo = document.getElementById("kInfo");
248 + const kVal = document.getElementById("kVal");
249 + const leafCount = document.getElementById("leafCount");
250 + const sumProb = document.getElementById("sumProb");
251 + const totalMass = document.getElementById("totalMass");
252 +
253 + const parseWarn = document.getElementById("parseWarn");
254 + const depthWarn = document.getElementById("depthWarn");
255 +
256 + const tip = document.getElementById("tip");
257 + const tipTitle = document.getElementById("tipTitle");
258 + const tipBody = document.getElementById("tipBody");
259 +
260 + const btnFit = document.getElementById("fit");
261 + const btnReset = document.getElementById("reset");
262 + const btnDownload = document.getElementById("download");
263 + const btnRerender = document.getElementById("rerender");
264 +
265 + // ========= Pan/Zoom =========
266 + let panX = 40, panY = 0, zoom = 1;
267 + let isPanning = false, startX = 0, startY = 0;
268 +
269 + function applyTransform() {
270 + viewport.setAttribute("transform", `translate(${panX},${panY}) scale(${zoom})`);
271 + }
272 + function resetView() {
273 + panX = 40; panY = 0; zoom = 1;
274 + applyTransform();
275 + }
276 +
277 + // ========= Edge endpoints to circle boundaries =========
278 + function edgeEndpoints(from, to, rFrom, rTo) {
279 + const dx = to.x - from.x;
280 + const dy = to.y - from.y;
281 + const len = Math.hypot(dx, dy) || 1;
282 + const ux = dx / len;
283 + const uy = dy / len;
284 + return {
285 + x1: from.x + ux * rFrom,
286 + y1: from.y + uy * rFrom,
287 + x2: to.x - ux * rTo,
288 + y2: to.y - uy * rTo
289 + };
290 + }
291 +
292 + // ========= Parsing: "name (a/b)" =========
293 + function parseOutcomes(text) {
294 + const raw = text
295 + .split(/\n|,/)
296 + .map(s => s.trim())
297 + .filter(Boolean);
298 +
299 + const items = [];
300 + const errors = [];
301 +
302 + const re = /^(.+?)\s*\(\s*([0-9]+)\s*\/\s*([0-9]+)\s*\)\s*$/i;
303 +
304 + for (const part of raw) {
305 + const m = part.match(re);
306 + if (!m) {
307 + errors.push(`Kann nicht lesen: "${part}" (erwartet: name (a/b))`);
308 + continue;
309 + }
310 + const name = m[1].trim();
311 + const a = BI(m[2]);
312 + const b = BI(m[3]);
313 + if (b === 0n) { errors.push(`Nenner 0 bei "${part}"`); continue; }
314 + items.push({ name, p: normRat(a, b) });
315 + }
316 +
317 + let sum = { num: 0n, den: 1n };
318 + for (const it of items) sum = addRat(sum, it.p);
319 +
320 + return { items, errors, sum };
321 + }
322 +
323 + // For "ohne Zurücklegen" integer counts via LCM of denominators
324 + function probsToCounts(items) {
325 + let L = 1n;
326 + for (const it of items) L = lcm(L, it.p.den);
327 +
328 + const counts = [];
329 + let total = 0n;
330 + for (const it of items) {
331 + const c = (it.p.num * (L / it.p.den));
332 + counts.push({ name: it.name, count: c });
333 + total += c;
334 + }
335 + return { counts, total, L };
336 + }
337 +
338 + // ========= Tree building =========
339 + function buildTree(items, n, withoutReplacement) {
340 + const root = {
341 + id: "root",
342 + depth: 0,
343 + label: "Start",
344 + parent: null,
345 + edgeP: null,
346 + pathP: { num: 1n, den: 1n },
347 + pathLabels: [],
348 + counts: null
349 + };
350 +
351 + const nodes = [root];
352 + const edges = [];
353 +
354 + let baseCounts = null;
355 + let baseTotal = null;
356 +
357 + if (withoutReplacement) {
358 + const { counts, total } = probsToCounts(items);
359 + baseCounts = counts;
360 + baseTotal = total;
361 + root.counts = counts.map(x => ({...x}));
362 + }
363 +
364 + let frontier = [root];
365 +
366 + for (let depth = 1; depth <= n; depth++) {
367 + const next = [];
368 + for (const parent of frontier) {
369 + let probs = [];
370 +
371 + if (!withoutReplacement) {
372 + probs = items.map(it => ({ name: it.name, p: it.p }));
373 + } else {
374 + const countsHere = parent.counts;
375 + const totalHere = countsHere.reduce((s, x) => s + x.count, 0n);
376 + for (const x of countsHere) {
377 + if (x.count <= 0n) continue;
378 + probs.push({ name: x.name, p: normRat(x.count, totalHere) });
379 + }
380 + }
381 +
382 + for (const pr of probs) {
383 + const id = `${parent.id}-${depth}-${pr.name}`;
384 + const child = {
385 + id,
386 + depth,
387 + label: pr.name,
388 + parent,
389 + edgeP: pr.p,
390 + pathP: mulRat(parent.pathP, pr.p),
391 + pathLabels: parent.pathLabels.concat(pr.name),
392 + counts: null
393 + };
394 +
395 + if (withoutReplacement) {
396 + const countsChild = parent.counts.map(x => ({...x}));
397 + const idx = countsChild.findIndex(x => x.name === pr.name);
398 + if (idx >= 0) countsChild[idx].count = countsChild[idx].count - 1n;
399 + child.counts = countsChild;
400 + }
401 +
402 + nodes.push(child);
403 + edges.push({ from: parent, to: child, p: pr.p });
404 + next.push(child);
405 + }
406 + }
407 + frontier = next;
408 + if (frontier.length === 0) break;
409 + }
410 +
411 + return { nodes, edges, baseCounts, baseTotal };
412 + }
413 +
414 + // ========= Layout =========
415 + function layoutTree(nodes, edges, maxDepth) {
416 + const byDepth = new Map();
417 + for (const node of nodes) {
418 + if (!byDepth.has(node.depth)) byDepth.set(node.depth, []);
419 + byDepth.get(node.depth).push(node);
420 + }
421 +
422 + const children = new Map();
423 + for (const e of edges) {
424 + if (!children.has(e.from.id)) children.set(e.from.id, []);
425 + children.get(e.from.id).push(e.to);
426 + }
427 +
428 + const leaves = nodes.filter(n => (children.get(n.id) || []).length === 0);
429 + const maxLeaves = Math.max(1, leaves.length);
430 + const yStep = Math.max(22, Math.min(34, Math.floor(680 / maxLeaves)));
431 +
432 + leaves.forEach((leaf, i) => leaf._leafY = i * yStep);
433 +
434 + for (let d = maxDepth - 1; d >= 0; d--) {
435 + const level = byDepth.get(d) || [];
436 + for (const node of level) {
437 + const kids = children.get(node.id) || [];
438 + if (kids.length === 0) continue;
439 + const ys = kids.map(k => k._leafY);
440 + node._leafY = ys.reduce((a,b)=>a+b,0) / ys.length;
441 + }
442 + }
443 +
444 + const allY = nodes.map(n => n._leafY ?? 0);
445 + const minY = Math.min(...allY);
446 + const maxY = Math.max(...allY);
447 + const h = Math.max(1, maxY - minY);
448 +
449 + const padLeft = 80, padTop = 80;
450 + const xStep = 220;
451 +
452 + for (const node of nodes) {
453 + node.x = padLeft + node.depth * xStep;
454 + node.y = padTop + ((node._leafY - minY) / h) * 720;
455 + if (!isFinite(node.y)) node.y = 400;
456 + }
457 +
458 + const width = padLeft + (maxDepth + 1) * xStep + 420;
459 + const height = padTop + 900;
460 + return { width, height, children };
461 + }
462 +
463 + // ========= Render =========
464 + function clear(el) { while (el.firstChild) el.removeChild(el.firstChild); }
465 +
466 + function updateStats(items, sum, total) {
467 + kVal.textContent = items.length ? items.length.toString() : "—";
468 + sumProb.textContent = items.length ? `${ratToString(sum)} ≈ ${ratToDecimal(sum)}` : "—";
469 +
470 + const n = parseInt(nSlider.value, 10);
471 + const k = items.length || 0;
472 +
473 + if (k > 0) {
474 + let pow = 1n;
475 + for (let i = 0; i < n; i++) pow *= BI(k);
476 + leafCount.textContent = withoutEl.checked ? `${pow.toString()} (max.)` : pow.toString();
477 + } else {
478 + leafCount.textContent = "—";
479 + }
480 +
481 + totalMass.textContent = withoutEl.checked
482 + ? (total != null ? total.toString() : "—")
483 + : "—";
484 + }
485 +
486 + function renderAll() {
487 + parseWarn.style.display = "none";
488 + depthWarn.style.display = "none";
489 +
490 + const { items, errors, sum } = parseOutcomes(outcomesEl.value);
491 +
492 + if (errors.length) {
493 + parseWarn.style.display = "block";
494 + parseWarn.textContent = errors.join(" | ");
495 + clear(viewport);
496 + updateStats(items, sum, null);
497 + return;
498 + }
499 + if (items.length < 2) {
500 + parseWarn.style.display = "block";
501 + parseWarn.textContent = "Bitte mindestens 2 Ergebnisse eingeben.";
502 + clear(viewport);
503 + updateStats(items, sum, null);
504 + return;
505 + }
506 +
507 + if (!(sum.num === sum.den)) {
508 + parseWarn.style.display = "block";
509 + parseWarn.textContent =
510 + `Hinweis: Summe ist ${ratToString(sum)} (sollte 1/1 sein). Zeichne trotzdem.`;
511 + }
512 +
513 + const withoutReplacement = withoutEl.checked;
514 + modeLabel.textContent = withoutReplacement ? "Ohne Zurücklegen" : "Mit Zurücklegen";
515 +
516 + let n = parseInt(nSlider.value, 10);
517 +
518 + // Limit depth for without replacement
519 + let baseTotal = null;
520 + if (withoutReplacement) {
521 + const tmp = probsToCounts(items);
522 + baseTotal = tmp.total;
523 +
524 + if (baseTotal <= 0n) {
525 + depthWarn.style.display = "block";
526 + depthWarn.textContent = "Ohne Zurücklegen: Gesamtbestand ist 0 (prüfe Eingabe).";
527 + } else if (n > Number(baseTotal)) {
528 + n = Number(baseTotal);
529 + nSlider.value = String(n);
530 + depthWarn.style.display = "block";
531 + depthWarn.textContent =
532 + `Ohne Zurücklegen: Tiefe auf ${n} begrenzt (Gesamtbestand ${baseTotal.toString()}).`;
533 + }
534 + }
535 +
536 + nVal.textContent = n;
537 + kInfo.textContent = `k=${items.length}`;
538 +
539 + const { nodes, edges, baseTotal: bt } = buildTree(items, n, withoutReplacement);
540 +
541 + const maxDepth = Math.max(...nodes.map(x => x.depth));
542 + const { width, height, children } = layoutTree(nodes, edges, maxDepth);
543 +
544 + svg.setAttribute("viewBox", `0 0 ${Math.max(1400, width)} ${Math.max(900, height)}`);
545 + clear(viewport);
546 +
547 + // Draw edges (to circle boundaries) with robust stroke attributes
548 + for (const e of edges) {
549 + const rFrom = e.from.depth === 0 ? 18 : 14;
550 + const rTo = e.to.depth === 0 ? 18 : 14;
551 +
552 + const p = edgeEndpoints(e.from, e.to, rFrom, rTo);
553 +
554 + const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
555 + line.setAttribute("x1", p.x1);
556 + line.setAttribute("y1", p.y1);
557 + line.setAttribute("x2", p.x2);
558 + line.setAttribute("y2", p.y2);
559 +
560 + line.setAttribute("stroke", "rgba(233,238,252,0.9)");
561 + line.setAttribute("stroke-width", "2.8");
562 + line.setAttribute("stroke-linecap", "round");
563 +
564 + viewport.appendChild(line);
565 +
566 + const tx = (p.x1 + p.x2) / 2;
567 + const ty = (p.y1 + p.y2) / 2 - 8;
568 +
569 + const label = document.createElementNS("http://www.w3.org/2000/svg", "text");
570 + label.setAttribute("x", tx);
571 + label.setAttribute("y", ty);
572 + label.setAttribute("text-anchor", "middle");
573 + label.setAttribute("class", "edgeLabel");
574 + label.textContent = ratToString(e.p);
575 + viewport.appendChild(label);
576 + }
577 +
578 + // Draw nodes + leaf prob as fraction only
579 + for (const node of nodes) {
580 + const g = document.createElementNS("http://www.w3.org/2000/svg", "g");
581 + g.setAttribute("data-id", node.id);
582 +
583 + const c = document.createElementNS("http://www.w3.org/2000/svg", "circle");
584 + c.setAttribute("cx", node.x);
585 + c.setAttribute("cy", node.y);
586 +
587 + const r = node.depth === 0 ? 18 : 14;
588 + c.setAttribute("r", r);
589 +
590 + const isLeaf = ((children.get(node.id) || []).length === 0);
591 +
592 + // Outline only, robust against CSS overrides
593 + // Alle Knoten gleich stylen
594 + c.setAttribute("fill", "none");
595 + c.setAttribute("stroke", "rgba(233,238,252,0.9)");
596 + c.setAttribute("stroke-width", "2.4");
597 +
598 + // Node text: write result IN the node (centered)
599 + const t = document.createElementNS("http://www.w3.org/2000/svg", "text");
600 + t.setAttribute("x", node.x);
601 + t.setAttribute("y", node.y + 4);
602 + t.setAttribute("text-anchor", "middle");
603 + t.setAttribute("class", "nodeText");
604 + t.textContent = node.depth === 0 ? "Start" : node.label;
605 +
606 + g.appendChild(c);
607 + g.appendChild(t);
608 + viewport.appendChild(g);
609 +
610 + // Leaf: only fraction at end of path
611 + if (isLeaf && node.depth > 0) {
612 + const lp = document.createElementNS("http://www.w3.org/2000/svg", "text");
613 + lp.setAttribute("x", node.x + 36);
614 + lp.setAttribute("y", node.y + 4);
615 + lp.setAttribute("class", "leafProb");
616 + lp.textContent = ratToString(node.pathP);
617 + viewport.appendChild(lp);
618 + }
619 +
620 + // Tooltip
621 + g.addEventListener("mousemove", (ev) => {
622 + const rect = canvas.getBoundingClientRect();
623 + tip.style.left = (ev.clientX - rect.left + 12) + "px";
624 + tip.style.top = (ev.clientY - rect.top + 12) + "px";
625 + tip.style.display = "block";
626 +
627 + tipTitle.textContent = node.depth === 0 ? "Start" : `Knoten: ${node.label}`;
628 + const path = node.pathLabels.length ? node.pathLabels.join(" → ") : "(leer)";
629 + const edgeStr = node.edgeP ? `${ratToString(node.edgeP)} ≈ ${ratToDecimal(node.edgeP)}` : "—";
630 + const pathStr = `${ratToString(node.pathP)} ≈ ${ratToDecimal(node.pathP)}`;
631 +
632 + let extra = "";
633 + if (withoutReplacement && node.counts) {
634 + const remTotal = node.counts.reduce((s,x)=>s+x.count,0n);
635 + const parts = node.counts.map(x => `${x.name}: ${x.count.toString()}`).join(", ");
636 + extra = `<div style="margin-top:6px;"><b>Restbestand:</b> ${remTotal.toString()}<br><code>${parts}</code></div>`;
637 + }
638 +
639 + tipBody.innerHTML =
640 + `<div><b>Stufe:</b> ${node.depth}/${maxDepth}</div>` +
641 + `<div><b>Pfad:</b> <code>${path}</code></div>` +
642 + `<div><b>Kanten-Wahrscheinlichkeit:</b> ${edgeStr}</div>` +
643 + `<div><b>Pfad-Wahrscheinlichkeit:</b> ${pathStr}</div>` +
644 + extra;
645 + });
646 + g.addEventListener("mouseleave", () => { tip.style.display = "none"; });
647 + }
648 +
649 + updateStats(items, sum, bt ?? baseTotal);
650 + }
651 +
652 + // ========= Controls =========
653 + function updateAndRender() {
654 + nVal.textContent = nSlider.value;
655 + renderAll();
656 + }
657 +
658 + outcomesEl.addEventListener("input", () => {
659 + window.clearTimeout(outcomesEl._t);
660 + outcomesEl._t = window.setTimeout(renderAll, 150);
661 + });
662 +
663 + withoutEl.addEventListener("change", renderAll);
664 + nSlider.addEventListener("input", updateAndRender);
665 + btnRerender.addEventListener("click", renderAll);
666 +
667 + btnReset.addEventListener("click", resetView);
668 + btnFit.addEventListener("click", resetView);
669 +
670 + btnDownload.addEventListener("click", () => {
671 + const serializer = new XMLSerializer();
672 + const svgClone = svg.cloneNode(true);
673 +
674 + // explizite Größe setzen (wichtig für PNG)
675 + const vb = svg.viewBox.baseVal;
676 + svgClone.setAttribute("width", vb.width);
677 + svgClone.setAttribute("height", vb.height);
678 +
679 + const svgData = serializer.serializeToString(svgClone);
680 + const svgBlob = new Blob([svgData], { type: "image/svg+xml;charset=utf-8" });
681 + const url = URL.createObjectURL(svgBlob);
682 +
683 + const img = new Image();
684 + img.onload = () => {
685 + const canvas = document.createElement("canvas");
686 + canvas.width = vb.width;
687 + canvas.height = vb.height;
688 +
689 + const ctx = canvas.getContext("2d");
690 +
691 + // Hintergrundfarbe (weiß statt transparent)
692 + ctx.fillStyle = "#ffffff";
693 + ctx.fillRect(0, 0, canvas.width, canvas.height);
694 +
695 + ctx.drawImage(img, 0, 0);
696 +
697 + URL.revokeObjectURL(url);
698 +
699 + canvas.toBlob((blob) => {
700 + const a = document.createElement("a");
701 + a.href = URL.createObjectURL(blob);
702 + a.download = "wahrscheinlichkeitsbaum.png";
703 + document.body.appendChild(a);
704 + a.click();
705 + a.remove();
706 + }, "image/png");
707 + };
708 +
709 + img.src = url;
710 +});
711 +
712 + // Toggle entire box
713 + document.getElementById("toggleBox").addEventListener("click", (e) => {
714 + if (e.target.id === "without") return;
715 + withoutEl.checked = !withoutEl.checked;
716 + renderAll();
717 + });
718 +
719 + // Pan
720 + svg.addEventListener("mousedown", (ev) => {
721 + isPanning = true;
722 + startX = ev.clientX;
723 + startY = ev.clientY;
724 + });
725 + window.addEventListener("mouseup", () => { isPanning = false; });
726 + window.addEventListener("mousemove", (ev) => {
727 + if (!isPanning) return;
728 + const dx = ev.clientX - startX;
729 + const dy = ev.clientY - startY;
730 + startX = ev.clientX;
731 + startY = ev.clientY;
732 + panX += dx;
733 + panY += dy;
734 + applyTransform();
735 + });
736 +
737 + // Zoom
738 + svg.addEventListener("wheel", (ev) => {
739 + ev.preventDefault();
740 + const delta = Math.sign(ev.deltaY);
741 + const factor = delta > 0 ? 0.9 : 1.1;
742 +
743 + const pt = svg.createSVGPoint();
744 + pt.x = ev.clientX; pt.y = ev.clientY;
745 + const ctm = svg.getScreenCTM();
746 + if (!ctm) return;
747 + const svgP = pt.matrixTransform(ctm.inverse());
748 +
749 + const prev = zoom;
750 + zoom = Math.max(0.2, Math.min(3.5, zoom * factor));
751 +
752 + panX = svgP.x - (svgP.x - panX) * (zoom / prev);
753 + panY = svgP.y - (svgP.y - panY) * (zoom / prev);
754 +
755 + applyTransform();
756 + }, { passive: false });
757 +
758 + // Init
759 + applyTransform();
760 + nVal.textContent = nSlider.value;
761 + renderAll();
762 +})();
763 +</script>
764 +</body>
765 +</html>