tonyassi commited on
Commit
04eb05c
·
verified ·
1 Parent(s): 6be826d

Update src/App.svelte

Browse files
Files changed (1) hide show
  1. src/App.svelte +81 -26
src/App.svelte CHANGED
@@ -6,6 +6,7 @@
6
  import { extent, bisector } from 'd3-array';
7
  import { line as d3line, area as d3area, curveMonotoneX } from 'd3-shape';
8
 
 
9
  type Row = {
10
  time_min: number;
11
  croissant_mgdl_delta: number;
@@ -17,62 +18,93 @@
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";
@@ -82,6 +114,7 @@
82
  updateResponsive();
83
  window.addEventListener('resize', updateResponsive);
84
 
 
85
  const text = await (await fetch('/glucosedata2.csv')).text();
86
  rows = csvParse(text, d => ({
87
  time_min: +d['time_min']!,
@@ -96,17 +129,21 @@
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,6 +151,7 @@
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,12 +160,14 @@
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));
@@ -145,7 +185,7 @@
145
  }
146
  function onLeave() { pointerT = null; }
147
 
148
- // Tooltip values
149
  let markerX: number | null = null;
150
  let topVal: number | null = null, botVal: number | null = null;
151
  let topY: number | null = null, botY: number | null = null;
@@ -181,6 +221,7 @@
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)
@@ -199,40 +240,49 @@
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>
@@ -241,10 +291,12 @@
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} />
@@ -283,8 +335,10 @@
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,17 +347,18 @@
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">
 
6
  import { extent, bisector } from 'd3-array';
7
  import { line as d3line, area as d3area, curveMonotoneX } from 'd3-shape';
8
 
