tonyassi commited on
Commit
9cd8d5c
·
verified ·
1 Parent(s): 91df233

Update src/App.svelte

Browse files
Files changed (1) hide show
  1. src/App.svelte +63 -44
src/App.svelte CHANGED
@@ -6,23 +6,30 @@
6
  import { extent, bisector } from 'd3-array';
7
  import { line as d3line, area as d3area, curveMonotoneX } from 'd3-shape';
8
 
9
- // ---- Data types (now include rice columns) --------------------------
10
  type Row = {
11
  time_min: number;
12
  croissant_mgdl_delta: number;
13
  croissant_with_anti_spike_mgdl_delta: number;
 
14
  white_rice_mgdl_delta: number;
15
  white_rice_with_anti_spike_mgdl_delta: number;
 
16
  };
17
 
18
  let rows: Row[] = [];
19
  let loaded = false;
20
 
21
- // ---- Toggle state ---------------------------------------------------
22
  type Food = 'croissant' | 'rice';
23
- let food: Food = 'croissant'; // default
 
 
 
 
24
  $: topLabel = food === 'croissant' ? 'Croissant' : 'Rice';
25
- $: bottomLabel = `${topLabel} + Anti-Spike`;
 
26
 
27
  // ---- Layout ---------------------------------------------------------
28
  const w = 960;
@@ -79,21 +86,27 @@
79
 
80
  // Images
81
  const CROISSANT = "/croissant.png";
82
- const RICE = "/rice.png";
83
  const ANTISPIKE = "/antispike.png";
 
 
84
  $: topImg = food === 'croissant' ? CROISSANT : RICE;
 
85
 
