rigelbar commited on
Commit
daecf69
·
verified ·
1 Parent(s): fd9b74d

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +1110 -17
index.html CHANGED
@@ -1,19 +1,1112 @@
1
  <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
  <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Weighted Decision Tool (with Radar Chart)</title>
7
+ <style>
8
+ :root{
9
+ --bg:#0b1220;
10
+ --panel:#111a2e;
11
+ --panel2:#0f1730;
12
+ --text:#e8eefc;
13
+ --muted:#aab6d8;
14
+ --line:#223055;
15
+ --accent:#6ea8ff;
16
+ --good:#48d597;
17
+ --warn:#ffcc66;
18
+ --bad:#ff6b6b;
19
+ --chip:#182545;
20
+ --shadow: 0 10px 30px rgba(0,0,0,.35);
21
+ --radius: 16px;
22
+ --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
23
+ --sans: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji";
24
+ }
25
+ *{ box-sizing: border-box; }
26
+ body{
27
+ margin:0;
28
+ font-family: var(--sans);
29
+ background: radial-gradient(1200px 800px at 20% 0%, #15234a 0%, var(--bg) 55%);
30
+ color: var(--text);
31
+ }
32
+ header{
33
+ padding: 22px 18px 10px;
34
+ max-width: 1200px;
35
+ margin: 0 auto;
36
+ display:flex;
37
+ align-items:flex-end;
38
+ justify-content:space-between;
39
+ gap:16px;
40
+ }
41
+ h1{
42
+ margin:0;
43
+ font-weight:800;
44
+ letter-spacing:.2px;
45
+ font-size: 20px;
46
+ }
47
+ .sub{
48
+ margin:6px 0 0;
49
+ color: var(--muted);
50
+ font-size: 13px;
51
+ line-height:1.35;
52
+ }
53
+ .toolbar{
54
+ display:flex;
55
+ gap:10px;
56
+ flex-wrap:wrap;
57
+ align-items:center;
58
+ justify-content:flex-end;
59
+ }
60
+ button, .btn{
61
+ border:1px solid var(--line);
62
+ background: linear-gradient(180deg, #16264a 0%, #0f1b35 100%);
63
+ color: var(--text);
64
+ padding: 10px 12px;
65
+ border-radius: 12px;
66
+ cursor:pointer;
67
+ font-weight:650;
68
+ font-size: 13px;
69
+ box-shadow: var(--shadow);
70
+ transition: transform .06s ease, border-color .2s ease, filter .2s ease;
71
+ user-select:none;
72
+ }
73
+ button:hover{ border-color:#2f4277; filter: brightness(1.05); }
74
+ button:active{ transform: translateY(1px); }
75
+ button.ghost{
76
+ background: transparent;
77
+ box-shadow:none;
78
+ }
79
+ button.danger{
80
+ background: linear-gradient(180deg, #3a1a24 0%, #241019 100%);
81
+ border-color:#5a2333;
82
+ }
83
+ .wrap{
84
+ max-width: 1200px;
85
+ margin: 0 auto;
86
+ padding: 0 18px 28px;
87
+ display:grid;
88
+ grid-template-columns: 1.2fr .8fr;
89
+ gap: 16px;
90
+ }
91
+ @media (max-width: 980px){
92
+ .wrap{ grid-template-columns: 1fr; }
93
+ header{ align-items:flex-start; flex-direction:column; }
94
+ .toolbar{ justify-content:flex-start; }
95
+ }
96
+ .card{
97
+ background: linear-gradient(180deg, rgba(255,255,255,.035) 0%, rgba(255,255,255,.02) 100%);
98
+ border: 1px solid rgba(255,255,255,.08);
99
+ border-radius: var(--radius);
100
+ box-shadow: var(--shadow);
101
+ overflow:hidden;
102
+ }
103
+ .card h2{
104
+ margin:0;
105
+ padding: 14px 14px 0;
106
+ font-size: 14px;
107
+ letter-spacing:.2px;
108
+ }
109
+ .card .pad{ padding: 14px; }
110
+ .note{
111
+ margin:0;
112
+ color: var(--muted);
113
+ font-size: 12.5px;
114
+ line-height: 1.4;
115
+ }
116
+
117
+ /* Table */
118
+ .tableWrap{
119
+ overflow:auto;
120
+ border-top:1px solid rgba(255,255,255,.06);
121
+ margin-top: 12px;
122
+ }
123
+ table{
124
+ width:100%;
125
+ border-collapse: separate;
126
+ border-spacing:0;
127
+ min-width: 820px;
128
+ }
129
+ th, td{
130
+ padding: 10px 10px;
131
+ border-bottom: 1px solid rgba(255,255,255,.06);
132
+ vertical-align: middle;
133
+ }
134
+ th{
135
+ position: sticky;
136
+ top: 0;
137
+ z-index: 1;
138
+ background: rgba(16, 25, 48, .85);
139
+ backdrop-filter: blur(8px);
140
+ text-align:left;
141
+ font-size: 12px;
142
+ color: var(--muted);
143
+ font-weight: 750;
144
+ letter-spacing: .3px;
145
+ }
146
+ tr:hover td{ background: rgba(255,255,255,.02); }
147
+ .col-criterion{ min-width: 220px; }
148
+ .col-weight{ min-width: 120px; }
149
+ .col-actions{ min-width: 120px; text-align:right; }
150
+ .optHead{
151
+ display:flex;
152
+ gap:8px;
153
+ align-items:center;
154
+ }
155
+ .pill{
156
+ font-size: 11px;
157
+ padding: 4px 8px;
158
+ border-radius: 999px;
159
+ background: rgba(255,255,255,.06);
160
+ border: 1px solid rgba(255,255,255,.08);
161
+ color: var(--muted);
162
+ white-space:nowrap;
163
+ }
164
+
165
+ /* Inputs */
166
+ input[type="text"], input[type="number"], textarea{
167
+ width:100%;
168
+ padding: 9px 10px;
169
+ border-radius: 12px;
170
+ border: 1px solid rgba(255,255,255,.12);
171
+ background: rgba(9, 14, 28, .55);
172
+ color: var(--text);
173
+ outline: none;
174
+ font-size: 13px;
175
+ }
176
+ input[type="number"]{ font-family: var(--mono); }
177
+ input:focus, textarea:focus{
178
+ border-color: rgba(110,168,255,.65);
179
+ box-shadow: 0 0 0 3px rgba(110,168,255,.12);
180
+ }
181
+ .small{
182
+ font-size: 12px;
183
+ color: var(--muted);
184
+ }
185
+ .rowActions{
186
+ display:flex;
187
+ justify-content:flex-end;
188
+ gap:8px;
189
+ }
190
+ .mini{
191
+ padding: 8px 10px;
192
+ border-radius: 12px;
193
+ font-size: 12px;
194
+ box-shadow:none;
195
+ }
196
+
197
+ /* Results */
198
+ .rank{
199
+ display:flex;
200
+ flex-direction:column;
201
+ gap:10px;
202
+ margin-top: 10px;
203
+ }
204
+ .rankItem{
205
+ display:flex;
206
+ align-items:center;
207
+ justify-content:space-between;
208
+ gap:12px;
209
+ padding: 10px 12px;
210
+ border-radius: 14px;
211
+ border: 1px solid rgba(255,255,255,.08);
212
+ background: rgba(10, 16, 34, .45);
213
+ }
214
+ .rankLeft{
215
+ display:flex;
216
+ flex-direction:column;
217
+ gap:3px;
218
+ }
219
+ .rankName{
220
+ font-weight: 820;
221
+ letter-spacing:.2px;
222
+ display:flex;
223
+ align-items:center;
224
+ gap:10px;
225
+ flex-wrap:wrap;
226
+ }
227
+ .badge{
228
+ font-size: 11px;
229
+ padding: 4px 9px;
230
+ border-radius: 999px;
231
+ border:1px solid rgba(255,255,255,.12);
232
+ background: rgba(255,255,255,.06);
233
+ color: var(--muted);
234
+ font-weight: 750;
235
+ }
236
+ .badge.win{
237
+ color: #07160f;
238
+ background: rgba(72,213,151,.92);
239
+ border-color: rgba(72,213,151,.85);
240
+ }
241
+ .score{
242
+ font-family: var(--mono);
243
+ font-weight: 800;
244
+ font-size: 14px;
245
+ white-space:nowrap;
246
+ }
247
+ .score small{
248
+ font-family: var(--sans);
249
+ font-weight: 650;
250
+ color: var(--muted);
251
+ font-size: 11px;
252
+ margin-left: 6px;
253
+ }
254
+ .divider{
255
+ height: 1px;
256
+ background: rgba(255,255,255,.06);
257
+ margin: 12px 0;
258
+ }
259
+
260
+ /* Radar */
261
+ .radarWrap{
262
+ display:grid;
263
+ grid-template-columns: 1fr;
264
+ gap: 10px;
265
+ align-items:start;
266
+ }
267
+ .radarRow{
268
+ display:flex;
269
+ gap: 10px;
270
+ flex-wrap:wrap;
271
+ align-items:center;
272
+ justify-content:space-between;
273
+ }
274
+ .legend{
275
+ display:flex;
276
+ gap:10px;
277
+ flex-wrap:wrap;
278
+ }
279
+ .legendItem{
280
+ display:flex;
281
+ align-items:center;
282
+ gap:8px;
283
+ font-size: 12px;
284
+ color: var(--muted);
285
+ background: rgba(255,255,255,.04);
286
+ border:1px solid rgba(255,255,255,.08);
287
+ border-radius: 999px;
288
+ padding: 6px 10px;
289
+ }
290
+ .swatch{
291
+ width: 10px;
292
+ height: 10px;
293
+ border-radius: 3px;
294
+ background: #999;
295
+ border: 1px solid rgba(255,255,255,.25);
296
+ flex: 0 0 auto;
297
+ }
298
+ svg{
299
+ width:100%;
300
+ height:auto;
301
+ display:block;
302
+ border-radius: 14px;
303
+ background: rgba(8, 12, 26, .35);
304
+ border:1px solid rgba(255,255,255,.07);
305
+ }
306
+ .why{
307
+ display:flex;
308
+ flex-direction:column;
309
+ gap:10px;
310
+ }
311
+ .why ul{
312
+ margin: 0;
313
+ padding-left: 18px;
314
+ color: var(--text);
315
+ }
316
+ .why li{
317
+ margin: 6px 0;
318
+ color: var(--text);
319
+ line-height:1.35;
320
+ }
321
+ .muted{ color: var(--muted); }
322
+ .kpi{
323
+ display:flex;
324
+ gap:10px;
325
+ flex-wrap:wrap;
326
+ margin-top: 6px;
327
+ }
328
+ .kpi .chip{
329
+ background: rgba(255,255,255,.05);
330
+ border:1px solid rgba(255,255,255,.08);
331
+ padding: 8px 10px;
332
+ border-radius: 999px;
333
+ color: var(--muted);
334
+ font-size: 12px;
335
+ }
336
+ .chip strong{ color: var(--text); font-weight: 850; }
337
+
338
+ /* Import/Export */
339
+ textarea{
340
+ min-height: 130px;
341
+ font-family: var(--mono);
342
+ font-size: 12px;
343
+ }
344
+ .twoCols{
345
+ display:grid;
346
+ grid-template-columns: 1fr 1fr;
347
+ gap:10px;
348
+ }
349
+ @media (max-width: 980px){
350
+ .twoCols{ grid-template-columns: 1fr; }
351
+ table{ min-width: 760px; }
352
+ }
353
+
354
+ /* Footer hint */
355
+ .foot{
356
+ max-width: 1200px;
357
+ margin: 0 auto;
358
+ padding: 0 18px 22px;
359
+ color: var(--muted);
360
+ font-size: 12px;
361
+ }
362
+ a{ color: #9dc0ff; }
363
+ </style>
364
+ </head>
365
+
366
+ <body>
367
+ <header>
368
+ <div>
369
+ <h1>Weighted Decision Tool</h1>
370
+ <p class="sub">
371
+ Score each option from <b>0–10</b> per criterion, set criterion weights, and it’ll compute totals,
372
+ show a <b>radar chart</b>, and explain <b>why the winner wins</b>.
373
+ </p>
374
+ </div>
375
+ <div class="toolbar">
376
+ <button id="btnAddCriterion" title="Add a new criterion">+ Criterion</button>
377
+ <button id="btnAddOption" title="Add a new option">+ Option</button>
378
+ <button id="btnReset" class="ghost" title="Reset to example data">Reset (example)</button>
379
+ </div>
380
+ </header>
381
+
382
+ <div class="wrap">
383
+ <!-- Left: Decision table -->
384
+ <section class="card">
385
+ <h2>Inputs</h2>
386
+ <div class="pad">
387
+ <p class="note">
388
+ <b>Weights</b> can be any non-negative numbers (they don’t need to add to 1). Higher weight = more important.
389
+ Scores are 0–10. Totals are computed as <span class="pill">Σ (weight × score)</span>.
390
+ </p>
391
+
392
+ <div class="tableWrap" id="tableWrap"></div>
393
+
394
+ <div class="divider"></div>
395
+
396
+ <div class="twoCols">
397
+ <div>
398
+ <h2 style="padding:0; font-size:13px;">Import / Export JSON</h2>
399
+ <p class="note" style="margin-top:6px;">
400
+ Copy to share, or paste to load. (This is plain JSON—no files needed.)
401
+ </p>
402
+ </div>
403
+ <div style="display:flex; gap:10px; justify-content:flex-end; align-items:flex-end; flex-wrap:wrap;">
404
+ <button class="mini" id="btnExport">Export</button>
405
+ <button class="mini" id="btnImport">Import</button>
406
+ <button class="mini danger" id="btnClear">Clear</button>
407
+ </div>
408
+ </div>
409
+ <div style="margin-top:10px;">
410
+ <textarea id="jsonBox" placeholder='{"options":[...],"criteria":[...],"scores":{...}}'></textarea>
411
+ <div class="small" id="jsonMsg" style="margin-top:8px;"></div>
412
+ </div>
413
+ </div>
414
+ </section>
415
+
416
+ <!-- Right: Results + Radar + Why -->
417
+ <aside class="card">
418
+ <h2>Results</h2>
419
+ <div class="pad">
420
+ <div class="kpi" id="kpis"></div>
421
+ <div class="rank" id="rank"></div>
422
+
423
+ <div class="divider"></div>
424
+
425
+ <div class="radarWrap">
426
+ <div class="radarRow">
427
+ <div>
428
+ <div style="font-weight:820; font-size:13px;">Radar chart</div>
429
+ <div class="small">Shows raw 0–10 criterion scores (weights appear in labels).</div>
430
+ </div>
431
+ <div class="legend" id="legend"></div>
432
+ </div>
433
+ <div id="radarHolder"></div>
434
+ </div>
435
+
436
+ <div class="divider"></div>
437
+
438
+ <div class="why">
439
+ <div style="font-weight:820; font-size:13px;">Why this wins</div>
440
+ <div class="small" id="whyIntro"></div>
441
+ <div id="whyBody"></div>
442
+ </div>
443
+ </div>
444
+ </aside>
445
+ </div>
446
+
447
+ <div class="foot">
448
+ Tip: If you want a faster decision, cap criteria to 5–8 and only add criteria that would genuinely change the outcome.
449
+ </div>
450
+
451
+ <script>
452
+ /* ==============================
453
+ Weighted Decision Tool (single file)
454
+ - Dynamic criteria/options
455
+ - Weighted total scoring
456
+ - SVG radar chart
457
+ - "Why this wins" explanation
458
+ ================================ */
459
+
460
+ (function(){
461
+ const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
462
+ const fmt = (n) => (Math.round(n * 100) / 100).toFixed(2).replace(/\.00$/, "");
463
+ const uid = () => (crypto && crypto.randomUUID) ? crypto.randomUUID() : ("id_" + Math.random().toString(16).slice(2));
464
+
465
+ const palette = [
466
+ { stroke:"#6ea8ff", fill:"rgba(110,168,255,.18)" },
467
+ { stroke:"#48d597", fill:"rgba(72,213,151,.16)" },
468
+ { stroke:"#ffcc66", fill:"rgba(255,204,102,.16)" },
469
+ { stroke:"#ff6b6b", fill:"rgba(255,107,107,.14)" },
470
+ { stroke:"#c38bff", fill:"rgba(195,139,255,.14)" },
471
+ { stroke:"#66e3ff", fill:"rgba(102,227,255,.14)" },
472
+ { stroke:"#ffd1f0", fill:"rgba(255,209,240,.14)" }
473
+ ];
474
+
475
+ const els = {
476
+ tableWrap: document.getElementById("tableWrap"),
477
+ rank: document.getElementById("rank"),
478
+ radarHolder: document.getElementById("radarHolder"),
479
+ legend: document.getElementById("legend"),
480
+ kpis: document.getElementById("kpis"),
481
+ whyIntro: document.getElementById("whyIntro"),
482
+ whyBody: document.getElementById("whyBody"),
483
+ jsonBox: document.getElementById("jsonBox"),
484
+ jsonMsg: document.getElementById("jsonMsg")
485
+ };
486
+
487
+ const btns = {
488
+ addCriterion: document.getElementById("btnAddCriterion"),
489
+ addOption: document.getElementById("btnAddOption"),
490
+ reset: document.getElementById("btnReset"),
491
+ export: document.getElementById("btnExport"),
492
+ import: document.getElementById("btnImport"),
493
+ clear: document.getElementById("btnClear")
494
+ };
495
+
496
+ let state = makeExampleState();
497
+
498
+ function makeExampleState(){
499
+ // Example: build vs buy vs outsource decision
500
+ const options = [
501
+ { id: uid(), name: "Build in-house" },
502
+ { id: uid(), name: "Buy SaaS" },
503
+ { id: uid(), name: "Outsource" }
504
+ ];
505
+ const criteria = [
506
+ { id: uid(), name: "Total cost (12 mo)", weight: 3 },
507
+ { id: uid(), name: "Time-to-value", weight: 2.5 },
508
+ { id: uid(), name: "Control / flexibility", weight: 2 },
509
+ { id: uid(), name: "Risk (delivery + vendor)", weight: 3 },
510
+ { id: uid(), name: "Scalability", weight: 1.5 }
511
+ ];
512
+ const scores = {}; // scores[critId][optId] = number 0..10
513
+
514
+ // Fill with reasonable example scores
515
+ for (const c of criteria){
516
+ scores[c.id] = {};
517
+ for (const o of options){
518
+ scores[c.id][o.id] = 5;
519
+ }
520
+ }
521
+
522
+ // Customize
523
+ // Total cost
524
+ scores[criteria[0].id][options[0].id] = 6; // build
525
+ scores[criteria[0].id][options[1].id] = 8; // SaaS
526
+ scores[criteria[0].id][options[2].id] = 7; // outsource
527
+ // Time-to-value
528
+ scores[criteria[1].id][options[0].id] = 4;
529
+ scores[criteria[1].id][options[1].id] = 9;
530
+ scores[criteria[1].id][options[2].id] = 7;
531
+ // Control / flexibility
532
+ scores[criteria[2].id][options[0].id] = 9;
533
+ scores[criteria[2].id][options[1].id] = 6;
534
+ scores[criteria[2].id][options[2].id] = 5;
535
+ // Risk
536
+ scores[criteria[3].id][options[0].id] = 6;
537
+ scores[criteria[3].id][options[1].id] = 7;
538
+ scores[criteria[3].id][options[2].id] = 5;
539
+ // Scalability
540
+ scores[criteria[4].id][options[0].id] = 8;
541
+ scores[criteria[4].id][options[1].id] = 7;
542
+ scores[criteria[4].id][options[2].id] = 6;
543
+
544
+ return { options, criteria, scores };
545
+ }
546
+
547
+ function normalizeState(){
548
+ // Ensure scores exist for all (crit, opt)
549
+ for (const c of state.criteria){
550
+ if (!state.scores[c.id]) state.scores[c.id] = {};
551
+ for (const o of state.options){
552
+ if (state.scores[c.id][o.id] == null || Number.isNaN(+state.scores[c.id][o.id])) {
553
+ state.scores[c.id][o.id] = 0;
554
+ }
555
+ }
556
+ }
557
+ // Remove scores for deleted criteria/options
558
+ const critIds = new Set(state.criteria.map(c => c.id));
559
+ const optIds = new Set(state.options.map(o => o.id));
560
+ for (const cid of Object.keys(state.scores)){
561
+ if (!critIds.has(cid)) delete state.scores[cid];
562
+ else {
563
+ for (const oid of Object.keys(state.scores[cid])){
564
+ if (!optIds.has(oid)) delete state.scores[cid][oid];
565
+ }
566
+ }
567
+ }
568
+ }
569
+
570
+ function compute(){
571
+ normalizeState();
572
+ const weightsSum = state.criteria.reduce((s,c)=> s + Math.max(0, +c.weight || 0), 0);
573
+
574
+ const totals = {};
575
+ const perCriterion = {}; // perCriterion[optId][critId] = weight*score (contribution)
576
+ for (const o of state.options){
577
+ totals[o.id] = 0;
578
+ perCriterion[o.id] = {};
579
+ }
580
+
581
+ for (const c of state.criteria){
582
+ const w = Math.max(0, +c.weight || 0);
583
+ for (const o of state.options){
584
+ const s = clamp(+state.scores[c.id][o.id] || 0, 0, 10);
585
+ const contrib = w * s;
586
+ totals[o.id] += contrib;
587
+ perCriterion[o.id][c.id] = contrib;
588
+ }
589
+ }
590
+
591
+ const ranked = [...state.options]
592
+ .map(o => ({ ...o, total: totals[o.id] }))
593
+ .sort((a,b) => b.total - a.total);
594
+
595
+ return { weightsSum, totals, perCriterion, ranked };
596
+ }
597
+
598
+ function render(){
599
+ const { weightsSum, ranked } = compute();
600
+ renderTable();
601
+ renderKpis(weightsSum);
602
+ renderRank(ranked);
603
+ renderRadar();
604
+ renderWhy(ranked);
605
+ saveToLocalStorage();
606
+ }
607
+
608
+ function renderKpis(weightsSum){
609
+ const critCount = state.criteria.length;
610
+ const optCount = state.options.length;
611
+ const sumText = weightsSum === 0 ? "0 (set weights)" : fmt(weightsSum);
612
+
613
+ els.kpis.innerHTML = `
614
+ <div class="chip">Criteria: <strong>${critCount}</strong></div>
615
+ <div class="chip">Options: <strong>${optCount}</strong></div>
616
+ <div class="chip">Weight sum: <strong>${sumText}</strong></div>
617
+ `;
618
+ }
619
+
620
+ function renderTable(){
621
+ const { weightsSum } = compute();
622
+
623
+ const thead = `
624
+ <thead>
625
+ <tr>
626
+ <th class="col-criterion">Criterion</th>
627
+ <th class="col-weight">Weight</th>
628
+ ${state.options.map((o, idx) => {
629
+ const label = `Option ${idx+1}`;
630
+ return `
631
+ <th>
632
+ <div class="optHead">
633
+ <span class="pill">${label}</span>
634
+ </div>
635
+ <div style="margin-top:8px; display:flex; gap:8px; align-items:center;">
636
+ <input type="text" data-role="optName" data-oid="${o.id}" value="${escapeHtml(o.name)}" aria-label="${label} name"/>
637
+ <button class="mini danger" data-role="removeOption" data-oid="${o.id}" title="Remove option">✕</button>
638
+ </div>
639
+ </th>
640
+ `;
641
+ }).join("")}
642
+ <th class="col-actions">Row</th>
643
+ </tr>
644
+ </thead>
645
+ `;
646
+
647
+ const tbody = `
648
+ <tbody>
649
+ ${state.criteria.map((c) => {
650
+ const w = Math.max(0, +c.weight || 0);
651
+ const weightHint = weightsSum > 0 ? (w / weightsSum) : 0;
652
+ return `
653
+ <tr>
654
+ <td class="col-criterion">
655
+ <input type="text" data-role="critName" data-cid="${c.id}" value="${escapeHtml(c.name)}" aria-label="Criterion name"/>
656
+ <div class="small" style="margin-top:6px;">
657
+ Weight share: <span class="pill">${weightsSum>0 ? Math.round(weightHint*100) + "%" : "—"}</span>
658
+ </div>
659
+ </td>
660
+ <td class="col-weight">
661
+ <input type="number" min="0" step="0.1" data-role="critWeight" data-cid="${c.id}" value="${w}" aria-label="Weight"/>
662
+ </td>
663
+ ${state.options.map((o) => {
664
+ const s = clamp(+state.scores[c.id][o.id] || 0, 0, 10);
665
+ return `
666
+ <td>
667
+ <input type="number" min="0" max="10" step="0.1"
668
+ data-role="score" data-cid="${c.id}" data-oid="${o.id}"
669
+ value="${s}" aria-label="Score for ${escapeHtml(o.name)} on ${escapeHtml(c.name)}"/>
670
+ <div class="small" style="margin-top:6px;">
671
+ Contribution: <span class="pill">${fmt(w*s)}</span>
672
+ </div>
673
+ </td>
674
+ `;
675
+ }).join("")}
676
+ <td class="col-actions">
677
+ <div class="rowActions">
678
+ <button class="mini danger" data-role="removeCriterion" data-cid="${c.id}" title="Remove criterion">Remove</button>
679
+ </div>
680
+ </td>
681
+ </tr>
682
+ `;
683
+ }).join("")}
684
+ </tbody>
685
+ `;
686
+
687
+ els.tableWrap.innerHTML = `
688
+ <table aria-label="Decision scoring table">
689
+ ${thead}
690
+ ${tbody}
691
+ </table>
692
+ `;
693
+
694
+ // Wire events (delegated)
695
+ els.tableWrap.onclick = (e) => {
696
+ const btn = e.target.closest("button");
697
+ if (!btn) return;
698
+ const role = btn.getAttribute("data-role");
699
+ if (role === "removeCriterion"){
700
+ const cid = btn.getAttribute("data-cid");
701
+ state.criteria = state.criteria.filter(c => c.id !== cid);
702
+ delete state.scores[cid];
703
+ render();
704
+ }
705
+ if (role === "removeOption"){
706
+ const oid = btn.getAttribute("data-oid");
707
+ state.options = state.options.filter(o => o.id !== oid);
708
+ for (const cid of Object.keys(state.scores)){
709
+ delete state.scores[cid][oid];
710
+ }
711
+ render();
712
+ }
713
+ };
714
+
715
+ els.tableWrap.oninput = (e) => {
716
+ const el = e.target;
717
+ const role = el.getAttribute("data-role");
718
+ if (!role) return;
719
+
720
+ if (role === "critName"){
721
+ const cid = el.getAttribute("data-cid");
722
+ const c = state.criteria.find(x => x.id === cid);
723
+ if (c) c.name = el.value;
724
+ render();
725
+ }
726
+ if (role === "critWeight"){
727
+ const cid = el.getAttribute("data-cid");
728
+ const c = state.criteria.find(x => x.id === cid);
729
+ if (c) c.weight = Math.max(0, +el.value || 0);
730
+ render();
731
+ }
732
+ if (role === "optName"){
733
+ const oid = el.getAttribute("data-oid");
734
+ const o = state.options.find(x => x.id === oid);
735
+ if (o) o.name = el.value;
736
+ render();
737
+ }
738
+ if (role === "score"){
739
+ const cid = el.getAttribute("data-cid");
740
+ const oid = el.getAttribute("data-oid");
741
+ const v = clamp(+el.value || 0, 0, 10);
742
+ if (!state.scores[cid]) state.scores[cid] = {};
743
+ state.scores[cid][oid] = v;
744
+ render();
745
+ }
746
+ };
747
+ }
748
+
749
+ function renderRank(ranked){
750
+ if (state.options.length === 0){
751
+ els.rank.innerHTML = `<div class="note">Add at least one option.</div>`;
752
+ return;
753
+ }
754
+
755
+ const winnerId = ranked[0]?.id;
756
+ els.rank.innerHTML = ranked.map((o, idx) => {
757
+ const isWinner = o.id === winnerId && ranked.length > 0;
758
+ const badge = isWinner ? `<span class="badge win">Winner</span>` : `<span class="badge">#${idx+1}</span>`;
759
+ return `
760
+ <div class="rankItem">
761
+ <div class="rankLeft">
762
+ <div class="rankName">
763
+ ${escapeHtml(o.name)} ${badge}
764
+ </div>
765
+ <div class="small">Weighted total score</div>
766
+ </div>
767
+ <div class="score">${fmt(o.total)} <small>pts</small></div>
768
+ </div>
769
+ `;
770
+ }).join("");
771
+ }
772
+
773
+ function renderWhy(ranked){
774
+ const { weightsSum } = compute();
775
+ els.whyBody.innerHTML = "";
776
+ els.whyIntro.textContent = "";
777
+
778
+ if (ranked.length < 1){
779
+ els.whyIntro.textContent = "Add options to see a winner.";
780
+ return;
781
+ }
782
+ if (state.criteria.length < 1){
783
+ els.whyIntro.textContent = "Add criteria to explain the result.";
784
+ return;
785
+ }
786
+ if (weightsSum === 0){
787
+ els.whyIntro.textContent = "Set at least one non-zero weight to produce a meaningful result.";
788
+ return;
789
+ }
790
+
791
+ const winner = ranked[0];
792
+ const runner = ranked[1] || null;
793
+
794
+ if (!runner){
795
+ els.whyIntro.textContent = `Only one option exists, so "${winner.name}" wins by default.`;
796
+ return;
797
+ }
798
+
799
+ const { perCriterion } = compute();
800
+
801
+ const deltas = state.criteria.map(c => {
802
+ const w = Math.max(0, +c.weight || 0);
803
+ const wScore = clamp(+state.scores[c.id][winner.id] || 0, 0, 10);
804
+ const rScore = clamp(+state.scores[c.id][runner.id] || 0, 0, 10);
805
+ const contribDelta = w * (wScore - rScore); // how much this criterion pushes winner vs runner
806
+ return {
807
+ cid: c.id,
808
+ name: c.name,
809
+ weight: w,
810
+ winnerScore: wScore,
811
+ runnerScore: rScore,
812
+ contribDelta
813
+ };
814
+ }).sort((a,b) => Math.abs(b.contribDelta) - Math.abs(a.contribDelta));
815
+
816
+ const margin = winner.total - runner.total;
817
+
818
+ els.whyIntro.innerHTML =
819
+ `Comparing <b>${escapeHtml(winner.name)}</b> (winner) to <b>${escapeHtml(runner.name)}</b> (runner-up): ` +
820
+ `the margin is <b>${fmt(margin)}</b> points. Biggest drivers below (weight × score difference).`;
821
+
822
+ const positives = deltas.filter(d => d.contribDelta > 0).slice(0, 4);
823
+ const negatives = deltas.filter(d => d.contribDelta < 0).slice(0, 3);
824
+
825
+ const posList = positives.length ? `
826
+ <div>
827
+ <div class="small muted" style="margin-bottom:6px;">Where the winner pulls ahead</div>
828
+ <ul>
829
+ ${positives.map(d => {
830
+ return `<li><b>${escapeHtml(d.name)}</b>: ${d.winnerScore} vs ${d.runnerScore}
831
+ <span class="pill">+${fmt(d.contribDelta)}</span></li>`;
832
+ }).join("")}
833
+ </ul>
834
+ </div>` : `<div class="note">No criteria where the winner scores higher than the runner-up (likely a tie).</div>`;
835
+
836
+ const negList = negatives.length ? `
837
+ <div style="margin-top:10px;">
838
+ <div class="small muted" style="margin-bottom:6px;">Where the runner-up is stronger (risk to the choice)</div>
839
+ <ul>
840
+ ${negatives.map(d => {
841
+ return `<li><b>${escapeHtml(d.name)}</b>: ${d.winnerScore} vs ${d.runnerScore}
842
+ <span class="pill">${fmt(d.contribDelta)}</span></li>`;
843
+ }).join("")}
844
+ </ul>
845
+ </div>` : "";
846
+
847
+ // A short actionable takeaway:
848
+ const top = deltas[0];
849
+ let takeaway = "";
850
+ if (top){
851
+ const direction = top.contribDelta >= 0 ? "helped" : "hurt";
852
+ takeaway = `Takeaway: The most influential criterion was <b>${escapeHtml(top.name)}</b> (it ${direction} the winner by <b>${fmt(top.contribDelta)}</b> points).`;
853
+ }
854
+
855
+ els.whyBody.innerHTML = posList + negList + `
856
+ <div class="divider"></div>
857
+ <div class="note">${takeaway}</div>
858
+ `;
859
+ }
860
+
861
+ function renderRadar(){
862
+ els.radarHolder.innerHTML = "";
863
+ els.legend.innerHTML = "";
864
+
865
+ const N = state.criteria.length;
866
+ const M = state.options.length;
867
+
868
+ if (N < 3){
869
+ els.radarHolder.innerHTML = `<div class="note">Add at least <b>3 criteria</b> to draw a radar chart.</div>`;
870
+ return;
871
+ }
872
+ if (M < 1){
873
+ els.radarHolder.innerHTML = `<div class="note">Add at least one option.</div>`;
874
+ return;
875
+ }
876
+
877
+ // Build legend
878
+ state.options.forEach((o, idx) => {
879
+ const col = palette[idx % palette.length];
880
+ els.legend.insertAdjacentHTML("beforeend", `
881
+ <div class="legendItem">
882
+ <span class="swatch" style="background:${col.stroke}"></span>
883
+ ${escapeHtml(o.name)}
884
+ </div>
885
+ `);
886
+ });
887
+
888
+ // SVG config
889
+ const size = 380;
890
+ const pad = 44;
891
+ const cx = size/2, cy = size/2;
892
+ const R = (size/2) - pad;
893
+ const rings = 5;
894
+
895
+ const angleFor = (i) => (-Math.PI/2) + (i * (2*Math.PI / N));
896
+
897
+ // Helpers
898
+ const polar = (r, a) => ({ x: cx + r*Math.cos(a), y: cy + r*Math.sin(a) });
899
+ const pointsToStr = (pts) => pts.map(p => `${p.x.toFixed(2)},${p.y.toFixed(2)}`).join(" ");
900
+
901
+ // Grid rings
902
+ let grid = "";
903
+ for (let k=1; k<=rings; k++){
904
+ const rr = R * (k / rings);
905
+ const pts = [];
906
+ for (let i=0; i<N; i++){
907
+ pts.push(polar(rr, angleFor(i)));
908
+ }
909
+ grid += `<polygon points="${pointsToStr(pts)}" fill="none" stroke="rgba(255,255,255,.10)" stroke-width="1" />`;
910
+ }
911
+
912
+ // Axes + labels
913
+ let axes = "";
914
+ for (let i=0; i<N; i++){
915
+ const a = angleFor(i);
916
+ const p0 = polar(0, a);
917
+ const p1 = polar(R, a);
918
+ axes += `<line x1="${p0.x}" y1="${p0.y}" x2="${p1.x}" y2="${p1.y}" stroke="rgba(255,255,255,.10)" stroke-width="1" />`;
919
+
920
+ const c = state.criteria[i];
921
+ const w = Math.max(0, +c.weight || 0);
922
+ const label = `${c.name} (${w})`;
923
+
924
+ // label position slightly beyond edge
925
+ const pl = polar(R + 14, a);
926
+ const anchor = (Math.cos(a) > 0.25) ? "start" : (Math.cos(a) < -0.25 ? "end" : "middle");
927
+ const dy = (Math.sin(a) > 0.25) ? 10 : (Math.sin(a) < -0.25 ? -6 : 4);
928
+
929
+ axes += `
930
+ <text x="${pl.x}" y="${pl.y + dy}" fill="rgba(232,238,252,.78)"
931
+ font-size="11" text-anchor="${anchor}">
932
+ ${escapeHtml(label)}
933
+ </text>
934
+ `;
935
+ }
936
+
937
+ // Polygons for each option
938
+ let polys = "";
939
+ state.options.forEach((o, idx) => {
940
+ const col = palette[idx % palette.length];
941
+ const pts = [];
942
+ for (let i=0; i<N; i++){
943
+ const c = state.criteria[i];
944
+ const s = clamp(+state.scores[c.id][o.id] || 0, 0, 10);
945
+ const rr = R * (s / 10);
946
+ pts.push(polar(rr, angleFor(i)));
947
+ }
948
+ polys += `
949
+ <polygon points="${pointsToStr(pts)}"
950
+ fill="${col.fill}" stroke="${col.stroke}" stroke-width="2" />
951
+ ${pts.map(p => `<circle cx="${p.x}" cy="${p.y}" r="2.6" fill="${col.stroke}" />`).join("")}
952
+ `;
953
+ });
954
+
955
+ // Center label
956
+ const center = `<circle cx="${cx}" cy="${cy}" r="2.5" fill="rgba(255,255,255,.35)" />`;
957
+
958
+ const svg = `
959
+ <svg viewBox="0 0 ${size} ${size}" role="img" aria-label="Radar chart of option scores across criteria">
960
+ <rect x="0" y="0" width="${size}" height="${size}" fill="transparent" />
961
+ ${grid}
962
+ ${axes}
963
+ ${polys}
964
+ ${center}
965
+ <text x="${size-10}" y="${size-10}" text-anchor="end"
966
+ fill="rgba(255,255,255,.35)" font-size="10" font-family="var(--mono)">
967
+ scale: 0–10
968
+ </text>
969
+ </svg>
970
+ `;
971
+
972
+ els.radarHolder.innerHTML = svg;
973
+ }
974
+
975
+ function exportJSON(){
976
+ normalizeState();
977
+ const payload = {
978
+ options: state.options.map(o => ({ id:o.id, name:o.name })),
979
+ criteria: state.criteria.map(c => ({ id:c.id, name:c.name, weight: +c.weight || 0 })),
980
+ scores: state.scores
981
+ };
982
+ els.jsonBox.value = JSON.stringify(payload, null, 2);
983
+ setJsonMsg("Exported current state to JSON.", "ok");
984
+ }
985
+
986
+ function importJSON(){
987
+ const txt = (els.jsonBox.value || "").trim();
988
+ if (!txt){
989
+ setJsonMsg("Paste JSON first.", "warn");
990
+ return;
991
+ }
992
+ try{
993
+ const obj = JSON.parse(txt);
994
+
995
+ // Basic validation
996
+ if (!obj || !Array.isArray(obj.options) || !Array.isArray(obj.criteria) || typeof obj.scores !== "object"){
997
+ throw new Error("Invalid shape. Expected { options:[], criteria:[], scores:{} }");
998
+ }
999
+
1000
+ const options = obj.options.map(o => ({
1001
+ id: String(o.id || uid()),
1002
+ name: String(o.name || "Option")
1003
+ }));
1004
+
1005
+ const criteria = obj.criteria.map(c => ({
1006
+ id: String(c.id || uid()),
1007
+ name: String(c.name || "Criterion"),
1008
+ weight: Math.max(0, +c.weight || 0)
1009
+ }));
1010
+
1011
+ const scores = {};
1012
+ for (const c of criteria){
1013
+ scores[c.id] = {};
1014
+ for (const o of options){
1015
+ const raw = obj.scores?.[c.id]?.[o.id];
1016
+ scores[c.id][o.id] = clamp(+raw || 0, 0, 10);
1017
+ }
1018
+ }
1019
+
1020
+ state = { options, criteria, scores };
1021
+ setJsonMsg("Imported JSON successfully.", "ok");
1022
+ render();
1023
+ }catch(err){
1024
+ setJsonMsg("Import failed: " + err.message, "bad");
1025
+ }
1026
+ }
1027
+
1028
+ function clearJSON(){
1029
+ els.jsonBox.value = "";
1030
+ setJsonMsg("Cleared JSON box.", "ok");
1031
+ }
1032
+
1033
+ function setJsonMsg(msg, kind){
1034
+ const color = kind === "ok" ? "rgba(72,213,151,.9)"
1035
+ : kind === "warn" ? "rgba(255,204,102,.9)"
1036
+ : "rgba(255,107,107,.9)";
1037
+ els.jsonMsg.style.color = color;
1038
+ els.jsonMsg.textContent = msg;
1039
+ }
1040
+
1041
+ function addCriterion(){
1042
+ const c = { id: uid(), name: "New criterion", weight: 1 };
1043
+ state.criteria.push(c);
1044
+ state.scores[c.id] = {};
1045
+ for (const o of state.options){
1046
+ state.scores[c.id][o.id] = 5;
1047
+ }
1048
+ render();
1049
+ }
1050
+
1051
+ function addOption(){
1052
+ const o = { id: uid(), name: "New option" };
1053
+ state.options.push(o);
1054
+ for (const c of state.criteria){
1055
+ if (!state.scores[c.id]) state.scores[c.id] = {};
1056
+ state.scores[c.id][o.id] = 5;
1057
+ }
1058
+ render();
1059
+ }
1060
+
1061
+ function resetExample(){
1062
+ state = makeExampleState();
1063
+ setJsonMsg("Reset to example data.", "ok");
1064
+ render();
1065
+ }
1066
+
1067
+ // Persistence (optional, nice UX)
1068
+ const LS_KEY = "weightedDecisionTool_v1";
1069
+ function saveToLocalStorage(){
1070
+ try{
1071
+ normalizeState();
1072
+ localStorage.setItem(LS_KEY, JSON.stringify(state));
1073
+ }catch(e){ /* ignore */ }
1074
+ }
1075
+ function loadFromLocalStorage(){
1076
+ try{
1077
+ const raw = localStorage.getItem(LS_KEY);
1078
+ if (!raw) return false;
1079
+ const obj = JSON.parse(raw);
1080
+ if (!obj || !Array.isArray(obj.options) || !Array.isArray(obj.criteria) || typeof obj.scores !== "object") return false;
1081
+ state = obj;
1082
+ normalizeState();
1083
+ return true;
1084
+ }catch(e){
1085
+ return false;
1086
+ }
1087
+ }
1088
+
1089
+ function escapeHtml(str){
1090
+ return String(str ?? "")
1091
+ .replaceAll("&","&amp;")
1092
+ .replaceAll("<","&lt;")
1093
+ .replaceAll(">","&gt;")
1094
+ .replaceAll('"',"&quot;")
1095
+ .replaceAll("'","&#039;");
1096
+ }
1097
+
1098
+ // Wire top buttons
1099
+ btns.addCriterion.addEventListener("click", addCriterion);
1100
+ btns.addOption.addEventListener("click", addOption);
1101
+ btns.reset.addEventListener("click", resetExample);
1102
+ btns.export.addEventListener("click", exportJSON);
1103
+ btns.import.addEventListener("click", importJSON);
1104
+ btns.clear.addEventListener("click", clearJSON);
1105
+
1106
+ // Boot
1107
+ loadFromLocalStorage(); // if it fails, we keep example state
1108
+ render();
1109
+ })();
1110
+ </script>
1111
+ </body>
1112
  </html>