|
6 | 6 | <title>Collatz Reverse Tree – Interactive Visualizer with (m,k) toggle & PNG export</title> |
7 | 7 | <!-- vis-network core library --> |
8 | 8 | <script type="text/javascript" src="https://unpkg.com/vis-network@9.1.6/dist/vis-network.min.js"></script> |
| 9 | + <script type="text/javascript" src="collatz-math.js"></script> |
9 | 10 | <style> |
10 | 11 | * { |
11 | 12 | box-sizing: border-box; |
|
192 | 193 | <div class="toolbar"> |
193 | 194 | <div class="control-group"> |
194 | 195 | <label>🔢 Max steps (depth):</label> |
195 | | - <input type="number" id="stepInput" min="1" max="12" value="4" step="1" style="width: 100px;"> |
| 196 | + <input type="number" id="stepInput" min="1" max="30" value="4" step="1" style="width: 100px;"> |
196 | 197 | <button id="generateBtn">⟳ Generate / Refresh Tree</button> |
197 | 198 | </div> |
198 | 199 | <button id="toggleDisplayBtn" class="toggle-btn">🔄 Toggle (m,k) Coordinates</button> |
199 | | - <button id="exportPngBtn" class="export-btn">📸 Export HD PNG (Full View)</button> |
| 200 | + <button id="exportPngBtn" class="export-btn">📸 Export PNG (Fitted View)</button> |
200 | 201 | <div class="info-panel" id="infoPanel"> |
201 | | - 📘 <strong>Collatz Reverse Tree</strong> · growing backwards from 1<br> |
202 | | - 🟢 Click any node to show its path · Switch to (m,k) mode |
| 202 | + 📘 <strong>Cycle-pruned Collatz Reverse Tree</strong> · growing backwards from 1<br> |
| 203 | + 🟢 Click any node to show its forward trajectory to 1 · Switch to (m,k) mode |
203 | 204 | </div> |
204 | 205 | <div class="stats" id="statsArea"> |
205 | 206 | Nodes: — | Edges: — |
|
222 | 223 | </div> |
223 | 224 | </div> |
224 | 225 | <div class="footer"> |
225 | | - 💡 Reverse rules: predecessors are 2x and (x‑1)/3 (if odd >1) | <span class="warning">⚠️ Depth ≥8 may produce >1500 nodes, export may be slow | Toggle (m,k): for any n, m = floor(n/6), k = n mod 6</span> |
| 226 | + 💡 Reverse rules: predecessors are 2x and (x−1)/3 when the latter is a positive odd integer. The edge 4 → 1 is omitted to break the 1–4–2 cycle. | <span class="warning">⚠️ Depth 27 produces 1,748 nodes in this implementation; deeper trees may render slowly. PNG export captures the fitted viewport. | (m,k): m = floor(n/6), k = n mod 6</span> |
226 | 227 | </div> |
227 | 228 | </div> |
228 | 229 |
|
|
232 | 233 | // Returns { nodes, edges, parentMap, stepsMap } |
233 | 234 | // -------------------------------------------------------------- |
234 | 235 | function generateCollatzTree(maxSteps) { |
235 | | - let stepsMap = new Map(); // number -> step count from root 1 |
236 | | - let parentMap = new Map(); // child -> parent |
237 | | - |
238 | | - if (maxSteps < 0) return { nodes: [], edges: [], parentMap, stepsMap }; |
239 | | - |
240 | | - stepsMap.set(1, 0); |
241 | | - parentMap.set(1, null); |
242 | | - let queue = [1]; |
243 | | - |
244 | | - while (queue.length > 0) { |
245 | | - let current = queue.shift(); |
246 | | - let currentStep = stepsMap.get(current); |
247 | | - if (currentStep >= maxSteps) continue; |
248 | | - |
249 | | - let pred1 = 2 * current; |
250 | | - let pred2 = null; |
251 | | - if ((current - 1) % 3 === 0) { |
252 | | - let candidate = (current - 1) / 3; |
253 | | - if (candidate % 2 === 1 && candidate !== 1) { |
254 | | - pred2 = candidate; |
255 | | - } |
256 | | - } |
257 | | - |
258 | | - [pred1, pred2].forEach(pred => { |
259 | | - if (pred === null) return; |
260 | | - if (!stepsMap.has(pred)) { |
261 | | - stepsMap.set(pred, currentStep + 1); |
262 | | - parentMap.set(pred, current); |
263 | | - queue.push(pred); |
264 | | - } else if (stepsMap.get(pred) > currentStep + 1) { |
265 | | - stepsMap.set(pred, currentStep + 1); |
266 | | - parentMap.set(pred, current); |
267 | | - queue.push(pred); |
268 | | - } |
269 | | - }); |
270 | | - } |
| 236 | + const { stepsMap, parentMap } = CollatzMath.generateReverseTree(1, maxSteps, { |
| 237 | + pruneRootCycle: true |
| 238 | + }); |
271 | 239 |
|
272 | 240 | let nodes = []; |
273 | 241 | let edges = []; |
|
282 | 250 | for (let [child, parent] of parentMap.entries()) { |
283 | 251 | if (parent !== null && nodeSet.has(child) && nodeSet.has(parent)) { |
284 | 252 | if (stepsMap.get(child) <= maxSteps && stepsMap.get(parent) <= maxSteps) { |
285 | | - edges.push({ from: parent, to: child }); |
| 253 | + edges.push({ id: `${parent}->${child}`, from: parent, to: child }); |
286 | 254 | } |
287 | 255 | } |
288 | 256 | } |
|
312 | 280 |
|
313 | 281 | // ---------- Helper: format number as (m,k) ---------- |
314 | 282 | function formatNumberToMK(n) { |
315 | | - const m = Math.floor(n / 6); |
316 | | - const k = n % 6; |
| 283 | + const { m, k } = CollatzMath.toMK(n); |
317 | 284 | return `(${m},${k})`; |
318 | 285 | } |
319 | 286 |
|
|
633 | 600 | } |
634 | 601 |
|
635 | 602 | // -------------------------------------------------------------- |
636 | | - // Show path from node to root 1 (always raw numbers) |
| 603 | + // Show the forward Collatz trajectory from the selected node to root 1 |
637 | 604 | // -------------------------------------------------------------- |
638 | 605 | function showNodePath(nodeId) { |
639 | 606 | if (!currentParentMap || !currentStepsMap) { |
|
652 | 619 | cur = currentParentMap.get(cur); |
653 | 620 | if (cur === null) break; |
654 | 621 | } |
655 | | - path.reverse(); |
656 | 622 | const stepCount = currentStepsMap.get(nodeId) || (path.length - 1); |
657 | 623 |
|
658 | 624 | let pathStr = path.join(" → "); |
659 | 625 | const isLong = pathStr.length > 260; |
660 | 626 | if (isLong) { |
661 | | - pathStr = pathStr.slice(0, 240) + "… → " + nodeId; |
| 627 | + pathStr = pathStr.slice(0, 240) + "… → 1"; |
662 | 628 | } |
663 | 629 |
|
664 | 630 | const infoDiv = document.getElementById('infoPanel'); |
665 | 631 | if (infoDiv) { |
666 | | - infoDiv.innerHTML = `🔍 <strong>Path details</strong> · Number ${nodeId} to 1<br> |
| 632 | + infoDiv.innerHTML = `🔍 <strong>Forward Collatz trajectory</strong> · ${nodeId} to 1<br> |
667 | 633 | 📌 Steps: ${stepCount}<br> |
668 | 634 | 🧩 Trajectory: ${pathStr}<br> |
669 | | - <span style="font-size:0.7rem;">✨ Forward Collatz: odd→3x+1, even→x/2</span> |
670 | | - <br><span style="font-size:0.65rem; color:#8b5a2b;">💡 Tip: Toggle (m,k) display on the graph – path always shows raw numbers</span>`; |
| 635 | + <span style="font-size:0.7rem;">✨ Rule: odd → 3x+1, even → x/2</span> |
| 636 | + <br><span style="font-size:0.65rem; color:#8b5a2b;">💡 Graph arrows show reverse expansion; this trajectory follows them back toward the root.</span>`; |
671 | 637 | if (network) { |
672 | | - network.selectNodes([nodeId]); |
673 | | - setTimeout(() => { if(network) network.selectNodes([]); }, 1600); |
| 638 | + const pathEdges = []; |
| 639 | + for (let index = 0; index < path.length - 1; index += 1) { |
| 640 | + pathEdges.push(`${path[index + 1]}->${path[index]}`); |
| 641 | + } |
| 642 | + network.setSelection( |
| 643 | + { nodes: path, edges: pathEdges }, |
| 644 | + { unselectAll: true, highlightEdges: false } |
| 645 | + ); |
| 646 | + setTimeout(() => { if (network) network.unselectAll(); }, 2400); |
674 | 647 | } |
675 | 648 | } else { |
676 | | - alert(`Number ${nodeId} → 1 path (${stepCount} steps):\n${pathStr}`); |
| 649 | + alert(`Forward Collatz trajectory ${nodeId} → 1 (${stepCount} steps):\n${pathStr}`); |
677 | 650 | } |
678 | | - showStatusMessage(`Displayed path for node ${nodeId} to root`, false); |
| 651 | + showStatusMessage(`Displayed forward trajectory for node ${nodeId}`, false); |
679 | 652 | } |
680 | 653 |
|
681 | 654 | // -------------------------------------------------------------- |
|
698 | 671 | showStatusMessage('Please enter a valid step count (≥1)', true); |
699 | 672 | return; |
700 | 673 | } |
701 | | - if (maxSteps > 12) { |
702 | | - if (!confirm(`Depth ${maxSteps} may create >5000 nodes and freeze the browser. Recommended ≤10. Continue anyway?`)) { |
703 | | - stepInput.value = "6"; |
704 | | - renderTree(6); |
705 | | - return; |
706 | | - } |
707 | | - } else if (maxSteps >= 8) { |
708 | | - if (!confirm(`Depth ${maxSteps} will generate many nodes (>1500), rendering and export will be slower. Continue?`)) { |
709 | | - stepInput.value = "6"; |
710 | | - renderTree(6); |
| 674 | + if (maxSteps > 30) { |
| 675 | + showStatusMessage('Please enter a depth from 1 to 30; larger depths are disabled to keep integer arithmetic and rendering manageable', true); |
| 676 | + return; |
| 677 | + } |
| 678 | + if (maxSteps >= 27) { |
| 679 | + if (!confirm(`Depth 27 produces 1,748 nodes, and deeper values grow quickly. Rendering and export may be slow. Continue with depth ${maxSteps}?`)) { |
711 | 680 | return; |
712 | 681 | } |
713 | 682 | } |
|
0 commit comments