tonyassi commited on
Commit
81a369f
·
verified ·
1 Parent(s): b50d145

Update src/App.svelte

Browse files
Files changed (1) hide show
  1. src/App.svelte +191 -39
src/App.svelte CHANGED
@@ -1,47 +1,199 @@
1
  <script lang="ts">
2
- import svelteLogo from './assets/svelte.svg'
3
- import viteLogo from '/vite.svg'
4
- import Counter from './lib/Counter.svelte'
5
- </script>
 
 
6
 
7
- <main>
8
- <div>
9
- <a href="https://vite.dev" target="_blank" rel="noreferrer">
10
- <img src={viteLogo} class="logo" alt="Vite Logo" />
11
- </a>
12
- <a href="https://svelte.dev" target="_blank" rel="noreferrer">
13
- <img src={svelteLogo} class="logo svelte" alt="Svelte Logo" />
14
- </a>
15
- </div>
16
- <h1>Vite + Svelte</h1>
17
 
18
- <div class="card">
19
- <Counter />
20
- </div>
 
 
 
 
 
 
21
 
22
- <p>
23
- Check out <a href="https://github.com/sveltejs/kit#readme" target="_blank" rel="noreferrer">SvelteKit</a>, the official Svelte app framework powered by Vite!
24
- </p>
25
-
26
- <p class="read-the-docs">
27
- Click on the Vite and Svelte logos to learn more
28
- </p>
29
- </main>
30
-
31
- <style>
32
- .logo {
33
- height: 6em;
34
- padding: 1.5em;
35
- will-change: filter;
36
- transition: filter 300ms;
 
37
  }