86
  onMount(async () => {
87
  updateResponsive();
88
  window.addEventListener('resize', updateResponsive);
89
 
90
- const text = await (await fetch('/glucosedata2.csv')).text();
 
91
  rows = csvParse(text, d => ({
92
  time_min: +d['time_min']!,
93
  croissant_mgdl_delta: +d['croissant_mgdl_delta']!,
94
  croissant_with_anti_spike_mgdl_delta: +d['croissant_with_anti_spike_mgdl_delta']!,
 
95
  white_rice_mgdl_delta: +d['white_rice_mgdl_delta']!,
96
- white_rice_with_anti_spike_mgdl_delta: +d['white_rice_with_anti_spike_mgdl_delta']!
 
97
  })) as unknown as Row[];
98
 
99
  loaded = true;
@@ -101,18 +114,20 @@
101
 
102
  onDestroy(() => window.removeEventListener('resize', updateResponsive));
103
 
104
- // ---- REACTIVE accessors (reassign when `food` changes) --------------
105
- // This is the key fix: these variables CHANGE when `food` changes,
106
- // so all dependent $: blocks recompute.
107
  $: topAcc = food === 'croissant'
108
  ? ((r: Row) => r.croissant_mgdl_delta)
109
  : ((r: Row) => r.white_rice_mgdl_delta);
110
 
111
  $: botAcc = food === 'croissant'
112
- ? ((r: Row) => r.croissant_with_anti_spike_mgdl_delta)
113
- : ((r: Row) => r.white_rice_with_anti_spike_mgdl_delta);
114
-
115
- // ---- Scales (react to rows + accessors) -----------------------------
 
 
 
 
116
  $: if (loaded) {
117
  const xDomain = extent(rows, d => d.time_min) as [number, number];
118
  const yMin = Math.min(...rows.map(topAcc), ...rows.map(botAcc));
@@ -124,6 +139,7 @@
124
  const X = (t: number) => (x ? x(t) : t);
125
  const Y = (v: number) => (y ? y(v) : v);
126
 
 
127
  const makePath = (acc: (r: Row) => number) =>
128
  d3line<Row>().x(d => X(d.time_min)).y(d => Y(acc(d))).curve(curveMonotoneX);
129
  const makeArea = (acc: (r: Row) => number) =>
@@ -138,7 +154,7 @@
138
  areaBot = makeArea(botAcc)(windowed) ?? '';
139
  }
140
 
141
- // Interpolation for tooltips (uses reactive accessors)
142
  const rightAt = bisector<Row, number>(d => d.time_min).right;
143
  function yAtTime(acc: (r: Row) => number, t: number) {
144
  const i = Math.max(0, Math.min(rows.length - 2, rightAt(rows, t) - 1));
@@ -156,7 +172,7 @@
156
  }
157
  function onLeave() { pointerT = null; }
158
 
159
- // Tooltip values (react to food!)
160
  let markerX: number | null = null;
161
  let topVal: number | null = null, botVal: number | null = null;
162
  let topY: number | null = null, botY: number | null = null;
@@ -192,13 +208,10 @@
192
  $: topMsgText = topVal !== null ? msgTop(topVal) : "";
193
  $: botMsgText = botVal !== null ? msgBot(botVal) : "";
194
 
195
- // y-axis title placement
196
- function clamp(n: number, lo: number, hi: number) { return Math.max(lo, Math.min(hi, n)); }
197
- // Fixed pixel anchor so the title doesn't move when the scale changes
198
  $: yTitleX = xTickCol - 6;
199
  $: yTitleY = plotY + (isMobile ? 20 : 30);
200
  $: yTitleDy = yTitleFS + (isMobile ? 28 : 4);
201
-
202
  </script>
203
 
204
  <div class="container">
@@ -211,7 +224,7 @@
211
  <br> <i>Hint: it's interactive so feel free to hover over. </i>
212
  </div>
213
 
214
- <!-- Image toggle (Croissant / Rice) -->
215
  <div class="switcher imgs" role="tablist" aria-label="Choose food">
216
  <button class="imgbtn" class:selected={food==='croissant'}
217
  aria-selected={food==='croissant'}
@@ -227,20 +240,22 @@
227
  </button>
228
  </div>
229
 
230
- <!-- =================== TOP: Selected food (alone) =================== -->
231
- <div class="card" style="position:relative; margin-bottom:14px;">
232
  {#if loaded}
233
  <svg viewBox={`0 0 ${w} ${H}`} style="width:100%;height:auto;touch-action:none;display:block"
234
  on:pointermove={onPtr} on:pointerdown={onPtr} on:pointerleave={onLeave}>
235
 
 
236
  <rect x={plotX} y={plotY} width={plotW} height={Y(30) - plotY} fill="rgba(223,64,133,0.12)" />
237
  <rect x={plotX} y={Y(30)} width={plotW} height={plotBottom - Y(30)} fill="rgba(40,200,120,0.14)" />
238
 
 
239
  <text x={yTitleX} y={yTitleY} fill="#4a4f93" font-size={yTitleFS} text-anchor="end" dominant-baseline="text-before-edge">
240
  glucose <tspan x={yTitleX} dy={yTitleDy} text-anchor="end">(mg/dL)</tspan>
241
  </text>
242
 
243
- <!-- Callout (dynamic) -->
244
  <text x={m.left + leftInset} y={plotY + calloutTop} fill="#111" font-weight="700" font-size={labelFS}>{topLabel}</text>
245
  <image href={topImg} x={m.left + leftInset} y={plotY + calloutTop + labelFS + 6} height={imgH} preserveAspectRatio="xMinYMin meet" />
246
 
@@ -248,6 +263,7 @@
248
  <line x1={plotX} x2={plotX + plotW} y1={Y(0)} y2={Y(0)} stroke="#cfd6ff" stroke-width="2" />
249
  <line x1={plotX} x2={plotX + plotW} y1={Y(30)} y2={Y(30)} stroke="#f8b9d0" stroke-dasharray="6 6" />
250
 
 
251
  {#each [{t:0,label:"eating time"},{t:120,label:"2 hours later"}] as xl}
252
  <text x={X(xl.t)} y={(H - m.bottom) + Math.min(m.bottom - 6, Math.round(axisXFS * 0.9))}
253
  text-anchor="middle" fill="#4a4f93" font-size={axisXFS}>{xl.label}</text>
@@ -256,19 +272,14 @@
256
  <line x1={X(t)} x2={X(t)} y1={plotBottom - xTickLen} y2={plotBottom} stroke="#4a4f93" stroke-width="2" />
257
  {/each}
258
 
259
- <!-- Y ticks in LEFT WHITE MARGIN -->
260
  <text x={xTickCol} y={Y(60) + yOff60} fill="#4a4f93" font-size={axisYFS} text-anchor="end">+60</text>
261
  <text x={xTickCol} y={Y(30) + yOff30} fill="#4a4f93" font-size={axisYFS} text-anchor="end">+30</text>
262
  <text x={xTickCol} y={Y(0) + yOff0 } fill="#4a4f93" font-size={axisYFS} text-anchor="end">baseline</text>
263
 
264
- <!-- Clip above baseline -->
265
- <defs>
266
- <clipPath id="clipTopAboveBaseline">
267
- <rect x={plotX} y={plotY} width={plotW} height={Y(0) - plotY} />
268
- </clipPath>
269
- </defs>
270
-
271
- <path d={areaTop} fill="#111" clip-path="url(#clipTopAboveBaseline)"/>
272
  <path d={pathTop} fill="none" stroke="#111" stroke-width="3.5"/>
273
 
274
  {#if markerX !== null && topY !== null}
@@ -288,8 +299,14 @@
288
  {:else} Loading… {/if}
289
  </div>
290
 
291
- <!-- =================== BOTTOM: With Anti-Spike =================== -->
292
- <div class="card" style="position:relative;">
 
 
 
 
 
 
293
  {#if loaded}
294
  <svg viewBox={`0 0 ${w} ${H}`} style="width:100%;height:auto;touch-action:none;display:block"
295
  on:pointermove={onPtr} on:pointerdown={onPtr} on:pointerleave={onLeave}>
@@ -301,8 +318,9 @@
301
  glucose <tspan x={yTitleX} dy={yTitleDy} text-anchor="end">(mg/dL)</tspan>
302
  </text>
303
 
304
- <text x={m.left + leftInset} y={plotY + calloutTop} fill="#111" font-weight="700" font-size={labelFS}>With Anti-Spike</text>
305
- <image href={ANTISPIKE} x={m.left + leftInset} y={plotY + calloutTop + labelFS + 6} height={imgH} preserveAspectRatio="xMinYMin meet" />
 
306
 
307
  <line x1={plotX} x2={plotX + plotW} y1={Y(0)} y2={Y(0)} stroke="#cfd6ff" stroke-width="2" />
308
  <line x1={plotX} x2={plotX + plotW} y1={Y(30)} y2={Y(30)} stroke="#f8b9d0" stroke-dasharray="6 6" />
@@ -319,13 +337,8 @@
319
  <text x={xTickCol} y={Y(30) + yOff30} fill="#4a4f93" font-size={axisYFS} text-anchor="end">+30</text>
320
  <text x={xTickCol} y={Y(0) + yOff0 } fill="#4a4f93" font-size={axisYFS} text-anchor="end">baseline</text>
321
 
322
- <defs>
323
- <clipPath id="clipBotAboveBaseline">
324
- <rect x={plotX} y={plotY} width={plotW} height={Y(0) - plotY} />
325
- </clipPath>
326
- </defs>
327
-
328
- <path d={areaBot} fill="#111" clip-path="url(#clipBotAboveBaseline)"/>
329
  <path d={pathBot} fill="none" stroke="#111" stroke-width="3.5"/>
330
 
331
  {#if markerX !== null && botY !== null}
@@ -344,4 +357,10 @@
344
  {/if}
345
  {:else} Loading… {/if}
346
  </div>
 
 
 
 
 
 
347
  </div>
 
6
  import { extent, bisector } from 'd3-array';
7
  import { line as d3line, area as d3area, curveMonotoneX } from 'd3-shape';
8
 
9
+ // ---- Data types (now include Veggie Starter columns) ----------------
10
  type Row = {
11
  time_min: number;
12
  croissant_mgdl_delta: number;
13
  croissant_with_anti_spike_mgdl_delta: number;
14
+ croissant_with_veggie_starter_mgdl_delta: number;
15
  white_rice_mgdl_delta: number;
16
  white_rice_with_anti_spike_mgdl_delta: number;
17
+ white_rice_with_veggie_starter_mgdl_delta: number;
18
  };
19
 
20
  let rows: Row[] = [];
21
  let loaded = false;
22
 
23
+ // ---- Toggles --------------------------------------------------------
24
  type Food = 'croissant' | 'rice';
25
+ type Mod = 'anti' | 'veggie';
26
+
27
+ let food: Food = 'croissant'; // default food
28
+ let mod: Mod = 'anti'; // default modifier
29
+
30
  $: topLabel = food === 'croissant' ? 'Croissant' : 'Rice';
31
+ $: bottomHead = mod === 'anti' ? 'With Anti-Spike' : 'With Veggie Starter';
32
+ $: bottomLabel = `${topLabel} + ${mod === 'anti' ? 'Anti-Spike' : 'Veggie Starter'}`;
33
 
34
  // ---- Layout ---------------------------------------------------------
35
  const w = 960;
 
86
 
87
  // Images
88
  const CROISSANT = "/croissant.png";
89
+ const RICE = "/rice.png";
90
  const ANTISPIKE = "/antispike.png";
91
+ const BROCCOLI = "/broccoli.png";
92
+
93
  $: topImg = food === 'croissant' ? CROISSANT : RICE;
94
+ $: bottomImg = mod === 'anti' ? ANTISPIKE : BROCCOLI;
95
 
96
  onMount(async () => {
97
  updateResponsive();
98
  window.addEventListener('resize', updateResponsive);
99
 
100
+ // Use your new CSV with Veggie Starter columns
101
+ const text = await (await fetch('/glucosedata3.csv')).text();
102
  rows = csvParse(text, d => ({
103
  time_min: +d['time_min']!,
104
  croissant_mgdl_delta: +d['croissant_mgdl_delta']!,
105
  croissant_with_anti_spike_mgdl_delta: +d['croissant_with_anti_spike_mgdl_delta']!,
106
+ croissant_with_veggie_starter_mgdl_delta: +d['croissant_with_veggie_starter_mgdl_delta']!,
107
  white_rice_mgdl_delta: +d['white_rice_mgdl_delta']!,
108
+ white_rice_with_anti_spike_mgdl_delta: +d['white_rice_with_anti_spike_mgdl_delta']!,
109
+ white_rice_with_veggie_starter_mgdl_delta: +d['white_rice_with_veggie_starter_mgdl_delta']!
110
  })) as unknown as Row[];
111
 
112
  loaded = true;
 
114
 
115
  onDestroy(() => window.removeEventListener('resize', updateResponsive));
116
 
117
+ // ---- REACTIVE accessors --------------------------------------------
 
 
118
  $: topAcc = food === 'croissant'
119
  ? ((r: Row) => r.croissant_mgdl_delta)
120
  : ((r: Row) => r.white_rice_mgdl_delta);
121
 
122
  $: botAcc = food === 'croissant'
123
+ ? (mod === 'anti'
124
+ ? ((r: Row) => r.croissant_with_anti_spike_mgdl_delta)
125
+ : ((r: Row) => r.croissant_with_veggie_starter_mgdl_delta))
126
+ : (mod === 'anti'
127
+ ? ((r: Row) => r.white_rice_with_anti_spike_mgdl_delta)
128
+ : ((r: Row) => r.white_rice_with_veggie_starter_mgdl_delta));
129
+
130
+ // ---- Scales (auto-fit to the two displayed series) -----------------
131
  $: if (loaded) {
132
  const xDomain = extent(rows, d => d.time_min) as [number, number];
133
  const yMin = Math.min(...rows.map(topAcc), ...rows.map(botAcc));
 
139
  const X = (t: number) => (x ? x(t) : t);
140
  const Y = (v: number) => (y ? y(v) : v);
141
 
142
+ // Paths + filled area (only 0..100 and only above baseline)
143
  const makePath = (acc: (r: Row) => number) =>
144
  d3line<Row>().x(d => X(d.time_min)).y(d => Y(acc(d))).curve(curveMonotoneX);
145
  const makeArea = (acc: (r: Row) => number) =>
 
154
  areaBot = makeArea(botAcc)(windowed) ?? '';
155
  }
156
 
157
+ // Interpolation for tooltips
158
  const rightAt = bisector<Row, number>(d => d.time_min).right;
159
  function yAtTime(acc: (r: Row) => number, t: number) {
160
  const i = Math.max(0, Math.min(rows.length - 2, rightAt(rows, t) - 1));
 
172
  }
173
  function onLeave() { pointerT = null; }
174
 
175
+ // Tooltip values
176
  let markerX: number | null = null;
177
  let topVal: number | null = null, botVal: number | null = null;
178
  let topY: number | null = null, botY: number | null = null;
 
208
  $: topMsgText = topVal !== null ? msgTop(topVal) : "";
209
  $: botMsgText = botVal !== null ? msgBot(botVal) : "";
210
 
211
+ // Fixed pixel anchor so the axis title doesn't move when scale changes
 
 
212
  $: yTitleX = xTickCol - 6;
213
  $: yTitleY = plotY + (isMobile ? 20 : 30);
214
  $: yTitleDy = yTitleFS + (isMobile ? 28 : 4);
 
215
  </script>
216
 
217
  <div class="container">
 
224
  <br> <i>Hint: it's interactive so feel free to hover over. </i>
225
  </div>
226
 
227
+ <!-- Food (image) toggle -->
228
  <div class="switcher imgs" role="tablist" aria-label="Choose food">
229
  <button class="imgbtn" class:selected={food==='croissant'}
230
  aria-selected={food==='croissant'}
 
240
  </button>
241
  </div>
242
 
243
+ <!-- =================== TOP: Food alone =================== -->
244
+ <div class="card" style="position:relative; margin-bottom:6px;">
245
  {#if loaded}
246
  <svg viewBox={`0 0 ${w} ${H}`} style="width:100%;height:auto;touch-action:none;display:block"
247
  on:pointermove={onPtr} on:pointerdown={onPtr} on:pointerleave={onLeave}>
248
 
249
+ <!-- Bands -->
250
  <rect x={plotX} y={plotY} width={plotW} height={Y(30) - plotY} fill="rgba(223,64,133,0.12)" />
251
  <rect x={plotX} y={Y(30)} width={plotW} height={plotBottom - Y(30)} fill="rgba(40,200,120,0.14)" />
252
 
253
+ <!-- y-axis title -->
254
  <text x={yTitleX} y={yTitleY} fill="#4a4f93" font-size={yTitleFS} text-anchor="end" dominant-baseline="text-before-edge">
255
  glucose <tspan x={yTitleX} dy={yTitleDy} text-anchor="end">(mg/dL)</tspan>
256
  </text>
257
 
258
+ <!-- Callout -->
259
  <text x={m.left + leftInset} y={plotY + calloutTop} fill="#111" font-weight="700" font-size={labelFS}>{topLabel}</text>
260
  <image href={topImg} x={m.left + leftInset} y={plotY + calloutTop + labelFS + 6} height={imgH} preserveAspectRatio="xMinYMin meet" />
261
 
 
263
  <line x1={plotX} x2={plotX + plotW} y1={Y(0)} y2={Y(0)} stroke="#cfd6ff" stroke-width="2" />
264
  <line x1={plotX} x2={plotX + plotW} y1={Y(30)} y2={Y(30)} stroke="#f8b9d0" stroke-dasharray="6 6" />
265
 
266
+ <!-- X labels + ticks -->
267
  {#each [{t:0,label:"eating time"},{t:120,label:"2 hours later"}] as xl}
268
  <text x={X(xl.t)} y={(H - m.bottom) + Math.min(m.bottom - 6, Math.round(axisXFS * 0.9))}
269
  text-anchor="middle" fill="#4a4f93" font-size={axisXFS}>{xl.label}</text>
 
272
  <line x1={X(t)} x2={X(t)} y1={plotBottom - xTickLen} y2={plotBottom} stroke="#4a4f93" stroke-width="2" />
273
  {/each}
274
 
275
+ <!-- Y ticks -->
276
  <text x={xTickCol} y={Y(60) + yOff60} fill="#4a4f93" font-size={axisYFS} text-anchor="end">+60</text>
277
  <text x={xTickCol} y={Y(30) + yOff30} fill="#4a4f93" font-size={axisYFS} text-anchor="end">+30</text>
278
  <text x={xTickCol} y={Y(0) + yOff0 } fill="#4a4f93" font-size={axisYFS} text-anchor="end">baseline</text>
279
 
280
+ <!-- Clip + path -->
281
+ <defs><clipPath id="clipTop"><rect x={plotX} y={plotY} width={plotW} height={Y(0) - plotY} /></clipPath></defs>
282
+ <path d={areaTop} fill="#111" clip-path="url(#clipTop)"/>
 
 
 
 
 
283
  <path d={pathTop} fill="none" stroke="#111" stroke-width="3.5"/>
284
 
285
  {#if markerX !== null && topY !== null}
 
299
  {:else} Loading… {/if}
300
  </div>
301
 
302
+ <!-- Modifier buttons (affect bottom plot) -->
303
+ <div class="switcher" role="tablist" aria-label="Choose modifier for comparison below" style="margin-bottom:14px;">
304
+ <button class:selected={mod==='anti'} aria-selected={mod==='anti'} on:click={() => mod='anti'}>Anti-Spike</button>
305
+ <button class:selected={mod==='veggie'} aria-selected={mod==='veggie'} on:click={() => mod='veggie'}>Veggie Starter</button>
306
+ </div>
307
+
308
+ <!-- =================== BOTTOM: Food + modifier =================== -->
309
+ <div class="card" style="position:relative; margin-bottom:6px;">
310
  {#if loaded}
311
  <svg viewBox={`0 0 ${w} ${H}`} style="width:100%;height:auto;touch-action:none;display:block"
312
  on:pointermove={onPtr} on:pointerdown={onPtr} on:pointerleave={onLeave}>
 
318
  glucose <tspan x={yTitleX} dy={yTitleDy} text-anchor="end">(mg/dL)</tspan>
319
  </text>
320
 
321
+ <!-- Reactive callout for modifier -->
322
+ <text x={m.left + leftInset} y={plotY + calloutTop} fill="#111" font-weight="700" font-size={labelFS}>{bottomHead}</text>
323
+ <image href={bottomImg} x={m.left + leftInset} y={plotY + calloutTop + labelFS + 6} height={imgH} preserveAspectRatio="xMinYMin meet" />
324
 
325
  <line x1={plotX} x2={plotX + plotW} y1={Y(0)} y2={Y(0)} stroke="#cfd6ff" stroke-width="2" />
326
  <line x1={plotX} x2={plotX + plotW} y1={Y(30)} y2={Y(30)} stroke="#f8b9d0" stroke-dasharray="6 6" />
 
337
  <text x={xTickCol} y={Y(30) + yOff30} fill="#4a4f93" font-size={axisYFS} text-anchor="end">+30</text>
338
  <text x={xTickCol} y={Y(0) + yOff0 } fill="#4a4f93" font-size={axisYFS} text-anchor="end">baseline</text>
339
 
340
+ <defs><clipPath id="clipBot"><rect x={plotX} y={plotY} width={plotW} height={Y(0) - plotY} /></clipPath></defs>
341
+ <path d={areaBot} fill="#111" clip-path="url(#clipBot)"/>
 
 
 
 
 
342
  <path d={pathBot} fill="none" stroke="#111" stroke-width="3.5"/>
343
 
344
  {#if markerX !== null && botY !== null}
 
357
  {/if}
358
  {:else} Loading… {/if}
359
  </div>
360
+
361
+ <!-- Duplicate modifier buttons below the bottom plot (as requested) -->
362
+ <div class="switcher" role="tablist" aria-label="Choose modifier (bottom)">
363
+ <button class:selected={mod==='anti'} aria-selected={mod==='anti'} on:click={() => mod='anti'}>Anti-Spike</button>
364
+ <button class:selected={mod==='veggie'} aria-selected={mod==='veggie'} on:click={() => mod='veggie'}>Veggie Starter</button>
365
+ </div>
366
  </div>