tonyassi commited on
Commit
caa850f
·
verified ·
1 Parent(s): 88f3b01

Update src/App.svelte

Browse files
Files changed (1) hide show
  1. src/App.svelte +67 -86
src/App.svelte CHANGED
@@ -10,96 +10,73 @@
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
  /** -------- Layout --------------------------------------------------- **/
19
  const w = 960;
20
  let H = 432; // desktop height
 
21
 
22
- // ➜ Add more left white space so y-labels never clip
23
- let m = { top: 20, right: 11, bottom: 28, left: 92 }; // desktop LEFT was ~54
24
-
25
- // Derived plot rect
26
  $: plotX = m.left;
27
  $: plotY = m.top;
28
  $: plotW = w - m.left - m.right;
29
  $: plotBottom = H - m.bottom;
30
 
31
- // Callouts (down a touch)
32
  const leftInset = 54;
33
  let labelFS = 18;
34
  let imgH = 100;
 
35
  $: calloutTop = isMobile ? 26 : 24;
36
 
37
- // Axes font sizes
38
  let axisXFS = 16;
39
  let axisYFS = 14;
40
-
41
- // y-axis title “glucose (mg/dL)”
42
  let yTitleFS = 12;
43
 
44
- // Tooltip position
45
  let tooltipW = 200;
46
  let tooltipTop = 28;
47
  let tooltipRight = 120;
48
 
49
- // X ticks (inside the band, centered under labels)
50
  const xTicksVals = [0, 120];
51
  $: xTickLen = 10;
52
-
53
- let isMobile = false;
54
-
55
- // ➜ Column where y-tick TEXT sits, to the LEFT of the plot (inside white area)
56
- // We keep a comfortable gap so long labels don’t clip.
57
  $: xTickCol = m.left - 12;
58
-
59
- // Fine vertical nudges for tick text
60
  $: yOff60 = 30;
61
  $: yOff30 = 6;
62
  $: yOff0 = -8;
63
 
64
  function updateResponsive() {
65
  isMobile = window.matchMedia('(max-width: 640px)').matches;
66
-
67
  if (isMobile) {
68
  H = Math.round(432 * 1.5);
69
- // ➜ Add extra left space on mobile too so “baseline” never clips
70
  m = { top: 22, right: 11, bottom: 56, left: 125 };
71
-
72
- labelFS = 32;
73
- imgH = 130;
74
- axisXFS = 32;
75
- axisYFS = 28;
76
- yTitleFS = 28;
77
- tooltipW = 130;
78
- tooltipTop = 24;
79
- tooltipRight = 12;
80
  } else {
81
  H = 432;
82
  m = { top: 20, right: 11, bottom: 28, left: 85 };
83
-
84
- labelFS = 18;
85
- imgH = 100;
86
- axisXFS = 16;
87
- axisYFS = 14;
88
- yTitleFS = 12;
89
- tooltipW = 200;
90
- tooltipTop = 28;
91
- tooltipRight = 120;
92
  }
93
  }
94
 
95
  let x: ((n: number) => number) | null = null;
96
  let y: ((n: number) => number) | null = null;
97
-
98
- // Pointer state
99
  let pointerT: number | null = null;
100
 
101
  const CROISSANT = "/croissant.png";
 
102
  const ANTISPIKE = "/antispike.png";
 
103
 