38
- .logo:hover {
39
- filter: drop-shadow(0 0 2em #646cffaa);
 
 
 
 
40
  }
41
- .logo.svelte:hover {
42
- filter: drop-shadow(0 0 2em #ff3e00aa);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  }
44
- .read-the-docs {
45
- color: #888;
 
 
 
 
 
 
 
46
  }
47
- </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  <script lang="ts">
2
+ import './app.css';
3
+ import { onMount } from 'svelte';
4
+ import { csvParse } from 'd3-dsv';
5
+ import { scaleLinear } from 'd3-scale';
6
+ import { extent, bisector } from 'd3-array';
7
+ import { line as d3line, curveMonotoneX } from 'd3-shape';
8
 
9
+ type Row = {
10
+ time_min: number;
11
+ croissant_mgdl_delta: number;
12
+ croissant_with_anti_spike_mgdl_delta: number;
13
+ };
 
 
 
 
 
14
 
15
+ let rows: Row[] = [];
16
+ let loaded = false;
17
+
18
+ // SVG viewport (viewBox); responsive via width:100%
19
+ const w = 960, h = 420;
20
+ const m = { top: 24, right: 24, bottom: 34, left: 24 };
21
+
22
+ // D3 scales
23
+ let x: any, y: any;
24
 
25
+ // Pointer state
26
+ let pointerT: number | null = null; // time (minutes)
27
+ let pointerVX = 0; // viewport px for tooltip horiz position
28
+
29
+ // Assets (served from /public)
30
+ const CROISSANT = "/croissant.png";
31
+ const ANTISPIKE = "/antispike.png";
32
+
33
+ const colorCroissant = "#111111"; // black curve
34
+
35
+ // Messages driven by value thresholds
36
+ function messageForValue(v: number) {
37
+ if (v >= 30) return "High spike → more cravings, fatigue, hunger.";
38
+ if (v < 0) return "Below baseline → possible energy dip / ‘crash’.";
39
+ if (v > 0) return "Moderate rise.";
40
+ return "At baseline.";
41
  }
42
+ function messageForAnti(v: number) {
43
+ if (v >= 22) return "Reduced spike vs. croissant alone.";
44
+ if (v >= 12) return "Smaller spike → steadier energy.";
45
+ if (v <= 2 && v >= -2) return "Near baseline → steadier energy ✅";
46
+ if (v < -1) return "Gentle dip, then recovery.";
47
+ return "Stable.";
48
  }
49
+
50
+ onMount(async () => {
51
+ const res = await fetch('/croissant+croissant_antistpike.csv');
52
+ const text = await res.text();
53
+
54
+ rows = csvParse(text, d => ({
55
+ time_min: +d['time_min']!,
56
+ croissant_mgdl_delta: +d['croissant_mgdl_delta']!,
57
+ croissant_with_anti_spike_mgdl_delta: +d['croissant_with_anti_spike_mgdl_delta']!
58
+ })) as unknown as Row[];
59
+
60
+ // domains
61
+ const xDomain = extent(rows, d => d.time_min) as [number, number];
62
+ const yMin = Math.min(...rows.map(r => Math.min(r.croissant_mgdl_delta, r.croissant_with_anti_spike_mgdl_delta)));
63
+ const yMax = Math.max(...rows.map(r => Math.max(r.croissant_mgdl_delta, r.croissant_with_anti_spike_mgdl_delta)));
64
+
65
+ x = scaleLinear().domain(xDomain).range([m.left, w - m.right]);
66
+ y = scaleLinear().domain([yMin - 5, yMax + 5]).range([h - m.bottom, m.top]);
67
+
68
+ loaded = true;
69
+ });
70
+
71
+ // SVG path builders
72
+ const makePath = (accessor: (r: Row) => number) =>
73
+ d3line<Row>()
74
+ .x(d => x(d.time_min))
75
+ .y(d => y(accessor(d)))
76
+ .curve(curveMonotoneX);
77
+
78
+ let pathCroissant = '';
79
+ let pathAnti = '';
80
+ $: if (loaded) {
81
+ pathCroissant = makePath(d => d.croissant_mgdl_delta)(rows) ?? '';
82
+ pathAnti = makePath(d => d.croissant_with_anti_spike_mgdl_delta)(rows) ?? '';
83
  }
84
+
85
+ // Interpolate y at time via bisector
86
+ const rightAt = bisector<Row, number>(d => d.time_min).right; // binary search for segment
87
+ function yAtTime(accessor: (r: Row) => number, t: number) {
88
+ const i = rightAt(rows, t) - 1;
89
+ const i0 = Math.max(0, Math.min(rows.length - 2, i));
90
+ const a = rows[i0], b = rows[i0 + 1];
91
+ const k = (t - a.time_min) / (b.time_min - a.time_min);
92
+ return accessor(a) + k * (accessor(b) - accessor(a));
93
  }
94
+
95
+ // Pointer events (unified mouse/touch/pen)
96
+ function onPtr(e: PointerEvent) {
97
+ const svg = e.currentTarget as SVGSVGElement;
98
+ const r = svg.getBoundingClientRect();
99
+ const px = Math.max(m.left, Math.min(r.width - m.right, e.clientX - r.left));
100
+ pointerVX = px;
101
+ // viewport px -> viewBox px -> time
102
+ const viewboxX = (px / r.width) * w;
103
+ pointerT = x.invert(viewboxX);
104
+ }
105
+ function onLeave() { pointerT = null; }
106
+
107
+ // Reactive marker values
108
+ let markerX: number | null = null;
109
+ let v1: number | null = null;
110
+ let v2: number | null = null;
111
+ let markerY1: number | null = null;
112
+ let markerY2: number | null = null;
113
+
114
+ $: if (loaded && pointerT !== null) {
115
+ markerX = x(pointerT);
116
+ v1 = yAtTime(d => d.croissant_mgdl_delta, pointerT);
117
+ v2 = yAtTime(d => d.croissant_with_anti_spike_mgdl_delta, pointerT);
118
+ markerY1 = y(v1);
119
+ markerY2 = y(v2);
120
+ } else {
121
+ markerX = v1 = v2 = markerY1 = markerY2 = null;
122
+ }
123
+
124
+ const xLabels = [
125
+ { t: 0, label: "eating time" },
126
+ { t: 120, label: "2 hours later" }
127
+ ];
128
+ </script>
129
+
130
+ <div class="container">
131
+ <div class="h1">Glucose response — croissant vs. Anti-Spike</div>
132
+
133
+ <div class="card" style="position:relative;">
134
+ {#if loaded}
135
+ <svg
136
+ viewBox={`0 0 ${w} ${h}`}
137
+ style="width:100%;height:auto;touch-action:none;display:block"
138
+ on:pointermove={onPtr}
139
+ on:pointerdown={onPtr}
140
+ on:pointerleave={onLeave}
141
+ >
142
+ <!-- Background -->
143
+ <rect x="0" y="0" width={w} height={h} rx="18" fill="#f7f8ff"></rect>
144
+
145
+ <!-- Guides: baseline (0) and +30 -->
146
+ <line x1={m.left} x2={w - m.right} y1={y(0)} y2={y(0)} stroke="#e0e3ff" stroke-width="2" />
147
+ <line x1={m.left} x2={w - m.right} y1={y(30)} y2={y(30)} stroke="#ffd6e7" stroke-dasharray="6 6" />
148
+
149
+ <!-- Axis labels (lightweight) -->
150
+ {#each xLabels as xl}
151
+ <text x={x(xl.t)} y={h - 8} text-anchor={xl.t === 0 ? "start" : "middle"} fill="#4a4f93" font-size="13">{xl.label}</text>
152
+ {/each}
153
+ <text x={m.left} y={y(60)} fill="#4a4f93" font-size="12">+60</text>
154
+ <text x={m.left} y={y(30)} fill="#4a4f93" font-size="12">+30</text>
155
+ <text x={m.left} y={y(0)} fill="#4a4f93" font-size="12">baseline</text>
156
+
157
+ <!-- Curves -->
158
+ <path d={pathCroissant} fill="none" stroke={colorCroissant} stroke-width="3.5"></path>
159
+ <path d={pathAnti} fill="none" style="stroke: var(--brand-pink)" stroke-width="3.5"></path>
160
+
161
+ <!-- Scrub markers -->
162
+ {#if markerX !== null && markerY1 !== null && markerY2 !== null}
163
+ <line x1={markerX} x2={markerX} y1={m.top} y2={h - m.bottom} stroke="rgba(25,31,112,.2)" stroke-width="1.5"></line>
164
+ <circle cx={markerX} cy={markerY1} r="5.5" fill={colorCroissant}></circle>
165
+ <circle cx={markerX} cy={markerY2} r="5.5" fill="#df4085"></circle>
166
+ {/if}
167
+ </svg>
168
+
169
+ <!-- Legend -->
170
+ <div class="legend" style="margin-top:12px">
171
+ <span class="dot" style="background:#111"></span> Croissant
172
+ <img src={CROISSANT} alt="croissant" />
173
+ <span class="dot" style="background:#df4085"></span> Croissant + Anti-Spike
174
+ <img src={ANTISPIKE} alt="Anti-Spike bottle" />
175
+ <span class="badge" style="margin-left:auto">Hover or drag to scrub</span>
176
+ </div>
177
+
178
+ <!-- Tooltip with messages -->
179
+ {#if v1 !== null && v2 !== null}
180
+ <div class="tooltip" style="left:min(calc(100% - 220px), max(12px, calc({pointerVX}px - 110px))); top: 12px;">
181
+ <div style="font-weight:700; font-family: var(--font-serif); color:#111; font-size:14px; margin-bottom:6px;">
182
+ At t = {Math.round(pointerT!)} min
183
+ </div>
184
+ <div style="display:grid; grid-template-columns:auto 1fr auto; gap:6px; align-items:center;">
185
+ <span class="dot" style="background:#111"></span><span>Croissant</span><strong>{v1.toFixed(0)} mg/dL</strong>
186
+ <span class="dot" style="background:#df4085"></span><span>+ Anti-Spike</span><strong>{v2.toFixed(0)} mg/dL</strong>
187
+ </div>
188
+ <hr style="border:none;border-top:1px solid #eee; margin:8px 0;" />
189
+ <div style="font-size:12px; color:#333; margin-bottom:4px;">{messageForValue(v1)}</div>
190
+ <div style="font-size:12px; color:#333;">{messageForAnti(v2)}</div>
191
+ </div>
192
+ {/if}
193
+
194
+ <div class="footer-note">Design: Bodoni / Avenir Next · Colors: #ffffff · #191f70 · #df4085</div>
195
+ {:else}
196
+ Loading…
197
+ {/if}
198
+ </div>
199
+ </div>