Änderungen von Dokument Interaktiv Behälter füllen
Zuletzt geändert von Holger Engels am 2025/12/18 12:53
Von Version 2.1
bearbeitet von Stefan Martin
am 2025/12/18 09:48
am 2025/12/18 09:48
Änderungskommentar:
Es gibt keinen Kommentar für diese Version
Auf Version 7.1
bearbeitet von Holger Engels
am 2025/12/18 12:53
am 2025/12/18 12:53
Änderungskommentar:
Es gibt keinen Kommentar für diese Version
Zusammenfassung
-
Seiteneigenschaften (2 geändert, 0 hinzugefügt, 0 gelöscht)
-
Anhänge (0 geändert, 1 hinzugefügt, 0 gelöscht)
Details
- Seiteneigenschaften
-
- Dokument-Autor
-
... ... @@ -1,1 +1,1 @@ 1 -XWiki. smartin1 +XWiki.holgerengels - Inhalt
-
... ... @@ -1,8 +1,11 @@ 1 -{{html clean="false"}} 1 +{{velocity}} 2 +{{html clean="false" wiki="true"}} 2 2 <iframe 3 - src="$doc.getAttachmentURL(' wahrscheinlichkeitsbaum.html')"4 + src="$doc.getAttachmentURL('baumdiagramm.html')" 4 4 width="100%" 5 5 height="900px" 6 6 style="border:1px solid #ccc;"> 7 7 </iframe> 8 8 {{/html}} 10 +{{/velocity}} 11 +
- 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>