Upload 3 files
Browse files- LICENSE +21 -0
- README.md +86 -5
- index.html +98 -17
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2026 Hugdrigo
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.md
CHANGED
|
@@ -1,12 +1,93 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: blue
|
| 5 |
-
colorTo:
|
| 6 |
sdk: static
|
|
|
|
| 7 |
pinned: false
|
| 8 |
license: mit
|
| 9 |
-
short_description:
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: ImpactLedger
|
| 3 |
+
emoji: 🌱
|
| 4 |
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
sdk: static
|
| 7 |
+
app_file: index.html
|
| 8 |
pinned: false
|
| 9 |
license: mit
|
| 10 |
+
short_description: Program impact and cost simulator.
|
| 11 |
---
|
| 12 |
|
| 13 |
+
# ImpactLedger
|
| 14 |
+
|
| 15 |
+
### Program Impact Intelligence Studio | Hugdrigo Portfolio Project #106
|
| 16 |
+
|
| 17 |
+
**ImpactLedger** is a self-contained, interactive educational simulator for comparing the costs, reach, observed outcome-rate changes and *assumed* attribution of fictional social programs. It includes a signed incremental-outcome model, interactive scenarios, reproducible Monte Carlo-style assumption shocks, a locally editable registry and portable JSON/CSV exports. It runs directly in the browser, with no backend or paid API.
|
| 18 |
+
|
| 19 |
+
**Short description:** Program impact and cost simulator.
|
| 20 |
+
|
| 21 |
+
> **Critical interpretation note:** These are synthetic, invented programs and illustrative calculations. A before-and-after rate difference does **not** establish causal impact. The attribution percentages are user-defined assumptions, not estimated causal effects. The simulated outcome units are only comparable because this artificial dataset treats them as standardized. Never aggregate real health, education and employment outcomes as if they were interchangeable without a validated common framework. This is not a randomized evaluation, a causal-inference system, a certified SROI calculation, or financial advice.
|
| 22 |
+
|
| 23 |
+
## Publishing to Hugging Face
|
| 24 |
+
|
| 25 |
+
1. Open [Create a new Space](https://huggingface.co/new-space).
|
| 26 |
+
2. Set owner to **`Hugdrigo`** and name to **`impactledger-program-intelligence`**.
|
| 27 |
+
3. Select **Static** SDK, **Blank** template, **Public** visibility, and **MIT** license.
|
| 28 |
+
4. Extract the downloaded ZIP and upload the three files to the **Space repository root**, not a subfolder: `README.md`, `index.html` and `LICENSE`.
|
| 29 |
+
5. Commit the changes and open the **App** tab.
|
| 30 |
+
|
| 31 |
+
The `README.md` front matter uses Hugging Face-compatible colors: `colorFrom: blue`, `colorTo: green`. The Space does not require Docker, GPUs, a billing account, package installs, network access or secrets.
|
| 32 |
+
|
| 33 |
+
## Features
|
| 34 |
+
|
| 35 |
+
| Area | Included functions |
|
| 36 |
+
|---|---|
|
| 37 |
+
| Impact overview | Budget, reach, signed modeled incremental outcomes, cost per positive outcome, budget allocation and program comparison |
|
| 38 |
+
| Scenario studio | Four presets, four interactive controls, scenario vs reference outcomes, per-program cost efficiency |
|
| 39 |
+
| Program registry | Eight fictional example programs, add/edit (up to 120), search, archive/restore, input bounds, CSV export, JSON backup and restore |
|
| 40 |
+
| Uncertainty lab | 400 reproducible seeded draws, synthetic P10/P50/P90, outcome-sensitivity comparison and methodology warnings |
|
| 41 |
+
| Methodology | Exact equations, assumptions, interpretation cautions and program catalog |
|
| 42 |
+
| UI | Responsive layout, dark mode, local-first storage and offline operation |
|
| 43 |
+
|
| 44 |
+
### Scenario presets
|
| 45 |
+
|
| 46 |
+
- **Baseline:** no adjustments.
|
| 47 |
+
- **Funding squeeze:** 15% reduced reach, -3 percentage points outcome rate, -20% budget, -5 percentage points attribution.
|
| 48 |
+
- **Delivery boost:** +35% reach, +6 percentage points outcome rate, +20% budget, +5 points attribution.
|
| 49 |
+
- **Measurement stress:** -10% reach, -8 points outcome rate, +15% cost, -20 points attribution.
|
| 50 |
+
|
| 51 |
+
Controls support reach -60% to +100%, measured outcome-rate changes -20 to +20 percentage points, budget -40% to +100%, and attribution -40 to +40 percentage points. Percentages are clamped to 0–100% where applicable.
|
| 52 |
+
|
| 53 |
+
## Transparent equations
|
| 54 |
+
|
| 55 |
+
For one fictional program:
|
| 56 |
+
|
| 57 |
+
```text
|
| 58 |
+
Adjusted reach = reach × (1 + reach_change_percent / 100)
|
| 59 |
+
Adjusted budget = budget × (1 + budget_change_percent / 100)
|
| 60 |
+
Adjusted follow-up rate = clamp(follow_up_rate + rate_change_pp, 0, 100)
|
| 61 |
+
Adjusted attribution = clamp(attribution_percent + attribution_change_pp, 0, 100)
|
| 62 |
+
Observed change (percentage points) = adjusted_follow_up_rate - baseline_rate
|
| 63 |
+
Modeled signed incremental outcomes = adjusted_reach × observed_change / 100 × adjusted_attribution / 100
|
| 64 |
+
Cost per positive incremental outcome = adjusted_budget / modeled_incremental_outcomes, only when modeled_incremental_outcomes > 0
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
Negative outcome-rate differences yield negative additional outcomes. Non-positive incremental outcomes show cost effectiveness as `N/A` rather than misleading positive efficiency. Portfolio totals assume that programs and outcome units do not overlap.
|
| 68 |
+
|
| 69 |
+
### Synthetic uncertainty draws
|
| 70 |
+
|
| 71 |
+
The same deterministic seeded generator yields 400 draws with bounded invented reach, follow-up rate and attribution shocks. Percentiles are descriptive summaries of the *chosen simulation*, **not** calibrated uncertainty estimates, confidence intervals, counterfactual results or claims of actual impact.
|
| 72 |
+
|
| 73 |
+
### Local data and safety
|
| 74 |
+
|
| 75 |
+
- All computation runs in the browser; no third-party scripts, external tracking, cloud storage or network APIs.
|
| 76 |
+
- Data saves in browser localStorage when supported, and can be backed up and restored using JSON.
|
| 77 |
+
- JSON import checks backup schema/version, unique numeric IDs, field types, ranges, a 2 MB file-size limit and up to 120 records.
|
| 78 |
+
- Imported program names are HTML-escaped before display. CSV exports quote cells and protect against spreadsheet formula injection while preserving legitimate negative numbers.
|
| 79 |
+
- Browser storage is not encrypted. Do not enter sensitive participant or beneficiary information.
|
| 80 |
+
- Archiving preserves program records in saved state; `Reset demo` is a deliberate, confirmed replacement of local data. Back up first.
|
| 81 |
+
|
| 82 |
+
## Suggested evaluation workflow
|
| 83 |
+
|
| 84 |
+
Start with fictional examples, replace each assumption only with authorized aggregated data, inspect program-by-program changes, document your counterfactual/evaluation design independently, and use the scenario studio to test how strongly results depend on assumptions. Real impact reports require verified outcomes, evidence grading and causal identification.
|
| 85 |
+
|
| 86 |
+
## Technical implementation
|
| 87 |
+
|
| 88 |
+
- Standalone `index.html` with inline accessible HTML, CSS, JavaScript and SVG; no dependencies or build step.
|
| 89 |
+
- MIT license in `LICENSE`.
|
| 90 |
+
- Designed for Hugging Face Static Spaces.
|
| 91 |
+
- All findings and comparison values are illustrative; the application is a portfolio demonstration, not an outcome assessment service.
|
| 92 |
+
|
| 93 |
+
© 2026 Hugdrigo. Project #106.
|
index.html
CHANGED
|
@@ -1,19 +1,100 @@
|
|
| 1 |
<!doctype html>
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
</html>
|
|
|
|
| 1 |
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
| 5 |
+
<meta name="theme-color" content="#102b3c">
|
| 6 |
+
<meta name="description" content="ImpactLedger: a local-first, transparent program impact and cost-effectiveness simulation studio using fictional data.">
|
| 7 |
+
<title>ImpactLedger | Program Impact Intelligence Studio</title>
|
| 8 |
+
<style>
|
| 9 |
+
:root{color-scheme:light;--bg:#f3f7f9;--card:#ffffff;--card2:#f8fafb;--text:#203749;--muted:#728696;--line:#e1e8ed;--nav:#102b3c;--nav2:#1d4153;--blue:#3772d0;--blueSoft:#e9f1fc;--sea:#118b83;--seaSoft:#e2f5f0;--amber:#a66f26;--red:#c25458;--shadow:0 8px 30px rgba(25,61,82,.045)}
|
| 10 |
+
body.dark{color-scheme:dark;--bg:#101e2a;--card:#182c3b;--card2:#213748;--text:#e7f2f7;--muted:#9db4c2;--line:#344f5d;--nav:#091a27;--nav2:#203d4e;--blue:#9ec4ff;--blueSoft:#273f5b;--sea:#65dac7;--seaSoft:#244a4d;--amber:#f1bd7d;--red:#ffa1a4;--shadow:none}
|
| 11 |
+
*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:var(--bg);color:var(--text);font:13px/1.55 system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}button,input,select{font:inherit}button{cursor:pointer}button:disabled{opacity:.45;cursor:not-allowed}a{color:var(--blue)}button:focus-visible,input:focus-visible,select:focus-visible{outline:3px solid #62b0ff;outline-offset:2px}.shell{min-height:100vh;display:grid;grid-template-columns:248px minmax(0,1fr)}.side{position:sticky;top:0;min-height:100vh;height:100vh;background:var(--nav);color:#acc4ce;padding:26px 17px 18px;display:flex;flex-direction:column}.identity{display:flex;gap:12px;align-items:center;padding:2px 7px;margin-bottom:38px}.emblem{width:45px;height:45px;background:linear-gradient(140deg,#9ef2d6,#79c4f5);color:#0c3544;border-radius:14px;display:grid;place-items:center;font-size:28px;font-weight:900;box-shadow:0 8px 22px #72dbc831}.identity b{color:#fff;font-size:21px;letter-spacing:-1.2px;display:block;line-height:1.05}.identity small{font-size:9px;font-weight:800;letter-spacing:1.65px;color:#91b8c8}.nav-title{font-size:10px;color:#769cad;letter-spacing:2px;font-weight:850;margin:0 10px 11px}.nav{display:grid;gap:5px}.nav button{width:100%;display:flex;align-items:center;gap:12px;padding:12px 13px;border:0;background:none;border-radius:11px;color:#b0cad4;text-align:left;font-size:12px;font-weight:750}.nav .glyph{font-size:17px;width:24px;text-align:center}.nav button.active,.nav button:hover{color:#fff;background:#25495c}.nav button.active{box-shadow:inset 3px 0 #72dec7}.side-end{margin-top:auto;padding:14px;border-radius:13px;background:#204154;font-size:11px;line-height:1.7}.side-end b{display:block;color:#fff;margin-bottom:3px}.online{height:8px;width:8px;display:inline-block;background:#7ff2cb;border-radius:50%;margin-right:7px}
|
| 12 |
+
main{min-width:0;max-width:1610px;width:100%;padding:24px clamp(17px,3vw,45px) 26px}.top{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:26px}.crumb{font-size:11px;font-weight:750;color:var(--muted)}.crumb strong{color:var(--text)}.actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.btn{background:var(--card);border:1px solid var(--line);color:var(--text);padding:10px 13px;white-space:nowrap;border-radius:10px;display:inline-flex;align-items:center;gap:7px;font-size:11px;font-weight:800}.btn:hover{filter:brightness(.96)}.btn.primary{background:#3179c5;border-color:#3179c5;color:#fff}.btn.subtle{background:var(--blueSoft);color:var(--blue);border-color:transparent}.btn.danger{color:var(--red)}.head{display:flex;justify-content:space-between;align-items:center;gap:15px;margin-bottom:22px}.eyebrow{color:var(--sea);font-size:10px;letter-spacing:2px;font-weight:900}.head h1{font-size:clamp(27px,3.2vw,39px);letter-spacing:-1.35px;margin:5px 0;line-height:1.15}.head p{max-width:790px;color:var(--muted);font-size:12px;margin:5px 0 0}.head-icon{height:65px;width:65px;border-radius:18px;background:var(--seaSoft);color:var(--sea);font-size:36px;display:grid;place-items:center}.hero{position:relative;overflow:hidden;border-radius:19px;background:linear-gradient(115deg,#173449 0%,#1b5870 57%,#16877a 100%);padding:27px 31px;color:white;margin-bottom:16px;min-height:164px;display:flex;align-items:center;justify-content:space-between}.hero:after{content:'';position:absolute;right:5%;top:-100px;height:355px;width:355px;border-radius:50%;border:1px solid #b2eff646;box-shadow:0 0 0 36px #b2eff610,0 0 0 75px #b2eff608;pointer-events:none}.hero b{font-size:10px;letter-spacing:2px;color:#a6f4e2}.hero h2{font-size:clamp(22px,3vw,34px);line-height:1.13;letter-spacing:-.95px;margin:11px 0}.hero p{font-size:12px;color:#d5e9ef;max-width:640px;margin:0}.hero-mark{font-size:83px;opacity:.55;margin-right:65px;position:relative;z-index:1}.grid4{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px;margin-bottom:16px}.stat,.panel{min-width:0;border-radius:15px;background:var(--card);border:1px solid var(--line);box-shadow:var(--shadow)}.stat{padding:18px}.stat .label{font-weight:850;letter-spacing:.8px;text-transform:uppercase;font-size:10px;color:var(--muted);display:flex;justify-content:space-between;gap:6px}.stat .label i{font-style:normal;background:var(--blueSoft);border-radius:8px;padding:5px;color:var(--blue);font-size:15px}.stat strong{display:block;font-size:clamp(21px,2.6vw,33px);margin:8px 0 1px;letter-spacing:-1px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-variant-numeric:tabular-nums}.stat small{color:var(--muted);font-size:10px}.columns{display:grid;grid-template-columns:minmax(0,1.45fr) minmax(0,1fr);gap:16px}.columns.equal{grid-template-columns:repeat(2,minmax(0,1fr))}.panel{padding:20px;margin-bottom:16px}.panel h3{font-size:15px;letter-spacing:-.3px;margin:0}.panel p.desc{font-size:11px;color:var(--muted);margin:4px 0 16px}.panel-title{display:flex;justify-content:space-between;gap:12px;align-items:center;margin-bottom:15px}.panel-title p{font-size:11px;color:var(--muted);margin:3px 0 0}.tag{font-size:10px;font-weight:800;padding:5px 9px;background:var(--seaSoft);color:var(--sea);border-radius:30px;white-space:nowrap}.muted{color:var(--muted)}.table-wrap{max-width:100%;overflow-x:auto}table{border-collapse:collapse;width:100%;font-size:11px;white-space:nowrap}th{text-align:left;color:var(--muted);font-size:9px;letter-spacing:.7px;text-transform:uppercase;background:var(--card2);font-weight:850}th,td{padding:12px 11px;border-bottom:1px solid var(--line)}tr:last-child td{border:0}th.right,td.right{text-align:right}td strong{font-size:11px}.tiny{font-size:10px}.positive{color:var(--sea);font-weight:850}.negative{color:var(--red);font-weight:850}.bar-row{margin:0 0 16px}.bar-heading{display:flex;justify-content:space-between;font-size:11px;gap:12px;margin-bottom:7px}.bar-heading b{font-size:11px}.track{background:var(--card2);height:11px;border-radius:15px;overflow:hidden}.track span{height:100%;display:block;background:linear-gradient(90deg,#52baa7,#3482c2);border-radius:inherit;min-width:0}.progress{display:flex;gap:8px;align-items:center;font-size:10px;color:var(--muted)}.quality{display:inline-flex;padding:3px 8px;border-radius:20px;background:var(--blueSoft);color:var(--blue);font-weight:850}.note{border:1px solid var(--line);background:var(--card2);border-radius:12px;padding:15px;margin:3px 0 16px;color:var(--muted);font-size:11px}.note b{color:var(--text)}.hint{font-size:10px;color:var(--muted)}.view{display:none}.view.active{display:block}.filters{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:14px}.filters .actions{flex-wrap:wrap}.input{background:var(--card);border:1px solid var(--line);border-radius:9px;color:var(--text);padding:10px 11px;max-width:100%;min-width:0}.search{width:220px}.form-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:15px}.field{min-width:0}.field label{font-size:10px;color:var(--muted);font-weight:800;display:block;margin-bottom:7px}.field input,.field select{width:100%;padding:11px;background:var(--card);border:1px solid var(--line);border-radius:9px;color:var(--text)}.field input[type=range]{padding:0;accent-color:var(--sea)}.fields3{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}.scenario-buttons{display:flex;gap:7px;flex-wrap:wrap;margin-bottom:18px}.scenario-buttons button{padding:10px 12px;border:1px solid var(--line);border-radius:9px;background:var(--card);color:var(--text);font-weight:750;font-size:11px}.scenario-buttons button.selected{border-color:#62b7a8;color:var(--sea);background:var(--seaSoft)}.slider{margin:0 0 16px}.slider-top{display:flex;justify-content:space-between;gap:12px;font-size:11px;font-weight:700;margin-bottom:9px}.slider output{color:var(--sea);font-weight:900}.slider input{width:100%;accent-color:var(--sea)}.chart{width:100%;height:220px;overflow:hidden}.chart svg{width:100%;height:100%}.legend{display:flex;flex-wrap:wrap;gap:14px;font-size:10px;color:var(--muted);margin:8px 0}.legend i{display:inline-block;width:9px;height:9px;border-radius:50%;background:#1e9a93;margin-right:5px}.two-cards{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:11px}.mini{padding:13px;background:var(--card2);border:1px solid var(--line);border-radius:11px}.mini span{display:block;color:var(--muted);font-size:10px}.mini b{font-size:22px;letter-spacing:-.7px}.step{display:flex;gap:13px;margin-bottom:18px}.step .badge{background:var(--blueSoft);color:var(--blue);font-weight:900;border-radius:11px;width:31px;height:31px;flex-shrink:0;display:grid;place-items:center}.step b{font-size:12px}.step p{margin:3px 0 0;color:var(--muted);font-size:11px}.rowactions{display:flex;gap:5px}.rowactions button{font-size:10px;padding:6px 7px;background:var(--card2);color:var(--text);border:1px solid var(--line);border-radius:7px}.toast{min-height:17px;font-size:11px;color:var(--sea);font-weight:800;margin-bottom:8px}.footer{display:flex;flex-wrap:wrap;justify-content:space-between;gap:10px;color:var(--muted);font-size:10px;padding:12px 0}.empty{padding:25px;text-align:center;color:var(--muted)}.modaloverlay{position:fixed;inset:0;z-index:15;background:#061d2aca;display:none;align-items:center;justify-content:center;padding:20px;overflow:auto}.modaloverlay.show{display:flex}.modal{width:min(660px,100%);max-height:min(94vh,850px);overflow:auto;background:var(--card);color:var(--text);padding:24px;border-radius:18px;box-shadow:0 20px 70px #00172266}.modal h2{font-size:23px;margin:0 0 5px}.modal p{font-size:11px;color:var(--muted);margin:0 0 18px}.modal .form-grid{gap:13px}.modal .form-grid .field input,.modal .form-grid .field select{padding:9px}.modal-bottom{display:flex;justify-content:flex-end;gap:8px;margin-top:20px}.hidden{display:none!important}
|
| 13 |
+
@media(max-width:1200px){.grid4{grid-template-columns:repeat(2,minmax(0,1fr))}.hero-mark{margin-right:14px}}@media(max-width:950px){.shell{grid-template-columns:minmax(0,1fr)}.side{height:auto;min-height:0;min-width:0;position:relative;display:block;padding:15px}.identity{margin:0 0 15px}.nav-title,.side-end{display:none}.nav{display:flex;overflow-x:auto;gap:5px}.nav button{width:auto;flex-shrink:0;padding:10px}.nav .glyph{width:auto}main{padding:18px}.columns,.columns.equal{grid-template-columns:1fr}}@media(max-width:560px){.chart{overflow-x:auto}.chart svg{width:680px;min-width:680px;height:100%}.top{flex-direction:column;align-items:stretch}.top .actions{width:100%}.grid4,.fields3{grid-template-columns:repeat(2,minmax(0,1fr))}.top{align-items:flex-start}.top .actions .btn{padding:8px}.crumb{max-width:175px}.head-icon,.hero-mark{display:none}.hero{padding:23px;min-height:145px}.panel{padding:15px}.stat{padding:14px}.stat strong{font-size:23px}.form-grid{grid-template-columns:1fr}.search{width:165px}.mini b{font-size:18px}.head h1{font-size:29px}}@media(max-width:375px){.grid4{grid-template-columns:1fr 1fr;gap:8px}.stat strong{font-size:19px}.stat .label{font-size:9px}.two-cards{grid-template-columns:1fr}.crumb{max-width:125px}}
|
| 14 |
+
</style>
|
| 15 |
+
</head>
|
| 16 |
+
<body>
|
| 17 |
+
<div class="shell">
|
| 18 |
+
<aside class="side"><div class="identity"><div class="emblem">◎</div><div><b>ImpactLedger</b><small>INTELLIGENCE STUDIO</small></div></div><div class="nav-title">WORKSPACE</div><nav class="nav" aria-label="Main navigation">
|
| 19 |
+
<button class="active" data-view="overview"><span class="glyph">▣</span> Impact overview</button><button data-view="studio"><span class="glyph">◉</span> Scenario studio</button><button data-view="programs"><span class="glyph">▤</span> Program registry</button><button data-view="uncertainty"><span class="glyph">◇</span> Uncertainty lab</button><button data-view="method"><span class="glyph">☷</span> Methodology</button></nav><div class="side-end"><b><span class="online"></span> Local-first workspace</b>Fictional impact programs<br>Transparent assumptions<br>No paid APIs or server</div></aside>
|
| 20 |
+
<main><div class="top"><div class="crumb">HUGDRIGO / PORTFOLIO / <strong>#106 IMPACTLEDGER</strong></div><div class="actions"><button class="btn" id="themeBtn" title="Toggle theme">◐ Theme</button><button class="btn" id="backupBtn">⇩ Backup JSON</button><button class="btn primary" id="createBtn">+ New program</button></div></div>
|
| 21 |
+
<section id="overview" class="view active"><header class="head"><div><div class="eyebrow">PROGRAM EFFECTIVENESS / IMPACT ACCOUNTING</div><h1>Impact overview</h1><p>Bring budgets, reach, measured outcomes and attribution assumptions into one transparent decision workspace.</p></div><div class="head-icon">◎</div></header><div class="hero"><div><b>PORTFOLIO INTELLIGENCE / PROJECT 106</b><h2>Measure what matters.<br>Explain what you assume.</h2><p>Compare program effectiveness without presenting modeled outcomes as verified causal impact.</p></div><div class="hero-mark">◈</div></div><div class="grid4" id="overviewKpis"></div><div class="columns"><section class="panel"><div class="panel-title"><div><h3>Incremental outcomes by program</h3><p>Illustrative incremental outcomes; negative results are retained</p></div><span class="tag">Attribution-adjusted</span></div><div id="impactChart" class="chart"></div><div class="legend"><span><i></i>Positive incremental outcomes</span><span><i style="background:#e99b73"></i>Negative outcomes</span></div></section><section class="panel"><div class="panel-title"><div><h3>Budget allocation</h3><p>Top active programs by assumed total budget</p></div></div><div id="budgetBars"></div></section></div><div class="columns"><section class="panel"><div class="panel-title"><div><h3>Program effectiveness</h3><p>Illustrative cost and incremental outcome comparisons</p></div><span class="tag">No causal proof</span></div><div class="table-wrap"><table><thead><tr><th>Program</th><th class="right">Budget</th><th class="right">Reach</th><th class="right">Incremental</th><th class="right">Cost / outcome</th></tr></thead><tbody id="overviewRows"></tbody></table></div></section><section class="panel"><div class="panel-title"><div><h3>Interpretation guide</h3><p>What the portfolio numbers can and cannot tell you</p></div></div><div class="step"><span class="badge">1</span><div><b>Measured change ≠ causal impact</b><p>Before-and-after differences may reflect confounding, selection and other factors.</p></div></div><div class="step"><span class="badge">2</span><div><b>Attribution is an assumption</b><p>A percentage chosen by the user is not established by this simulator.</p></div></div><div class="step"><span class="badge">3</span><div><b>Negative impacts stay visible</b><p>Harmful or deteriorating results are not concealed in aggregates.</p></div></div></section></div></section>
|
| 22 |
+
<section id="studio" class="view"><header class="head"><div><div class="eyebrow">INTERACTIVE WHAT-IF ANALYSIS</div><h1>Scenario studio</h1><p>Stress-test how program coverage, measured change, costs and assumed attribution affect impact efficiency.</p></div><div class="head-icon">◉</div></header><div class="scenario-buttons" id="presets"></div><div class="columns"><section class="panel"><div class="panel-title"><div><h3>Scenario controls</h3><p>Changes apply to all active programs; original data remains unchanged</p></div><span class="tag">Illustrative</span></div><div class="slider"><div class="slider-top"><span>Reach adjustment</span><output id="reachOut"></output></div><input type="range" id="reachSlider" min="-60" max="100" step="5"></div><div class="slider"><div class="slider-top"><span>Follow-up outcome rate adjustment</span><output id="liftOut"></output></div><input type="range" id="liftSlider" min="-20" max="20" step="1"></div><div class="slider"><div class="slider-top"><span>Budget adjustment</span><output id="costOut"></output></div><input type="range" id="costSlider" min="-40" max="100" step="5"></div><div class="slider"><div class="slider-top"><span>Attribution assumption adjustment</span><output id="attribOut"></output></div><input type="range" id="attribSlider" min="-40" max="40" step="5"></div><div class="note"><b>Assumption discipline:</b> Changing attribution does not establish causal effects. Rates are clamped to 0–100%; reach and budget cannot become negative.</div></section><section class="panel"><div class="panel-title"><div><h3>Scenario outcomes</h3><p>Relative to the unchanged reference portfolio</p></div></div><div class="two-cards" id="scenarioCards"></div><div class="panel-title" style="margin-top:20px"><div><h3>Net incremental outcomes</h3><p>Baseline vs selected scenario</p></div></div><div id="compareChart" class="chart" style="height:190px"></div></section></div><section class="panel"><div class="panel-title"><div><h3>Detailed comparison</h3><p>Negative incremental results are possible and shown as negative values</p></div></div><div class="table-wrap"><table><thead><tr><th>Program</th><th class="right">Scenario reach</th><th class="right">Budget</th><th class="right">Incremental outcomes</th><th class="right">Cost per positive outcome</th></tr></thead><tbody id="scenarioRows"></tbody></table></div></section></section>
|
| 23 |
+
<section id="programs" class="view"><header class="head"><div><div class="eyebrow">MANAGE / COMPARE / PRESERVE</div><h1>Program registry</h1><p>Edit impact programs and assumptions. Archiving keeps each record in your backup rather than deleting it.</p></div><div class="head-icon">▤</div></header><div id="registryKpis" class="grid4"></div><section class="panel"><div class="filters"><div class="actions"><input class="input search" id="search" type="search" placeholder="Search programs..." aria-label="Search programs"><select class="input" id="registryFilter" aria-label="Registry filter"><option value="active">Active</option><option value="archived">Archived</option><option value="all">All programs</option></select></div><div class="actions"><button class="btn" id="csvBtn">⇩ Export CSV</button><button class="btn" id="importBtn">⇧ Restore JSON</button><button class="btn danger" id="resetBtn">↺ Reset demo</button></div></div><div id="registryStatus" class="toast" role="status" aria-live="polite"></div><div class="table-wrap"><table><thead><tr><th>Program</th><th>Sector</th><th class="right">Budget ($)</th><th class="right">Reach</th><th class="right">Pre / Post</th><th class="right">Attribution</th><th class="right">Evidence</th><th>Actions</th></tr></thead><tbody id="registryRows"></tbody></table></div></section><div class="note"><b>Data ownership.</b> Your changes are stored in this browser if local storage is available. Export a JSON backup before resetting the demo or switching devices. No data leaves your browser.</div></section>
|
| 24 |
+
<section id="uncertainty" class="view"><header class="head"><div><div class="eyebrow">UNCERTAINTY / SENSITIVITY / PRIORITIES</div><h1>Uncertainty lab</h1><p>Reproducible illustrative Monte Carlo draws reveal how sensitive modeled outcomes are to assumptions.</p></div><div class="head-icon">◇</div></header><div id="riskKpis" class="grid4"></div><div class="columns"><section class="panel"><div class="panel-title"><div><h3>Uncertainty distribution</h3><p>400 deterministic draws · illustrative net incremental outcomes</p></div><span class="tag">Seeded</span></div><div id="riskChart" class="chart" style="height:260px"></div><div id="riskPercentiles" class="two-cards"></div></section><section class="panel"><div class="panel-title"><div><h3>Priorities & limitations</h3><p>Signals to investigate before reporting real impact</p></div></div><div id="watchlist"></div></section></div><div class="columns equal"><section class="panel"><div class="panel-title"><div><h3>Program outcome sensitivity</h3><p>Assumed incremental outcomes under ±10pp attribution</p></div></div><div id="sensitivity"></div></section><section class="panel"><div class="panel-title"><div><h3>How draws are constructed</h3></div></div><div class="step"><span class="badge">1</span><div><b>Reach shocks</b><p>Each draw changes projected participants around the current scenario using bounded multiplicative shocks.</p></div></div><div class="step"><span class="badge">2</span><div><b>Measured change shocks</b><p>Follow-up outcome rates receive bounded synthetic variation, with valid rate limits.</p></div></div><div class="step"><span class="badge">3</span><div><b>Attribution shocks</b><p>Attribution assumptions fluctuate, but draws are not calibrated uncertainty intervals.</p></div></div></section></div><div class="note"><b>Important:</b> The 10th/50th/90th percentiles are distributions of invented assumptions, not inferential confidence or credible intervals and not proof of impact.</div></section>
|
| 25 |
+
<section id="method" class="view"><header class="head"><div><div class="eyebrow">REPRODUCIBLE / TRANSPARENT / NO EXTERNAL SERVICES</div><h1>Methodology</h1><p>Inspect the exact logic and limitations behind each score. A simulation does not substitute a real evaluation design.</p></div><div class="head-icon">☷</div></header><div class="columns equal"><section class="panel"><div class="panel-title"><div><h3>Impact equations</h3></div></div><div class="step"><span class="badge">1</span><div><b>Observed outcome rate difference</b><p>Change = follow-up outcome rate − baseline outcome rate, expressed in percentage points.</p></div></div><div class="step"><span class="badge">2</span><div><b>Attributed incremental outcomes</b><p>Incremental = reach × (follow-up − baseline)/100 × assumed attribution/100. Negative outcomes are preserved.</p></div></div><div class="step"><span class="badge">3</span><div><b>Cost effectiveness</b><p>Cost per positive incremental outcome = budget ÷ positive incremental outcomes. Undefined when net additional outcomes ≤ 0.</p></div></div><div class="step"><span class="badge">4</span><div><b>Portfolio accumulation</b><p>Aggregate budgets, reach and signed incremental outcomes. Programs are assumed non-overlapping for summary purposes.</p></div></div></section><section class="panel"><div class="panel-title"><div><h3>Scope boundaries</h3></div></div><div class="step"><span class="badge">!</span><div><b>Not causal inference</b><p>No randomized design, control group adjustment, confounder identification or counterfactual estimation.</p></div></div><div class="step"><span class="badge">!</span><div><b>Do not claim verified social return</b><p>Different outcome types should not be treated as inherently comparable; the demo assumes standardized outcome units.</p></div></div><div class="step"><span class="badge">!</span><div><b>No privacy certification</b><p>Browser local storage is not encrypted. Use fictional or non-sensitive data.</p></div></div><div class="step"><span class="badge">✓</span><div><b>Completely offline</b><p>HTML, CSS and JavaScript. No API keys, analytics, paid services, external scripts or network requests.</p></div></div></section></div><div class="panel"><div class="panel-title"><div><h3>Demo program catalog</h3><p>All assumptions are editable; no figures represent real-world research findings</p></div></div><div id="methodTable" class="table-wrap"></div></div><div class="note"><b>License:</b> MIT · 2026 Hugdrigo · Project #106. Educational program evaluation simulator only.</div></section>
|
| 26 |
+
<footer class="footer"><span>© 2026 ImpactLedger · Hugdrigo · Project #106</span><span>Fictional demonstration · Transparent formulas · Local-first</span></footer>
|
| 27 |
+
</main></div>
|
| 28 |
+
<div id="modalOverlay" class="modaloverlay" role="presentation"><div class="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle"><h2 id="modalTitle">New program</h2><p>Use fictional demonstration data. Changes are stored locally when allowed.</p><form id="programForm"><div class="form-grid">
|
| 29 |
+
<div class="field"><label for="fName">Program name</label><input id="fName" maxlength="60" required placeholder="e.g. Mentorship pilot"></div><div class="field"><label for="fSector">Sector</label><select id="fSector"><option>Education</option><option>Health</option><option>Employment</option><option>Climate</option><option>Inclusion</option><option>Community</option></select></div>
|
| 30 |
+
<div class="field"><label for="fBudget">Total program budget (USD)</label><input id="fBudget" type="number" min="0" max="100000000" step="0.01" required></div><div class="field"><label for="fReach">Participants reached</label><input id="fReach" type="number" min="0" max="10000000" step="1" required></div>
|
| 31 |
+
<div class="field"><label for="fPre">Baseline outcome rate (%)</label><input id="fPre" type="number" min="0" max="100" step="0.1" required></div><div class="field"><label for="fPost">Follow-up outcome rate (%)</label><input id="fPost" type="number" min="0" max="100" step="0.1" required></div>
|
| 32 |
+
<div class="field"><label for="fAttrib">Assumed attribution (%)</label><input id="fAttrib" type="number" min="0" max="100" step="0.1" required></div><div class="field"><label for="fEvidence">Evidence strength (1–5, subjective)</label><select id="fEvidence"><option value="1">1 - exploratory</option><option value="2">2 - limited</option><option value="3">3 - moderate</option><option value="4">4 - stronger</option><option value="5">5 - highest demo tier</option></select></div>
|
| 33 |
+
</div><div class="modal-bottom"><button class="btn" id="cancelBtn" type="button">Cancel</button><button class="btn primary" type="submit">Save program</button></div></form></div></div>
|
| 34 |
+
<input type="file" id="fileInput" accept=".json,application/json" hidden>
|
| 35 |
+
<script>
|
| 36 |
+
(()=>{'use strict';
|
| 37 |
+
const $=id=>document.getElementById(id), storageKey='hugdrigo-impactledger106-v1', sectorOptions=['Education','Health','Employment','Climate','Inclusion','Community'];
|
| 38 |
+
const demo=[
|
| 39 |
+
{id:1,name:'Learning Bridge',sector:'Education',budget:168000,reach:2450,pre:38,post:63,attrib:62,evidence:3,archived:false},
|
| 40 |
+
{id:2,name:'Healthy Start',sector:'Health',budget:213000,reach:3200,pre:51,post:69,attrib:57,evidence:4,archived:false},
|
| 41 |
+
{id:3,name:'Career Launch',sector:'Employment',budget:184500,reach:1680,pre:32,post:57,attrib:71,evidence:3,archived:false},
|
| 42 |
+
{id:4,name:'Clean Energy Homes',sector:'Climate',budget:289000,reach:1120,pre:26,post:48,attrib:52,evidence:2,archived:false},
|
| 43 |
+
{id:5,name:'Digital Access',sector:'Inclusion',budget:94000,reach:2100,pre:45,post:61,attrib:43,evidence:3,archived:false},
|
| 44 |
+
{id:6,name:'Community Connect',sector:'Community',budget:128000,reach:1820,pre:58,post:54,attrib:55,evidence:2,archived:false},
|
| 45 |
+
{id:7,name:'Skills Forward',sector:'Employment',budget:152000,reach:1340,pre:39,post:72,attrib:65,evidence:4,archived:false},
|
| 46 |
+
{id:8,name:'Green Futures',sector:'Climate',budget:115000,reach:980,pre:22,post:44,attrib:47,evidence:2,archived:false}];
|
| 47 |
+
const presets={baseline:{label:'Baseline',reach:0,lift:0,cost:0,attrib:0},tight:{label:'Funding squeeze',reach:-15,lift:-3,cost:-20,attrib:-5},accelerate:{label:'Delivery boost',reach:35,lift:6,cost:20,attrib:5},stress:{label:'Measurement stress',reach:-10,lift:-8,cost:15,attrib:-20}};
|
| 48 |
+
const clone=x=>JSON.parse(JSON.stringify(x)), clamp=(x,min,max)=>Math.max(min,Math.min(max,x)), esc=x=>String(x).replace(/[&<>"']/g,ch=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[ch]));
|
| 49 |
+
const money=x=>Number(x).toLocaleString('en-US',{style:'currency',currency:'USD',maximumFractionDigits:0}),num=x=>Number(x).toLocaleString('en-US',{maximumFractionDigits:1}),compact=x=>Number(x).toLocaleString('en-US',{notation:'compact',maximumFractionDigits:1}),signed=x=>(x>0?'+':'')+num(x), pct=x=>num(x)+'%';
|
| 50 |
+
let state={programs:clone(demo),scenario:{reach:0,lift:0,cost:0,attrib:0},theme:'light'}, editing=null, active='overview';
|
| 51 |
+
function validProgram(p){return p&&typeof p==='object'&&Number.isSafeInteger(p.id)&&p.id>=1&&p.id<=1e9&&typeof p.name==='string'&&p.name.trim().length>0&&p.name.length<=60&§orOptions.includes(p.sector)&&[['budget',0,1e8],['reach',0,1e7],['pre',0,100],['post',0,100],['attrib',0,100],['evidence',1,5]].every(([key,a,b])=>typeof p[key]==='number'&&Number.isFinite(p[key])&&p[key]>=a&&p[key]<=b)&&(Number.isInteger(p.reach)&&Number.isInteger(p.evidence))&&typeof p.archived==='boolean'}
|
| 52 |
+
function validBackup(d){return d&&d.format==='ImpactLedger-106'&&d.version===1&&Array.isArray(d.programs)&&d.programs.length<=120&&d.programs.every(validProgram)&&new Set(d.programs.map(p=>p.id)).size===d.programs.length&&d.scenario&&[['reach',-60,100],['lift',-20,20],['cost',-40,100],['attrib',-40,40]].every(([k,lo,hi])=>Number.isFinite(d.scenario[k])&&d.scenario[k]>=lo&&d.scenario[k]<=hi)&&['light','dark'].includes(d.theme)}
|
| 53 |
+
try{const loaded=JSON.parse(localStorage.getItem(storageKey)||'null');if(validBackup(loaded))state={programs:loaded.programs,scenario:loaded.scenario,theme:loaded.theme}}catch(_){}
|
| 54 |
+
function save(){try{localStorage.setItem(storageKey,JSON.stringify({format:'ImpactLedger-106',version:1,...state}))}catch(_){}}
|
| 55 |
+
function activePrograms(){return state.programs.filter(p=>!p.archived)}
|
| 56 |
+
function projected(p,s={reach:0,lift:0,cost:0,attrib:0}){const reach=p.reach*(1+s.reach/100),budget=p.budget*(1+s.cost/100),post=clamp(p.post+s.lift,0,100),attrib=clamp(p.attrib+s.attrib,0,100),delta=post-p.pre,incremental=reach*delta/100*attrib/100;return {...p,reach,budget,post,attrib,delta,incremental,unitCost:incremental>0?budget/incremental:null}}
|
| 57 |
+
function portfolio(items){return items.reduce((s,p)=>(s.budget+=p.budget,s.reach+=p.reach,s.incremental+=p.incremental,s.count++,s.weightEvidence+=p.evidence*p.reach,s),{budget:0,reach:0,incremental:0,count:0,weightEvidence:0})}
|
| 58 |
+
function current(){return activePrograms().map(p=>projected(p,state.scenario))}
|
| 59 |
+
function baseline(){return activePrograms().map(p=>projected(p))}
|
| 60 |
+
function stat(label,val,caption,ico){return `<div class="stat"><div class="label">${esc(label)}<i>${ico}</i></div><strong title="${esc(val)}">${esc(val)}</strong><small>${esc(caption)}</small></div>`}
|
| 61 |
+
function outcome(val){return `<span class="${val<0?'negative':'positive'}">${esc(signed(val))}</span>`}
|
| 62 |
+
function unitCost(p){return p.unitCost===null?'N/A':money(p.unitCost)}
|
| 63 |
+
function svgBars(data){const w=760,h=216,ml=40,mr=14,top=22,bottom=45,plot=h-top-bottom; if(!data.length)return '<div class="empty">No active programs</div>';const max=Math.max(1,...data.map(x=>Math.abs(x.incremental)))*1.1;const zero=plot/2+top;const space=(w-ml-mr)/data.length,bw=Math.min(60,space*.58);let out=`<svg viewBox="0 0 ${w} ${h}" role="img" aria-label="Signed attributed incremental outcomes chart">`;
|
| 64 |
+
for(let g=-2;g<=2;g++){const y=zero-g*plot/4;out+=`<line x1="${ml}" y1="${y}" x2="${w-mr}" y2="${y}" stroke="${g===0?'#9bb2bd':'#cfdae0'}" stroke-dasharray="${g===0?'0':'3 5'}" opacity=".7"/><text x="${ml-7}" y="${y+3}" text-anchor="end" fill="#94a6b1" font-size="9">${g===0?'0':num(g*max/2)}</text>`}
|
| 65 |
+
data.forEach((x,i)=>{const height=Math.abs(x.incremental)/max*plot/2,cx=ml+space*(i+.5),y=x.incremental>=0?zero-height:zero;out+=`<rect x="${cx-bw/2}" y="${y}" width="${bw}" height="${Math.max(1,height)}" rx="5" fill="${x.incremental>=0?'#26a99e':'#e9a279'}"><title>${esc(x.name)}: ${signed(x.incremental)}</title></rect><text x="${cx}" y="${h-bottom+20}" fill="#8a9ca8" text-anchor="middle" font-size="9">${esc(x.name.split(' ')[0].slice(0,13))}</text>`});return out+'</svg>'}
|
| 66 |
+
function renderOverview(){const data=baseline(),tot=portfolio(data),avg=tot.reach?tot.weightEvidence/tot.reach:0;$('overviewKpis').innerHTML=stat('Total program budget',money(tot.budget),'Assumed one-cycle spending','$')+stat('Participants reached',compact(tot.reach),'May overlap across programs','♙')+stat('Net incremental',signed(tot.incremental),'Assumption-adjusted outcome units','↗')+stat('Cost / net outcome',tot.incremental>0?money(tot.budget/tot.incremental):'N/A',`Evidence tier ${avg?avg.toFixed(1):'0.0'} / 5`,'◎');$('impactChart').innerHTML=svgBars(data);let sorted=data.slice().sort((a,b)=>b.budget-a.budget),max=Math.max(1,...sorted.map(p=>p.budget));$('budgetBars').innerHTML=sorted.slice(0,6).map(p=>`<div class="bar-row"><div class="bar-heading"><b>${esc(p.name)}</b><span>${money(p.budget)}</span></div><div class="track"><span style="width:${100*p.budget/max}%"></span></div></div>`).join('')||'<div class="empty">No active programs</div>';$('overviewRows').innerHTML=sorted.map(p=>`<tr><td><strong>${esc(p.name)}</strong><div class="hint">${esc(p.sector)}</div></td><td class="right">${money(p.budget)}</td><td class="right">${num(p.reach)}</td><td class="right">${outcome(p.incremental)}</td><td class="right">${unitCost(p)}</td></tr>`).join('')||'<tr><td colspan="5" class="empty">No active programs</td></tr>'}
|
| 67 |
+
function setScenario(values){state.scenario={reach:values.reach,lift:values.lift,cost:values.cost,attrib:values.attrib};save();renderScenario();renderRisk()}
|
| 68 |
+
function presetName(){return Object.keys(presets).find(k=>['reach','lift','cost','attrib'].every(f=>presets[k][f]===state.scenario[f]))||'custom'}
|
| 69 |
+
function renderScenario(){const p=presetName();$('presets').innerHTML=Object.entries(presets).map(([key,s])=>`<button type="button" class="${p===key?'selected':''}" data-preset="${key}">${esc(s.label)}</button>`).join('')+`<button type="button" class="${p==='custom'?'selected':''}" disabled>Custom settings</button>`;
|
| 70 |
+
for(const [k,id,txt] of [['reach','reachSlider','%'],['lift','liftSlider','pp'],['cost','costSlider','%'],['attrib','attribSlider','pp']]){$(id).value=state.scenario[k];$(k==='lift'?'liftOut':k==='attrib'?'attribOut':k==='reach'?'reachOut':'costOut').textContent=(state.scenario[k]>0?'+':'')+state.scenario[k]+txt}
|
| 71 |
+
const base=portfolio(baseline()),scenario=current(),tot=portfolio(scenario);const diff=tot.incremental-base.incremental;$('scenarioCards').innerHTML=`<div class="mini"><span>Adjusted budget</span><b>${money(tot.budget)}</b></div><div class="mini"><span>Participants reached</span><b>${compact(tot.reach)}</b></div><div class="mini"><span>Incremental outcomes</span><b>${signed(tot.incremental)}</b></div><div class="mini"><span>Change from baseline</span><b class="${diff<0?'negative':'positive'}">${signed(diff)}</b></div>`;
|
| 72 |
+
let vals=[{name:'Reference',incremental:base.incremental},{name:'Scenario',incremental:tot.incremental}];$('compareChart').innerHTML=svgBars(vals);$('scenarioRows').innerHTML=scenario.map(p=>`<tr><td><strong>${esc(p.name)}</strong></td><td class="right">${num(p.reach)}</td><td class="right">${money(p.budget)}</td><td class="right">${outcome(p.incremental)}</td><td class="right">${unitCost(p)}</td></tr>`).join('')||'<tr><td colspan="5" class="empty">No active programs</td></tr>'}
|
| 73 |
+
function renderRegistry(){const all=state.programs,act=activePrograms(),tot=portfolio(act.map(p=>projected(p))),arch=all.length-act.length;$('registryKpis').innerHTML=stat('Active programs',String(act.length),'Currently included in portfolio','▤')+stat('Archived programs',String(arch),'Retained in backups','▣')+stat('Combined budget',money(tot.budget),'Active program assumptions','$')+stat('Evidence level',tot.reach?num(tot.weightEvidence/tot.reach)+'/5':'N/A','Reach-weighted subjective tier','◎');const query=$('search').value.toLowerCase().trim(),filter=$('registryFilter').value;let show=all.filter(x=>(filter==='all'||(filter==='active'?!x.archived:x.archived))&&(x.name.toLowerCase().includes(query)||x.sector.toLowerCase().includes(query)));$('registryRows').innerHTML=show.map(p=>`<tr><td><strong>${esc(p.name)}</strong>${p.archived?'<div class="hint">Archived</div>':''}</td><td>${esc(p.sector)}</td><td class="right">${money(p.budget)}</td><td class="right">${num(p.reach)}</td><td class="right">${num(p.pre)}% / ${num(p.post)}%</td><td class="right">${pct(p.attrib)}</td><td class="right"><span class="quality">${p.evidence}/5</span></td><td><div class="rowactions"><button type="button" data-edit="${p.id}">Edit</button><button type="button" data-archive="${p.id}">${p.archived?'Restore':'Archive'}</button></div></td></tr>`).join('')||'<tr><td colspan="8" class="empty">No matching programs</td></tr>'}
|
| 74 |
+
function rng(seed){let s=seed>>>0;return ()=>{s=(Math.imul(1664525,s)+1013904223)>>>0;return s/4294967296}}
|
| 75 |
+
function simulate(programs,scenario,trials=400){const random=rng(1062026),out=[];for(let k=0;k<trials;k++){let total=0;for(const p of programs){const stretch=.83+random()*.34,rateNoise=(random()+random()+random()-1.5)*7,attribNoise=(random()+random()-1)*16;const reach=p.reach*(1+scenario.reach/100)*stretch,post=clamp(p.post+scenario.lift+rateNoise,0,100),attrib=clamp(p.attrib+scenario.attrib+attribNoise,0,100);total+=reach*(post-p.pre)/100*attrib/100}out.push(total)}return out.sort((a,b)=>a-b)}
|
| 76 |
+
function renderRisk(){const data=current(),tot=portfolio(data), draws=simulate(activePrograms(),state.scenario);const percentile=q=>draws.length?draws[Math.floor((draws.length-1)*q)]:0;const p10=percentile(.1),p50=percentile(.5),p90=percentile(.9),neg=data.filter(p=>p.incremental<0),lowEvidence=data.filter(p=>p.evidence<=2);$('riskKpis').innerHTML=stat('Modeled P10',signed(p10),'Synthetic lower scenario percentile','◃')+stat('Modeled median',signed(p50),'Middle simulated outcome','◇')+stat('Modeled P90',signed(p90),'Synthetic upper scenario percentile','▹')+stat('Declining programs',String(neg.length),'Negative incremental outcomes','!');
|
| 77 |
+
const bins=18,min=draws.length?draws[0]:0,max=draws.length?draws[draws.length-1]:1,range=Math.max(max-min,1e-9),counts=Array(bins).fill(0);for(const v of draws)counts[clamp(Math.floor((v-min)/range*bins),0,bins-1)]++;const top=Math.max(1,...counts),w=760,h=250,l=40,r=12,t=18,b=39,area=w-l-r,step=area/bins;$('riskChart').innerHTML=`<svg viewBox="0 0 ${w} ${h}" role="img" aria-label="Synthetic impact outcomes histogram">`+counts.map((v,i)=>{const bh=(h-t-b-8)*v/top;return `<rect x="${l+i*step+2}" y="${h-b-bh}" width="${step-4}" height="${bh}" rx="3" fill="#3cae9f"><title>${v} simulations</title></rect>`}).join('')+`<line x1="${l}" y1="${h-b}" x2="${w-r}" y2="${h-b}" stroke="#9db4c2"/><text x="${l}" y="${h-9}" fill="#90a9b6" font-size="10">${num(min)}</text><text x="${w-r}" y="${h-9}" text-anchor="end" fill="#90a9b6" font-size="10">${num(max)}</text></svg>`;
|
| 78 |
+
$('riskPercentiles').innerHTML=`<div class="mini"><span>Middle 80% illustrative range</span><b>${num(p10)} – ${num(p90)}</b></div><div class="mini"><span>Reference net incremental</span><b>${signed(tot.incremental)}</b></div>`;
|
| 79 |
+
const issues=[...neg.map(x=>({name:x.name,msg:'Negative attributed outcome estimate; investigate program performance.'})),...lowEvidence.map(x=>({name:x.name,msg:'Low subjective evidence tier; prioritize rigorous evaluation.'}))]; if(tot.reach===0)issues.unshift({name:'No active reach',msg:'Add or restore program records to compare effects.'});$('watchlist').innerHTML=(issues.slice(0,7).map(i=>`<div class="step"><span class="badge">!</span><div><b>${esc(i.name)}</b><p>${esc(i.msg)}</p></div></div>`).join('')||'<div class="note">No basic negative-outcome or low-evidence flags with these inputs. This does not establish validity.</div>');$('sensitivity').innerHTML=data.map(p=>{const lo=projected(p,{...state.scenario,attrib:state.scenario.attrib-10}).incremental,hi=projected(p,{...state.scenario,attrib:state.scenario.attrib+10}).incremental;return `<div class="bar-row"><div class="bar-heading"><b>${esc(p.name)}</b><span>${signed(lo)} to ${signed(hi)}</span></div><div class="track"><span style="width:${clamp(Math.abs(p.incremental)/Math.max(1,...data.map(x=>Math.abs(x.incremental)))*100,0,100)}%"></span></div></div>`}).join('')||'<div class="empty">No active programs</div>'}
|
| 80 |
+
function renderMethod(){$('methodTable').innerHTML=`<table><thead><tr><th>Program</th><th>Sector</th><th class="right">Baseline rate</th><th class="right">Follow-up rate</th><th class="right">Attribution assumption</th><th class="right">Subjective evidence</th></tr></thead><tbody>`+state.programs.map(p=>`<tr><td>${esc(p.name)}${p.archived?' (archived)':''}</td><td>${esc(p.sector)}</td><td class="right">${pct(p.pre)}</td><td class="right">${pct(p.post)}</td><td class="right">${pct(p.attrib)}</td><td class="right">${p.evidence}/5</td></tr>`).join('')+'</tbody></table>'}
|
| 81 |
+
function render(){renderOverview();renderScenario();renderRegistry();renderRisk();renderMethod();document.body.classList.toggle('dark',state.theme==='dark')}
|
| 82 |
+
function showView(name){if(!['overview','studio','programs','uncertainty','method'].includes(name))return;active=name;document.querySelectorAll('.view').forEach(el=>el.classList.toggle('active',el.id===name));document.querySelectorAll('.nav button').forEach(b=>b.classList.toggle('active',b.dataset.view===name));window.scrollTo({top:0,behavior:'instant'})}
|
| 83 |
+
function setStatus(msg){$('registryStatus').textContent=msg}
|
| 84 |
+
function openModal(id=null){editing=id;const p=state.programs.find(p=>p.id===id)||{name:'',sector:'Education',budget:10000,reach:500,pre:35,post:50,attrib:50,evidence:3};$('modalTitle').textContent=id===null?'New program':'Edit program';for(const [key,field] of [['name','fName'],['sector','fSector'],['budget','fBudget'],['reach','fReach'],['pre','fPre'],['post','fPost'],['attrib','fAttrib'],['evidence','fEvidence']])$(field).value=p[key];$('modalOverlay').classList.add('show');$('fName').focus()}
|
| 85 |
+
function closeModal(){$('modalOverlay').classList.remove('show');editing=null}
|
| 86 |
+
function csvCell(v){let s=String(v??'');if(/^[\s\uFEFF]*[=+@\t\r]/.test(s)||/^[\s\uFEFF]*-(?!\d+(?:\.\d+)?$)/.test(s))s="'"+s;return '"'+s.replace(/"/g,'""')+'"'}
|
| 87 |
+
function download(filename,text,mime){const blob=new Blob([text],{type:mime});const url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download=filename;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),2000)}
|
| 88 |
+
function backup(){download('ImpactLedger_106_backup.json',JSON.stringify({format:'ImpactLedger-106',version:1,...state},null,2),'application/json')}
|
| 89 |
+
function csv(){const heads=['id','name','sector','budget_usd','reach','baseline_rate_pct','followup_rate_pct','attribution_pct','evidence_tier','archived','incremental_outcomes','cost_per_positive_outcome_usd'];const rows=[heads.join(',')];for(const p of state.programs){const a=projected(p);rows.push([p.id,p.name,p.sector,p.budget,p.reach,p.pre,p.post,p.attrib,p.evidence,p.archived,a.incremental,a.unitCost??''].map(csvCell).join(','))}download('ImpactLedger_106_programs.csv','\uFEFF'+rows.join('\r\n'),'text/csv;charset=utf-8');setStatus('CSV exported with formula-injection protection.')}
|
| 90 |
+
function importBackup(file){if(!file)return;if(file.size>2*1024*1024){setStatus('Import rejected: file exceeds 2 MB.');return}const reader=new FileReader();reader.onload=()=>{try{const data=JSON.parse(String(reader.result));if(!validBackup(data))throw Error('Invalid program fields or backup format.');state={programs:clone(data.programs),scenario:{...data.scenario},theme:data.theme};save();render();setStatus('Backup restored successfully.')}catch(e){setStatus('Import rejected: '+e.message)}};reader.onerror=()=>setStatus('Unable to read the file.');reader.readAsText(file)}
|
| 91 |
+
for(const b of document.querySelectorAll('.nav button'))b.addEventListener('click',()=>showView(b.dataset.view));$('themeBtn').addEventListener('click',()=>{state.theme=state.theme==='dark'?'light':'dark';save();render()});$('backupBtn').addEventListener('click',backup);$('createBtn').addEventListener('click',()=>openModal());$('cancelBtn').addEventListener('click',closeModal);$('modalOverlay').addEventListener('click',e=>{if(e.target===$('modalOverlay'))closeModal()});document.addEventListener('keydown',e=>{if(e.key==='Escape')closeModal()});
|
| 92 |
+
$('programForm').addEventListener('submit',e=>{e.preventDefault();const row={id:editing===null?Math.max(0,...state.programs.map(x=>x.id))+1:editing,name:$('fName').value.trim(),sector:$('fSector').value,budget:Number($('fBudget').value),reach:Number($('fReach').value),pre:Number($('fPre').value),post:Number($('fPost').value),attrib:Number($('fAttrib').value),evidence:Number($('fEvidence').value),archived:state.programs.find(x=>x.id===editing)?.archived||false};if(!validProgram(row)){alert('Please correct the program name and numeric ranges.');return}if(editing===null){if(state.programs.length>=120){alert('Maximum 120 programs.');return}state.programs.push(row)}else{const i=state.programs.findIndex(x=>x.id===editing);if(i<0)return;state.programs[i]=row}save();render();closeModal();setStatus('Program saved successfully.')});
|
| 93 |
+
$('registryRows').addEventListener('click',e=>{const edit=e.target.closest('[data-edit]'),archive=e.target.closest('[data-archive]');if(edit){openModal(Number(edit.dataset.edit));return}if(archive){const p=state.programs.find(x=>x.id===Number(archive.dataset.archive));if(!p)return;p.archived=!p.archived;save();render();setStatus(`${p.name} ${p.archived?'archived':'restored'}.`)}});
|
| 94 |
+
$('presets').addEventListener('click',e=>{const b=e.target.closest('[data-preset]');if(b&&presets[b.dataset.preset])setScenario(presets[b.dataset.preset])});for(const [k,id] of [['reach','reachSlider'],['lift','liftSlider'],['cost','costSlider'],['attrib','attribSlider']])$(id).addEventListener('input',()=>{state.scenario[k]=Number($(id).value);save();renderScenario();renderRisk()});
|
| 95 |
+
$('search').addEventListener('input',renderRegistry);$('registryFilter').addEventListener('change',renderRegistry);$('csvBtn').addEventListener('click',csv);$('importBtn').addEventListener('click',()=>$('fileInput').click());$('fileInput').addEventListener('change',e=>{importBackup(e.target.files?.[0]);e.target.value=''});$('resetBtn').addEventListener('click',()=>{if(!confirm('Restore fictional demo data? This replaces the current local workspace. Back up first if needed.'))return;state={programs:clone(demo),scenario:{reach:0,lift:0,cost:0,attrib:0},theme:state.theme};save();render();setStatus('Demo reset complete.')});
|
| 96 |
+
render();window.ImpactLedgerTest={projected,portfolio,simulate,validProgram,validBackup,csvCell,activePrograms,refresh:render,getState:()=>clone(state)};
|
| 97 |
+
})();
|
| 98 |
+
</script>
|
| 99 |
+
</body>
|
| 100 |
</html>
|