Änderungen von Dokument Interaktiv Behälter füllen

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

Von Version 4.1
bearbeitet von Stefan Martin
am 2025/12/18 09:59
Änderungskommentar: Es gibt keinen Kommentar für diese Version
Auf Version 2.1
bearbeitet von Stefan Martin
am 2025/12/18 09:48
Ä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('baumdiagramm.html')"
3 + src="$doc.getAttachmentURL('wahrscheinlichkeitsbaum.html')"
4 4   width="100%"
5 5   height="900px"
6 6   style="border:1px solid #ccc;">
baumdiagramm.html
Author
... ... @@ -1,1 +1,0 @@
1 -XWiki.smartin
Größe
... ... @@ -1,1 +1,0 @@
1 -24.3 KB
Inhalt
... ... @@ -1,765 +1,0 @@
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>