104
  onMount(async () => {
105
  updateResponsive();
@@ -109,7 +86,9 @@
109
  rows = csvParse(text, d => ({
110
  time_min: +d['time_min']!,
111
  croissant_mgdl_delta: +d['croissant_mgdl_delta']!,
112
- croissant_with_anti_spike_mgdl_delta: +d['croissant_with_anti_spike_mgdl_delta']!
 
 
113
  })) as unknown as Row[];
114
 
115
  loaded = true;
@@ -117,11 +96,17 @@
117
 
118
  onDestroy(() => window.removeEventListener('resize', updateResponsive));
119
 
 
 
 
 
 
 
120
  /** -------- Scales --------------------------------------------------- **/
121
  $: if (loaded) {
122
  const xDomain = extent(rows, d => d.time_min) as [number, number];
123
- const yMin = Math.min(...rows.map(r => Math.min(r.croissant_mgdl_delta, r.croissant_with_anti_spike_mgdl_delta)));
124
- const yMax = Math.max(...rows.map(r => Math.max(r.croissant_mgdl_delta, r.croissant_with_anti_spike_mgdl_delta)));
125
  x = scaleLinear().domain(xDomain).range([m.left, m.left + (w - m.left - m.right)]);
126
  y = scaleLinear().domain([yMin - 5, yMax + 5]).range([H - m.bottom, m.top]);
127
  }
@@ -129,7 +114,6 @@
129
  const X = (t: number) => (x ? x(t) : t);
130
  const Y = (v: number) => (y ? y(v) : v);
131
 
132
- // Paths + filled area (only 0..100 and only above baseline)
133
  const makePath = (acc: (r: Row) => number) =>
134
  d3line<Row>().x(d => X(d.time_min)).y(d => Y(acc(d))).curve(curveMonotoneX);
135
  const makeArea = (acc: (r: Row) => number) =>
@@ -138,13 +122,12 @@
138
  let pathTop = '', pathBot = '', areaTop = '', areaBot = '';
139
  $: if (loaded) {
140
  const windowed = rows.filter(d => d.time_min >= 0 && d.time_min <= 100);
141
- pathTop = makePath(d => d.croissant_mgdl_delta)(rows) ?? '';
142
- pathBot = makePath(d => d.croissant_with_anti_spike_mgdl_delta)(rows) ?? '';
143
- areaTop = makeArea(d => d.croissant_mgdl_delta)(windowed) ?? '';
144
- areaBot = makeArea(d => d.croissant_with_anti_spike_mgdl_delta)(windowed) ?? '';
145
  }
146
 
147
- // Interpolation
148
  const rightAt = bisector<Row, number>(d => d.time_min).right;
149
  function yAtTime(acc: (r: Row) => number, t: number) {
150
  const i = Math.max(0, Math.min(rows.length - 2, rightAt(rows, t) - 1));
@@ -169,8 +152,8 @@
169
 
170
  $: if (loaded && pointerT !== null && y) {
171
  markerX = X(pointerT);
172
- topVal = yAtTime(d => d.croissant_mgdl_delta, pointerT);
173
- botVal = yAtTime(d => d.croissant_with_anti_spike_mgdl_delta, pointerT);
174
  topY = Y(topVal);
175
  botY = Y(botVal);
176
  } else {
@@ -189,7 +172,7 @@
189
  : "At baseline";
190
 
191
  const msgBot = (v: number) =>
192
- v >= 22 ? "Reduced spike vs croissant alone"
193
  : v >= 12 ? "Smaller spike → steadier energy"
194
  : v <= 2 && v >= -2 ? "Near baseline ✅"
195
  : v < -1 ? "Gentle dip, then recovery"
@@ -197,17 +180,9 @@
197
 
198
  $: topMsgText = topVal !== null ? msgTop(topVal) : "";
199
  $: botMsgText = botVal !== null ? msgBot(botVal) : "";
200
- const avenir = `'Avenir Next', Avenir, 'Helvetica Neue', Arial, sans-serif`;
201
- const serif = `var(--font-serif)`;
202
- $: topMsgFont = topMsgText === "At baseline" ? avenir : serif;
203
- $: botMsgFont = botMsgText === "At baseline" ? avenir : serif;
204
-
205
- // X-label Y in white margin (tight)
206
- $: xLabelY = (H - m.bottom) + Math.min(m.bottom - 6, Math.round(axisXFS * 0.9));
207
 
208
- // y-axis title ABOVE +60 and to the LEFT of the +60 label
209
  function clamp(n: number, lo: number, hi: number) { return Math.max(lo, Math.min(hi, n)); }
210
- $: yTitleX = xTickCol - 6; // move a bit left of the +60 text column
211
  $: yTitleY = (loaded && y)
212
  ? (Y(60) - (yTitleFS * 2 + (isMobile ? -110 : -70)))
213
  : plotY + 6;
@@ -224,44 +199,52 @@
224
  <br> <i>Hint: it's interactive so feel free to hover over. </i>
225
  </div>
226
 
227
- <!-- =================== TOP: Without modifier =================== -->
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  <div class="card" style="position:relative; margin-bottom:14px;">
229
  {#if loaded}
230
  <svg viewBox={`0 0 ${w} ${H}`} style="width:100%;height:auto;touch-action:none;display:block"
231
  on:pointermove={onPtr} on:pointerdown={onPtr} on:pointerleave={onLeave}>
232
 
233
- <!-- Bands inside plot box -->
234
  <rect x={plotX} y={plotY} width={plotW} height={Y(30) - plotY} fill="rgba(223,64,133,0.12)" />
235
  <rect x={plotX} y={Y(30)} width={plotW} height={plotBottom - Y(30)} fill="rgba(40,200,120,0.14)" />
236
 
237
- <!-- y-axis title ABOVE +60 and LEFT of tick labels -->
238
  <text x={yTitleX} y={yTitleY} fill="#4a4f93" font-size={yTitleFS} text-anchor="end" dominant-baseline="text-before-edge">
239
- glucose
240
- <tspan x={yTitleX} dy={yTitleDy} text-anchor="end">(mg/dL)</tspan>
241
  </text>
242
 
243
- <!-- Callout -->
244
- <text x={m.left + leftInset} y={plotY + calloutTop} fill="#111" font-weight="700" font-size={labelFS}>Without Anti-Spike</text>
245
- <image href={CROISSANT} x={m.left + leftInset} y={plotY + calloutTop + labelFS + 6} height={imgH} preserveAspectRatio="xMinYMin meet" />
246
 
247
- <!-- Guides -->
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
- <!-- X labels + inside ticks -->
252
- {#each xLabels as xl}
253
- <text x={X(xl.t)} y={xLabelY} text-anchor="middle" fill="#4a4f93" font-size={axisXFS}>{xl.label}</text>
254
  {/each}
255
  {#each xTicksVals as t}
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} />
@@ -279,11 +262,11 @@
279
 
280
  {#if topVal !== null}
281
  <div class="tooltip" style={`top:${tooltipTop}px; right:${tooltipRight}px; left:auto; width:${tooltipW}px;`}>
282
- <div style={`font-weight:700; font-family:${topMsgText === 'At baseline' ? "'Avenir Next', Avenir, 'Helvetica Neue', Arial, sans-serif" : "var(--font-serif)"}; color:#111; font-size:14px; margin-bottom:6px;`}>
283
  {topMsgText}
284
  </div>
285
  <div style="display:flex;justify-content:space-between;gap:12px;">
286
- <div>Croissant</div>
287
  <strong>{topVal.toFixed(0)} mg/dL</strong>
288
  </div>
289
  </div>
@@ -300,10 +283,8 @@
300
  <rect x={plotX} y={plotY} width={plotW} height={Y(30) - plotY} fill="rgba(223,64,133,0.12)" />
301
  <rect x={plotX} y={Y(30)} width={plotW} height={plotBottom - Y(30)} fill="rgba(40,200,120,0.14)" />
302
 
303
- <!-- y-axis title ABOVE +60 and LEFT of tick labels -->
304
  <text x={yTitleX} y={yTitleY} fill="#4a4f93" font-size={yTitleFS} text-anchor="end" dominant-baseline="text-before-edge">
305
- glucose
306
- <tspan x={yTitleX} dy={yTitleDy} text-anchor="end">(mg/dL)</tspan>
307
  </text>
308
 
309
  <text x={m.left + leftInset} y={plotY + calloutTop} fill="#111" font-weight="700" font-size={labelFS}>With Anti-Spike</text>
@@ -312,17 +293,17 @@
312
  <line x1={plotX} x2={plotX + plotW} y1={Y(0)} y2={Y(0)} stroke="#cfd6ff" stroke-width="2" />
313
  <line x1={plotX} x2={plotX + plotW} y1={Y(30)} y2={Y(30)} stroke="#f8b9d0" stroke-dasharray="6 6" />
314
 
315
- {#each xLabels as xl}
316
- <text x={X(xl.t)} y={xLabelY} text-anchor="middle" fill="#4a4f93" font-size={axisXFS}>{xl.label}</text>
 
317
  {/each}
318
  {#each xTicksVals as t}
319
  <line x1={X(t)} x2={X(t)} y1={plotBottom - xTickLen} y2={plotBottom} stroke="#4a4f93" stroke-width="2" />
320
  {/each}
321
 
322
- <!-- Y ticks in LEFT WHITE MARGIN -->
323
  <text x={xTickCol} y={Y(60) + yOff60} fill="#4a4f93" font-size={axisYFS} text-anchor="end">+60</text>
324
  <text x={xTickCol} y={Y(30) + yOff30} fill="#4a4f93" font-size={axisYFS} text-anchor="end">+30</text>
325
- <text x={xTickCol} y={Y(0) + yOff0 } fill="#4a4f93" font-size={axisYFS} text-anchor="end">baseline</text>
326
 
327
  <defs>
328
  <clipPath id="clipBotAboveBaseline">
@@ -341,11 +322,11 @@
341
 
342
  {#if botVal !== null}
343
  <div class="tooltip" style={`top:${tooltipTop}px; right:${tooltipRight}px; left:auto; width:${tooltipW}px;`}>
344
- <div style={`font-weight:700; font-family:${botMsgText === 'At baseline' ? "'Avenir Next', Avenir, 'Helvetica Neue', Arial, sans-serif" : "var(--font-serif)"}; color:#111; font-size:14px; margin-bottom:6px;`}>
345
  {botMsgText}
346
  </div>
347
  <div style="display:flex;justify-content:space-between;gap:12px;">
348
- <div>Croissant + Anti-Spike</div>
349
  <strong>{botVal.toFixed(0)} mg/dL</strong>
350
  </div>
351
  </div>
 
10
  time_min: number;
11
  croissant_mgdl_delta: number;
12
  croissant_with_anti_spike_mgdl_delta: number;
13
+ white_rice_mgdl_delta: number;
14
+ white_rice_with_anti_spike_mgdl_delta: number;
15
  };
16
 
17
  let rows: Row[] = [];
18
  let loaded = false;
19
 
20
+ /** -------- Food toggle -------------------------------------------- **/
21
+ type Food = 'croissant' | 'rice';
22
+ let food: Food = 'croissant'; // default selection
23
+ $: topLabel = food === 'croissant' ? 'Croissant' : 'Rice';
24
+ $: bottomLabel = `${topLabel} + Anti-Spike`;
25
+
26
  /** -------- Layout --------------------------------------------------- **/
27
  const w = 960;
28
  let H = 432; // desktop height
29
+ let m = { top: 20, right: 11, bottom: 28, left: 92 };
30
 
 
 
 
 
31
  $: plotX = m.left;
32
  $: plotY = m.top;
33
  $: plotW = w - m.left - m.right;
34
  $: plotBottom = H - m.bottom;
35
 
 
36
  const leftInset = 54;
37
  let labelFS = 18;
38
  let imgH = 100;
39
+ let isMobile = false;
40
  $: calloutTop = isMobile ? 26 : 24;
41
 
 
42
  let axisXFS = 16;
43
  let axisYFS = 14;
 
 
44
  let yTitleFS = 12;
45
 
 
46
  let tooltipW = 200;
47
  let tooltipTop = 28;
48
  let tooltipRight = 120;
49
 
 
50
  const xTicksVals = [0, 120];
51
  $: xTickLen = 10;
 
 
 
 
 
52
  $: xTickCol = m.left - 12;
 
 
53
  $: yOff60 = 30;
54
  $: yOff30 = 6;
55
  $: yOff0 = -8;
56
 
57
  function updateResponsive() {
58
  isMobile = window.matchMedia('(max-width: 640px)').matches;
 
59
  if (isMobile) {
60
  H = Math.round(432 * 1.5);
 
61
  m = { top: 22, right: 11, bottom: 56, left: 125 };
62
+ labelFS = 32; imgH = 130; axisXFS = 32; axisYFS = 28; yTitleFS = 28;
63
+ tooltipW = 130; tooltipTop = 24; tooltipRight = 12;
 
 
 
 
 
 
 
64
  } else {
65
  H = 432;
66
  m = { top: 20, right: 11, bottom: 28, left: 85 };
67
+ labelFS = 18; imgH = 100; axisXFS = 16; axisYFS = 14; yTitleFS = 12;
68
+ tooltipW = 200; tooltipTop = 28; tooltipRight = 120;
 
 
 
 
 
 
 
69
  }
70
  }
71
 
72
  let x: ((n: number) => number) | null = null;
73
  let y: ((n: number) => number) | null = null;
 
 
74
  let pointerT: number | null = null;
75
 
76
  const CROISSANT = "/croissant.png";
77
+ const RICE = "/rice.png";
78
  const ANTISPIKE = "/antispike.png";
79
+ $: topImg = food === 'croissant' ? CROISSANT : RICE;
80
 
81
  onMount(async () => {
82
  updateResponsive();
 
86
  rows = csvParse(text, d => ({
87
  time_min: +d['time_min']!,
88
  croissant_mgdl_delta: +d['croissant_mgdl_delta']!,
89
+ croissant_with_anti_spike_mgdl_delta: +d['croissant_with_anti_spike_mgdl_delta']!,
90
+ white_rice_mgdl_delta: +d['white_rice_mgdl_delta']!,
91
+ white_rice_with_anti_spike_mgdl_delta: +d['white_rice_with_anti_spike_mgdl_delta']!
92
  })) as unknown as Row[];
93
 
94
  loaded = true;
 
96
 
97
  onDestroy(() => window.removeEventListener('resize', updateResponsive));
98
 
99
+ /** -------- Accessors (react to toggle) ----------------------------- **/
100
+ const topAcc = (r: Row) =>
101
+ food === 'croissant' ? r.croissant_mgdl_delta : r.white_rice_mgdl_delta;
102
+ const botAcc = (r: Row) =>
103
+ food === 'croissant' ? r.croissant_with_anti_spike_mgdl_delta : r.white_rice_with_anti_spike_mgdl_delta;
104
+
105
  /** -------- Scales --------------------------------------------------- **/
106
  $: if (loaded) {
107
  const xDomain = extent(rows, d => d.time_min) as [number, number];
108
+ const yMin = Math.min(...rows.map(topAcc), ...rows.map(botAcc));
109
+ const yMax = Math.max(...rows.map(topAcc), ...rows.map(botAcc));
110
  x = scaleLinear().domain(xDomain).range([m.left, m.left + (w - m.left - m.right)]);
111
  y = scaleLinear().domain([yMin - 5, yMax + 5]).range([H - m.bottom, m.top]);
112
  }
 
114
  const X = (t: number) => (x ? x(t) : t);
115
  const Y = (v: number) => (y ? y(v) : v);
116
 
 
117
  const makePath = (acc: (r: Row) => number) =>
118
  d3line<Row>().x(d => X(d.time_min)).y(d => Y(acc(d))).curve(curveMonotoneX);
119
  const makeArea = (acc: (r: Row) => number) =>
 
122
  let pathTop = '', pathBot = '', areaTop = '', areaBot = '';
123
  $: if (loaded) {
124
  const windowed = rows.filter(d => d.time_min >= 0 && d.time_min <= 100);
125
+ pathTop = makePath(topAcc)(rows) ?? '';
126
+ pathBot = makePath(botAcc)(rows) ?? '';
127
+ areaTop = makeArea(topAcc)(windowed) ?? '';
128
+ areaBot = makeArea(botAcc)(windowed) ?? '';
129
  }
130
 
 
131
  const rightAt = bisector<Row, number>(d => d.time_min).right;
132
  function yAtTime(acc: (r: Row) => number, t: number) {
133
  const i = Math.max(0, Math.min(rows.length - 2, rightAt(rows, t) - 1));
 
152
 
153
  $: if (loaded && pointerT !== null && y) {
154
  markerX = X(pointerT);
155
+ topVal = yAtTime(topAcc, pointerT);
156
+ botVal = yAtTime(botAcc, pointerT);
157
  topY = Y(topVal);
158
  botY = Y(botVal);
159
  } else {
 
172
  : "At baseline";
173
 
174
  const msgBot = (v: number) =>
175
+ v >= 22 ? "Reduced spike vs alone"
176
  : v >= 12 ? "Smaller spike → steadier energy"
177
  : v <= 2 && v >= -2 ? "Near baseline ✅"
178
  : v < -1 ? "Gentle dip, then recovery"
 
180
 
181
  $: topMsgText = topVal !== null ? msgTop(topVal) : "";
182
  $: botMsgText = botVal !== null ? msgBot(botVal) : "";
 
 
 
 
 
 
 
183
 
 
184
  function clamp(n: number, lo: number, hi: number) { return Math.max(lo, Math.min(hi, n)); }
185
+ $: yTitleX = xTickCol - 6;
186
  $: yTitleY = (loaded && y)
187
  ? (Y(60) - (yTitleFS * 2 + (isMobile ? -110 : -70)))
188
  : plotY + 6;
 
199
  <br> <i>Hint: it's interactive so feel free to hover over. </i>
200
  </div>
201
 
202
+ <!-- Toggle buttons -->
203
+ <div class="switcher" role="tablist" aria-label="Choose food">
204
+ <button class:selected={food==='croissant'}
205
+ aria-selected={food==='croissant'}
206
+ on:click={() => (food='croissant')}>
207
+ Croissant
208
+ </button>
209
+ <button class:selected={food==='rice'}
210
+ aria-selected={food==='rice'}
211
+ on:click={() => (food='rice')}>
212
+ Rice
213
+ </button>
214
+ </div>
215
+
216
+ <!-- =================== TOP: Selected food =================== -->
217
  <div class="card" style="position:relative; margin-bottom:14px;">
218
  {#if loaded}
219
  <svg viewBox={`0 0 ${w} ${H}`} style="width:100%;height:auto;touch-action:none;display:block"
220
  on:pointermove={onPtr} on:pointerdown={onPtr} on:pointerleave={onLeave}>
221
 
 
222
  <rect x={plotX} y={plotY} width={plotW} height={Y(30) - plotY} fill="rgba(223,64,133,0.12)" />
223
  <rect x={plotX} y={Y(30)} width={plotW} height={plotBottom - Y(30)} fill="rgba(40,200,120,0.14)" />
224
 
 
225
  <text x={yTitleX} y={yTitleY} fill="#4a4f93" font-size={yTitleFS} text-anchor="end" dominant-baseline="text-before-edge">
226
+ glucose <tspan x={yTitleX} dy={yTitleDy} text-anchor="end">(mg/dL)</tspan>
 
227
  </text>
228
 
229
+ <!-- Dynamic callout -->
230
+ <text x={m.left + leftInset} y={plotY + calloutTop} fill="#111" font-weight="700" font-size={labelFS}>{topLabel}</text>
231
+ <image href={topImg} x={m.left + leftInset} y={plotY + calloutTop + labelFS + 6} height={imgH} preserveAspectRatio="xMinYMin meet" />
232
 
 
233
  <line x1={plotX} x2={plotX + plotW} y1={Y(0)} y2={Y(0)} stroke="#cfd6ff" stroke-width="2" />
234
  <line x1={plotX} x2={plotX + plotW} y1={Y(30)} y2={Y(30)} stroke="#f8b9d0" stroke-dasharray="6 6" />
235
 
236
+ {#each [{t:0,label:"eating time"},{t:120,label:"2 hours later"}] as xl}
237
+ <text x={X(xl.t)} y={(H - m.bottom) + Math.min(m.bottom - 6, Math.round(axisXFS * 0.9))}
238
+ text-anchor="middle" fill="#4a4f93" font-size={axisXFS}>{xl.label}</text>
239
  {/each}
240
  {#each xTicksVals as t}
241
  <line x1={X(t)} x2={X(t)} y1={plotBottom - xTickLen} y2={plotBottom} stroke="#4a4f93" stroke-width="2" />
242
  {/each}
243
 
 
244
  <text x={xTickCol} y={Y(60) + yOff60} fill="#4a4f93" font-size={axisYFS} text-anchor="end">+60</text>
245
  <text x={xTickCol} y={Y(30) + yOff30} fill="#4a4f93" font-size={axisYFS} text-anchor="end">+30</text>
246
+ <text x={xTickCol} y={Y(0) + yOff0 } fill="#4a4f93" font-size={axisYFS} text-anchor="end">baseline</text>
247
 
 
248
  <defs>
249
  <clipPath id="clipTopAboveBaseline">
250
  <rect x={plotX} y={plotY} width={plotW} height={Y(0) - plotY} />
 
262
 
263
  {#if topVal !== null}
264
  <div class="tooltip" style={`top:${tooltipTop}px; right:${tooltipRight}px; left:auto; width:${tooltipW}px;`}>
265
+ <div style="font-weight:700; color:#111; font-size:14px; margin-bottom:6px;">
266
  {topMsgText}
267
  </div>
268
  <div style="display:flex;justify-content:space-between;gap:12px;">
269
+ <div>{topLabel}</div>
270
  <strong>{topVal.toFixed(0)} mg/dL</strong>
271
  </div>
272
  </div>
 
283
  <rect x={plotX} y={plotY} width={plotW} height={Y(30) - plotY} fill="rgba(223,64,133,0.12)" />
284
  <rect x={plotX} y={Y(30)} width={plotW} height={plotBottom - Y(30)} fill="rgba(40,200,120,0.14)" />
285
 
 
286
  <text x={yTitleX} y={yTitleY} fill="#4a4f93" font-size={yTitleFS} text-anchor="end" dominant-baseline="text-before-edge">
287
+ glucose <tspan x={yTitleX} dy={yTitleDy} text-anchor="end">(mg/dL)</tspan>
 
288
  </text>
289
 
290
  <text x={m.left + leftInset} y={plotY + calloutTop} fill="#111" font-weight="700" font-size={labelFS}>With Anti-Spike</text>
 
293
  <line x1={plotX} x2={plotX + plotW} y1={Y(0)} y2={Y(0)} stroke="#cfd6ff" stroke-width="2" />
294
  <line x1={plotX} x2={plotX + plotW} y1={Y(30)} y2={Y(30)} stroke="#f8b9d0" stroke-dasharray="6 6" />
295
 
296
+ {#each [{t:0,label:"eating time"},{t:120,label:"2 hours later"}] as xl}
297
+ <text x={X(xl.t)} y={(H - m.bottom) + Math.min(m.bottom - 6, Math.round(axisXFS * 0.9))}
298
+ text-anchor="middle" fill="#4a4f93" font-size={axisXFS}>{xl.label}</text>
299
  {/each}
300
  {#each xTicksVals as t}
301
  <line x1={X(t)} x2={X(t)} y1={plotBottom - xTickLen} y2={plotBottom} stroke="#4a4f93" stroke-width="2" />
302
  {/each}
303
 
 
304
  <text x={xTickCol} y={Y(60) + yOff60} fill="#4a4f93" font-size={axisYFS} text-anchor="end">+60</text>
305
  <text x={xTickCol} y={Y(30) + yOff30} fill="#4a4f93" font-size={axisYFS} text-anchor="end">+30</text>
306
+ <text x={xTickCol} y={Y(0) + yOff0 } fill="#4a4f93" font-size={axisYFS} text-anchor="end">baseline</text>
307
 
308
  <defs>
309
  <clipPath id="clipBotAboveBaseline">
 
322
 
323
  {#if botVal !== null}
324
  <div class="tooltip" style={`top:${tooltipTop}px; right:${tooltipRight}px; left:auto; width:${tooltipW}px;`}>
325
+ <div style="font-weight:700; color:#111; font-size:14px; margin-bottom:6px;">
326
  {botMsgText}
327
  </div>
328
  <div style="display:flex;justify-content:space-between;gap:12px;">
329
+ <div>{bottomLabel}</div>
330
  <strong>{botVal.toFixed(0)} mg/dL</strong>
331
  </div>
332
  </div>