9
+ /** ---------- Data types (now includes rice columns) ----------------- */
10
  type Row = {
11
  time_min: number;
12
  croissant_mgdl_delta: number;
 
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;
29
  let H = 432; // desktop height
30
+
31
+ // Add more left white space so y-labels never clip
32
  let m = { top: 20, right: 11, bottom: 28, left: 92 };
33
 
34
+ // Derived plot rect
35
  $: plotX = m.left;
36
  $: plotY = m.top;
37
  $: plotW = w - m.left - m.right;
38
  $: plotBottom = H - m.bottom;
39
 
40
+ // Callouts
41
  const leftInset = 54;
42
  let labelFS = 18;
43
  let imgH = 100;
44
  let isMobile = false;
45
  $: calloutTop = isMobile ? 26 : 24;
46
 
47
+ // Axes font sizes
48
  let axisXFS = 16;
49
  let axisYFS = 14;
50
+
51
+ // y-axis title “glucose (mg/dL)”
52
  let yTitleFS = 12;
53
 
54
+ // Tooltip position
55
  let tooltipW = 200;
56
  let tooltipTop = 28;
57
  let tooltipRight = 120;
58
 
59
+ // X ticks (inside the band, centered under labels)
60
  const xTicksVals = [0, 120];
61
  $: xTickLen = 10;
62
+
63
+ // Column where y-tick TEXT sits
64
  $: xTickCol = m.left - 12;
65
+
66
+ // Fine vertical nudges for tick text
67
  $: yOff60 = 30;
68
  $: yOff30 = 6;
69
  $: yOff0 = -8;
70
 
71
  function updateResponsive() {
72
  isMobile = window.matchMedia('(max-width: 640px)').matches;
73
+
74
  if (isMobile) {
75
  H = Math.round(432 * 1.5);
76
  m = { top: 22, right: 11, bottom: 56, left: 125 };
77
+
78
+ labelFS = 32;
79
+ imgH = 130;
80
+ axisXFS = 32;
81
+ axisYFS = 28;
82
+ yTitleFS = 28;
83
+ tooltipW = 130;
84
+ tooltipTop = 24;
85
+ tooltipRight = 12;
86
  } else {
87
  H = 432;
88
  m = { top: 20, right: 11, bottom: 28, left: 85 };
89
+
90
+ labelFS = 18;
91
+ imgH = 100;
92
+ axisXFS = 16;
93
+ axisYFS = 14;
94
+ yTitleFS = 12;
95
+ tooltipW = 200;
96
+ tooltipTop = 28;
97
+ tooltipRight = 120;
98
  }
99
  }
100
 
101
  let x: ((n: number) => number) | null = null;
102
  let y: ((n: number) => number) | null = null;
103
+
104
+ // Pointer state
105
  let pointerT: number | null = null;
106
 
107
+ // Images
108
  const CROISSANT = "/croissant.png";
109
  const RICE = "/rice.png";
110
  const ANTISPIKE = "/antispike.png";
 
114
  updateResponsive();
115
  window.addEventListener('resize', updateResponsive);
116
 
117
+ // IMPORTANT: glucosedata2.csv must include the two rice columns below.
118
  const text = await (await fetch('/glucosedata2.csv')).text();
119
  rows = csvParse(text, d => ({
120
  time_min: +d['time_min']!,
 
129
 
130
  onDestroy(() => window.removeEventListener('resize', updateResponsive));
131
 
132
+ /** ---------- Accessors that change with the toggle ------------------- */
133
  const topAcc = (r: Row) =>
134
  food === 'croissant' ? r.croissant_mgdl_delta : r.white_rice_mgdl_delta;
135
  const botAcc = (r: Row) =>
136
  food === 'croissant' ? r.croissant_with_anti_spike_mgdl_delta : r.white_rice_with_anti_spike_mgdl_delta;
137
 
138
+ /** ---------- Scales (reactive to data + toggle) --------------------- */
139
  $: if (loaded) {
140
  const xDomain = extent(rows, d => d.time_min) as [number, number];
141
+
142
+ // Use the currently-selected food columns for y extents,
143
+ // so the range adjusts correctly when switching.
144
  const yMin = Math.min(...rows.map(topAcc), ...rows.map(botAcc));
145
  const yMax = Math.max(...rows.map(topAcc), ...rows.map(botAcc));
146
+
147
  x = scaleLinear().domain(xDomain).range([m.left, m.left + (w - m.left - m.right)]);
148
  y = scaleLinear().domain([yMin - 5, yMax + 5]).range([H - m.bottom, m.top]);
149
  }
 
151
  const X = (t: number) => (x ? x(t) : t);
152
  const Y = (v: number) => (y ? y(v) : v);
153
 
154
+ // Paths + filled area (only 0..100 and only above baseline)
155
  const makePath = (acc: (r: Row) => number) =>
156
  d3line<Row>().x(d => X(d.time_min)).y(d => Y(acc(d))).curve(curveMonotoneX);
157
  const makeArea = (acc: (r: Row) => number) =>
 
160
  let pathTop = '', pathBot = '', areaTop = '', areaBot = '';
161
  $: if (loaded) {
162
  const windowed = rows.filter(d => d.time_min >= 0 && d.time_min <= 100);
163
+ // These react to `food` because topAcc/botAcc do.
164
  pathTop = makePath(topAcc)(rows) ?? '';
165
  pathBot = makePath(botAcc)(rows) ?? '';
166
  areaTop = makeArea(topAcc)(windowed) ?? '';
167
  areaBot = makeArea(botAcc)(windowed) ?? '';
168
  }
169
 
170
+ // Interpolation
171
  const rightAt = bisector<Row, number>(d => d.time_min).right;
172
  function yAtTime(acc: (r: Row) => number, t: number) {
173
  const i = Math.max(0, Math.min(rows.length - 2, rightAt(rows, t) - 1));
 
185
  }
186
  function onLeave() { pointerT = null; }
187
 
188
+ // Tooltip values (reactive)
189
  let markerX: number | null = null;
190
  let topVal: number | null = null, botVal: number | null = null;
191
  let topY: number | null = null, botY: number | null = null;
 
221
  $: topMsgText = topVal !== null ? msgTop(topVal) : "";
222
  $: botMsgText = botVal !== null ? msgBot(botVal) : "";
223
 
224
+ // y-axis title ABOVE +60 and to the LEFT of the +60 label
225
  function clamp(n: number, lo: number, hi: number) { return Math.max(lo, Math.min(hi, n)); }
226
  $: yTitleX = xTickCol - 6;
227
  $: yTitleY = (loaded && y)
 
240
  <br> <i>Hint: it's interactive so feel free to hover over. </i>
241
  </div>
242
 
243
+ <!-- ========= IMAGE TOGGLE (croissant / rice) ========================= -->
244
+ <div class="switcher imgs" role="tablist" aria-label="Choose food">
245
+ <button class="imgbtn" class:selected={food==='croissant'}
246
  aria-selected={food==='croissant'}
247
+ on:click={() => (food='croissant')}
248
+ title="Croissant" aria-label="Croissant">
249
+ <img src="/croissant.png" alt="Croissant"/>
250
+ <span>Croissant</span>
251
  </button>
252
+ <button class="imgbtn" class:selected={food==='rice'}
253
  aria-selected={food==='rice'}
254
+ on:click={() => (food='rice')}
255
+ title="Rice" aria-label="Rice">
256
+ <img src="/rice.png" alt="Rice"/>
257
+ <span>Rice</span>
258
  </button>
259
  </div>
260
 
261
+ <!-- =================== TOP: Selected food (alone) =================== -->
262
  <div class="card" style="position:relative; margin-bottom:14px;">
263
  {#if loaded}
264
  <svg viewBox={`0 0 ${w} ${H}`} style="width:100%;height:auto;touch-action:none;display:block"
265
  on:pointermove={onPtr} on:pointerdown={onPtr} on:pointerleave={onLeave}>
266
 
267
+ <!-- Bands inside plot box -->
268
  <rect x={plotX} y={plotY} width={plotW} height={Y(30) - plotY} fill="rgba(223,64,133,0.12)" />
269
  <rect x={plotX} y={Y(30)} width={plotW} height={plotBottom - Y(30)} fill="rgba(40,200,120,0.14)" />
270
 
271
+ <!-- y-axis title -->
272
  <text x={yTitleX} y={yTitleY} fill="#4a4f93" font-size={yTitleFS} text-anchor="end" dominant-baseline="text-before-edge">
273
+ glucose
274
+ <tspan x={yTitleX} dy={yTitleDy} text-anchor="end">(mg/dL)</tspan>
275
  </text>
276
 
277
  <!-- Dynamic callout -->
278
  <text x={m.left + leftInset} y={plotY + calloutTop} fill="#111" font-weight="700" font-size={labelFS}>{topLabel}</text>
279
  <image href={topImg} x={m.left + leftInset} y={plotY + calloutTop + labelFS + 6} height={imgH} preserveAspectRatio="xMinYMin meet" />
280
 
281
+ <!-- Guides -->
282
  <line x1={plotX} x2={plotX + plotW} y1={Y(0)} y2={Y(0)} stroke="#cfd6ff" stroke-width="2" />
283
  <line x1={plotX} x2={plotX + plotW} y1={Y(30)} y2={Y(30)} stroke="#f8b9d0" stroke-dasharray="6 6" />
284
 
285
+ <!-- X labels + inside ticks -->
286
  {#each [{t:0,label:"eating time"},{t:120,label:"2 hours later"}] as xl}
287
  <text x={X(xl.t)} y={(H - m.bottom) + Math.min(m.bottom - 6, Math.round(axisXFS * 0.9))}
288
  text-anchor="middle" fill="#4a4f93" font-size={axisXFS}>{xl.label}</text>
 
291
  <line x1={X(t)} x2={X(t)} y1={plotBottom - xTickLen} y2={plotBottom} stroke="#4a4f93" stroke-width="2" />
292
  {/each}
293
 
294
+ <!-- Y ticks in LEFT WHITE MARGIN -->
295
  <text x={xTickCol} y={Y(60) + yOff60} fill="#4a4f93" font-size={axisYFS} text-anchor="end">+60</text>
296
  <text x={xTickCol} y={Y(30) + yOff30} fill="#4a4f93" font-size={axisYFS} text-anchor="end">+30</text>
297
+ <text x={xTickCol} y={Y(0) + yOff0 } fill="#4a4f93" font-size={axisYFS} text-anchor="end">baseline</text>
298
 
299
+ <!-- Clip above baseline -->
300
  <defs>
301
  <clipPath id="clipTopAboveBaseline">
302
  <rect x={plotX} y={plotY} width={plotW} height={Y(0) - plotY} />
 
335
  <rect x={plotX} y={plotY} width={plotW} height={Y(30) - plotY} fill="rgba(223,64,133,0.12)" />
336
  <rect x={plotX} y={Y(30)} width={plotW} height={plotBottom - Y(30)} fill="rgba(40,200,120,0.14)" />
337
 
338
+ <!-- y-axis title ABOVE +60 and LEFT of tick labels -->
339
  <text x={yTitleX} y={yTitleY} fill="#4a4f93" font-size={yTitleFS} text-anchor="end" dominant-baseline="text-before-edge">
340
+ glucose
341
+ <tspan x={yTitleX} dy={yTitleDy} text-anchor="end">(mg/dL)</tspan>
342
  </text>
343
 
344
  <text x={m.left + leftInset} y={plotY + calloutTop} fill="#111" font-weight="700" font-size={labelFS}>With Anti-Spike</text>
 
347
  <line x1={plotX} x2={plotX + plotW} y1={Y(0)} y2={Y(0)} stroke="#cfd6ff" stroke-width="2" />
348
  <line x1={plotX} x2={plotX + plotW} y1={Y(30)} y2={Y(30)} stroke="#f8b9d0" stroke-dasharray="6 6" />
349
 
350
+ {#each xTicksVals as t}
351
+ <line x1={X(t)} x2={X(t)} y1={plotBottom - xTickLen} y2={plotBottom} stroke="#4a4f93" stroke-width="2" />
352
+ {/each}
353
  {#each [{t:0,label:"eating time"},{t:120,label:"2 hours later"}] as xl}
354
  <text x={X(xl.t)} y={(H - m.bottom) + Math.min(m.bottom - 6, Math.round(axisXFS * 0.9))}
355
  text-anchor="middle" fill="#4a4f93" font-size={axisXFS}>{xl.label}</text>
356
  {/each}
 
 
 
357
 
358
+ <!-- Y ticks in LEFT WHITE MARGIN -->
359
  <text x={xTickCol} y={Y(60) + yOff60} fill="#4a4f93" font-size={axisYFS} text-anchor="end">+60</text>
360
  <text x={xTickCol} y={Y(30) + yOff30} fill="#4a4f93" font-size={axisYFS} text-anchor="end">+30</text>
361
+ <text x={xTickCol} y={Y(0) + yOff0 } fill="#4a4f93" font-size={axisYFS} text-anchor="end">baseline</text>
362
 
363
  <defs>
364
  <clipPath id="clipBotAboveBaseline">