From 8a6c6a04765c0af22ed3ee31fc179889ce5cc945 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 04:46:12 +0000 Subject: [PATCH] Reset the engine after a template builds, not before Loading a starter template left the step-0 history entry empty, so every chart opened on a hole. Run the Predator & Prey template straight after loading it and the Rabbits series came back [null, 89, 94, 93, 87, 76] with the starting population of 80 nowhere in it and the curve beginning at step 1. The order was the whole of it. Both loaders called _clearAll, which resets the engine, and only then ran the template's load(). reset() ends by recording a step-0 baseline of the current nodes, so it captured an empty diagram; the nodes arrived afterwards and nothing reset again. The saved-diagram loader already had this right, clearing, loading, then resetting. Reset now happens once the template has built its nodes. Beyond the missing first chart point, that baseline is what scrub shows at step 0 and what spike attribution reads as the "from" value for the first real step, so all three were working from nothing. Both entry points had their own copy of the same nine lines, which is how one bug came to live in two places, so they now share _installTemplate and each keeps only its own last step: the Library hides its modal, the welcome demo re-renders the properties panel. Covered by a smoke test driving both entry points, asserting every node appears in the step-0 baseline and that a stepped series has no hole at index 0. Without the fix it reports step0Entries 0 and firstIsNull true for both. It calls _loadTemplate and _loadDemo rather than the new shared helper, so it measures the behaviour and not the presence of a function. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SH7ouoNymubArm7VEgALHg --- js/app-library.js | 17 +++++++++++++++-- js/app.js | 9 +-------- test/smoke.js | 31 +++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 10 deletions(-) diff --git a/js/app-library.js b/js/app-library.js index bb1d111..2929a56 100644 --- a/js/app-library.js +++ b/js/app-library.js @@ -225,16 +225,29 @@ class AppLibrary { } } - async _loadTemplate(t) { - if (!await this._confirmGuard(`Load "${t.name}"? Your current diagram will be replaced (Ctrl+Z to undo).`, 'Load template')) return; + // Swap the current diagram for a starter template. Shared with _loadDemo so + // the two cannot drift; each caller adds its own last step. + _installTemplate(t) { const prev = this._snapshot(); this._clearAll(); t.load(); this.diagram.meta.name = t.name; this.diagram.meta.description = t.desc; this._applyMeta(); + // Reset AFTER the template has built its nodes, not before. _clearAll resets + // too, but that runs against an empty diagram, so the step-0 baseline it + // records holds nothing. Leaving it that way meant the first point of every + // chart was a hole: run a freshly loaded template and the series began at + // step 1, so the starting amounts never plotted and spike attribution had no + // "from" value for the first step. + this.engine.reset(); this._commitReplace(prev); this.renderer.fitView(); + } + + async _loadTemplate(t) { + if (!await this._confirmGuard(`Load "${t.name}"? Your current diagram will be replaced (Ctrl+Z to undo).`, 'Load template')) return; + this._installTemplate(t); this._hideModal('lib-overlay'); } diff --git a/js/app.js b/js/app.js index 3500e7a..aa9feee 100644 --- a/js/app.js +++ b/js/app.js @@ -850,14 +850,7 @@ class App { _loadDemo() { const t = this._templates[0]; // Predator & Prey if (!t) return; - const prev = this._snapshot(); - this._clearAll(); - t.load(); - this.diagram.meta.name = t.name; - this.diagram.meta.description = t.desc; - this._applyMeta(); - this._commitReplace(prev); - this.renderer.fitView(); + this._installTemplate(t); this._renderProps(); } diff --git a/test/smoke.js b/test/smoke.js index c061694..77e0f93 100644 --- a/test/smoke.js +++ b/test/smoke.js @@ -175,6 +175,37 @@ const URL = process.env.SMOKE_URL || 'http://localhost:8080/'; if (n > 0) ok(`template "${name}" loaded (${n} nodes)`); else fail(`template "${name}" empty`); } + // Loading a template has to reset the engine AFTER the template builds its + // nodes. _clearAll resets too, but against an empty diagram, so the step-0 + // baseline it writes is empty and every chart series opens on a hole: the + // starting amounts never plot and the curve begins at step 1. Both entry + // points (Library template, welcome demo) go through _installTemplate. + const step0 = await page.evaluate(async () => { + const out = {}; + // Drive the real entry points, not the shared helper, so this test measures + // behaviour rather than the presence of a particular function. + for (const [label, load] of [['template', () => window.app._loadTemplate(window.app._templates[0])], + ['demo', () => window.app._loadDemo()]]) { + await load(); + const e = window.app.engine, d = window.app.diagram; + const h0 = e.history[0]; + const snap = h0 ? h0.snap : null; + const ids = snap ? Object.keys(snap) : []; + // Every node the template built must appear in the step-0 baseline. + const covered = d.nodes.size > 0 && [...d.nodes.keys()].every(id => snap && snap[id] !== undefined); + // And stepping must leave no hole at index 0 of a series. + const probe = [...d.nodes.values()][0]; + for (let i = 0; i < 3; i++) e.doStep(); + const series = e.history.map(h => (h.snap && h.snap[probe.id] !== undefined) ? h.snap[probe.id] : null); + out[label] = { nodes: d.nodes.size, step0Entries: ids.length, covered, firstIsNull: series[0] === null, len: series.length }; + } + return out; + }); + if (step0.template.covered && !step0.template.firstIsNull + && step0.demo.covered && !step0.demo.firstIsNull) + ok('template/demo load records a step-0 baseline, so charts start at step 0 instead of a hole'); + else fail('template step-0 baseline: ' + JSON.stringify(step0)); + // Navigation: zoom controls step the scale and update the readout; fit-to-content // re-frames without error. const nav = await page.evaluate(() => {