-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathplot-digitizer.html
More file actions
493 lines (440 loc) · 50.6 KB
/
Copy pathplot-digitizer.html
File metadata and controls
493 lines (440 loc) · 50.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
<!DOCTYPE html>
<html lang="en" class="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Plot Digitizer | Extract Data from Graphs | STEMKit</title>
<meta name="title" content="Plot Digitizer | Extract Data from Graphs | STEMKit">
<meta name="description" content="Securely and instantly extract numerical data from literature graphs and plots using precision color sweeping. Runs entirely in your browser.">
<meta name="keywords" content="plot digitizer, extract data from graph, graph reader, image to data, scientific graph reader, color sweep digitizer">
<link rel="canonical" href="https://stemkit.net/plot-digitizer.html">
<meta property="og:url" content="https://stemkit.net/plot-digitizer.html">
<meta property="twitter:url" content="https://stemkit.net/plot-digitizer.html">
<meta property="og:title" content="Plot Digitizer | Extract Data from Graphs">
<meta property="twitter:title" content="Plot Digitizer | Extract Data from Graphs">
<meta property="og:description" content="Instantly extract numerical data from literature graphs via precision color sweeping.">
<meta property="twitter:description" content="Instantly extract numerical data from literature graphs via precision color sweeping.">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebApplication",
"name": "STEMKit Plot Digitizer",
"url": "https://stemkit.net/plot-digitizer.html",
"description": "Securely extract numerical data from graphs.",
"applicationCategory": "UtilityApplication",
"operatingSystem": "All",
"browserRequirements": "Requires JavaScript",
"offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD" },
"featureList": [ "Two-Point Axis Calibration", "Linear and Logarithmic Axes", "Magnifier-Assisted Manual Tracing", "Multiple Datasets", "Local Image Processing", "Python Script Generation", "CSV Export" ]
}
</script>
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"HowTo","name":"How to extract data from a published plot","totalTime":"PT5M","step":[{"@type":"HowToStep","position":1,"name":"Upload the figure","text":"Load a raster image of the plot. Use the highest resolution available, because pixel size sets the floor on achievable precision."},{"@type":"HowToStep","position":2,"name":"Calibrate both axes","text":"Click each calibration button, click that position on the image, and type the value it represents. Place the two marks on each axis as far apart as possible, and tick Log for logarithmic axes."},{"@type":"HowToStep","position":3,"name":"Trace the curve","text":"Select Manual mode, then click individual points or hold and drag along the curve. Use the magnifier to land on the centre of the line. Add one dataset per series."},{"@type":"HowToStep","position":4,"name":"Clean up and export","text":"Erase stray points or undo the last one, then export a CSV, generate a Matplotlib script, or open the interactive preview to verify the extraction."}]}
</script>
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"FAQPage","mainEntity":[{"@type":"Question","name":"How accurate is digitized data?","acceptedAnswer":{"@type":"Answer","text":"With a high-resolution figure, well-separated calibration marks and careful clicking, agreement with the underlying values is typically within about a percent. Accuracy is bounded by the image itself: pixel size, line thickness and axis span set a floor. Treat digitized values as estimates, state in your methods that they were extracted from a figure, and cite the original."}},{"@type":"Question","name":"Where should I put the calibration points?","acceptedAnswer":{"@type":"Answer","text":"As far apart as the axis allows, on positions whose values you know exactly, normally the first and last labelled tick. The error in every extracted point scales inversely with the pixel distance between the two marks, so marks placed near each other magnify every later click."}},{"@type":"Question","name":"Why was my logarithmic axis rejected?","acceptedAnswer":{"@type":"Answer","text":"A logarithmic mapping interpolates in log base 10 space, which is undefined for zero or negative numbers, so both calibration values on that axis must be strictly greater than zero. An axis that crosses zero is not logarithmic."}},{"@type":"Question","name":"How many points should I trace?","acceptedAnswer":{"@type":"Answer","text":"Enough to capture the shape, not the whole line. Place points densely where the curve bends, turns or peaks, and sparsely along straight runs, since a smooth section is reconstructed well from its endpoints. Holding and dragging lays down a trail automatically as you follow the curve."}},{"@type":"Question","name":"Can I trace several curves from one figure?","acceptedAnswer":{"@type":"Answer","text":"Yes. Add a dataset per series, give each a name and colour, and select the right one before tracing. Names carry through to the CSV Dataset column and become legend entries in the generated Matplotlib script."}},{"@type":"Question","name":"Does the order I click in matter?","acceptedAnswer":{"@type":"Answer","text":"Not for the output. Points are sorted by x when exported and drawn, so tracing right-to-left or filling a gap still yields an ordered series. It matters only for Undo, which removes the most recently added point."}},{"@type":"Question","name":"Is my figure uploaded anywhere?","acceptedAnswer":{"@type":"Answer","text":"No. The image is read locally by your browser and drawn to a canvas. Nothing is transmitted, stored or logged, so unpublished or confidential figures are safe to work with."}}]}
</script>
<meta name="author" content="STEMKit">
<meta name="theme-color" content="#4f46e5">
<meta property="og:type" content="website">
<meta property="og:image" content="https://stemkit.net/assets/og-image.webp">
<meta property="twitter:card" content="summary_large_image">
<meta property="twitter:image" content="https://stemkit.net/assets/og-image.webp">
<link rel="icon" type="image/x-icon" href="https://stemkit.net/assets/favicon.ico">
<link rel="icon" type="image/png" sizes="32x32" href="https://stemkit.net/assets/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="https://stemkit.net/assets/favicon-16x16.png">
<link rel="apple-touch-icon" href="https://stemkit.net/assets/apple-touch-icon.png">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/all.min.css">
<link rel="stylesheet" href="src/home.css">
<link rel="stylesheet" href="src/output.css">
<link rel="stylesheet" href="src/stemkit-docs.css">
<link rel="stylesheet" href="src/tools/plot-digitizer.css">
</head>
<body class="bg-slate-50 dark:bg-slate-950 text-slate-900 dark:text-slate-100 min-h-screen flex flex-col font-sans transition-colors duration-300 overflow-x-hidden">
<nav class="sticky top-0 z-50 w-full bg-white/80 dark:bg-slate-950/80 backdrop-blur-md border-b border-slate-200 dark:border-slate-800 transition-colors duration-300 flex-shrink-0">
<div class="max-w-[1600px] mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-16 sm:h-20">
<a href="index.html" class="flex items-center gap-2 hover:opacity-80 transition-opacity">
<div class="bg-indigo-600 text-white p-2 sm:p-2.5 rounded-lg shadow-sm flex items-center justify-center">
<i class="fa-solid fa-flask text-lg sm:text-xl"></i>
</div>
<span class="text-xl sm:text-2xl font-black tracking-tighter text-slate-900 dark:text-white">STEMKit</span>
</a>
<div class="hidden md:flex items-center gap-6 text-sm font-semibold text-slate-600 dark:text-slate-300">
<a href="index.html#data-tools" class="hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors">Data</a>
<a href="index.html#comp-tools" class="hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors">Compute</a>
<a href="index.html#pub-tools" class="hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors">Writing & Citations</a>
<a href="index.html#focus-tools" class="hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors">Focus</a>
<div class="h-5 w-px bg-slate-200 dark:bg-slate-700"></div>
<button class="themeToggle p-2 rounded-full hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors" aria-label="Toggle dark mode">
<i class="fa-solid fa-moon dark:hidden text-lg"></i>
<i class="fa-solid fa-sun hidden dark:block text-yellow-400 text-lg"></i>
</button>
<a href="plot-digitizer.html" class="bg-indigo-600 hover:bg-indigo-700 text-white px-5 py-2.5 rounded-full text-sm font-bold transition-all shadow-md hover:shadow-lg">
<i class="fa-solid fa-crosshairs mr-1"></i> Plot Digitizer
</a>
</div>
<div class="flex items-center gap-2 md:hidden">
<button class="themeToggle p-2 rounded-full hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors text-slate-600 dark:text-slate-300">
<i class="fa-solid fa-moon dark:hidden text-lg"></i>
<i class="fa-solid fa-sun hidden dark:block text-yellow-400 text-lg"></i>
</button>
<button id="mobile-menu-btn" class="text-slate-600 dark:text-slate-300 hover:text-indigo-600 p-2 focus:outline-none">
<i class="fa-solid fa-bars text-2xl" id="menu-icon"></i>
</button>
</div>
</div>
</div>
<div id="mobile-menu" class="hidden md:hidden bg-white/95 dark:bg-slate-900/95 backdrop-blur-md border-b border-slate-200 dark:border-slate-800 absolute w-full shadow-xl">
<div class="px-4 pt-2 pb-6 space-y-2 text-base font-semibold text-slate-600 dark:text-slate-300">
<a href="index.html#data-tools" class="mobile-link block py-3 px-4 rounded-xl hover:bg-slate-50 dark:hover:bg-slate-800 hover:text-indigo-600 transition-colors">Data & Stats</a>
<a href="index.html#comp-tools" class="mobile-link block py-3 px-4 rounded-xl hover:bg-slate-50 dark:hover:bg-slate-800 hover:text-indigo-600 transition-colors">Compute</a>
<a href="index.html#pub-tools" class="mobile-link block py-3 px-4 rounded-xl hover:bg-slate-50 dark:hover:bg-slate-800 hover:text-indigo-600 transition-colors">Writing & Citations</a>
<a href="index.html#focus-tools" class="mobile-link block py-3 px-4 rounded-xl hover:bg-slate-50 dark:hover:bg-slate-800 hover:text-indigo-600 transition-colors">Focus & Flow</a>
</div>
</div>
</nav>
<div class="flex-grow w-full flex flex-col relative" style="height: calc(100vh - 80px);">
<main class="flex-grow max-w-[1600px] mx-auto px-4 py-6 w-full flex flex-col items-center relative h-full">
<section id="uploadZone" class="w-full max-w-4xl flex flex-col items-center justify-center border-2 border-dashed border-indigo-300 dark:border-indigo-700 rounded-3xl bg-indigo-50/50 hover:bg-indigo-50 dark:bg-indigo-900/10 dark:hover:bg-indigo-900/20 cursor-pointer p-16 text-center group transition-colors mt-10">
<input type="file" id="fileInput" accept="image/png, image/jpeg, image/webp" class="hidden">
<div class="w-20 h-20 bg-indigo-100 dark:bg-indigo-900/50 rounded-2xl flex items-center justify-center mb-6 text-indigo-600 dark:text-indigo-400 group-hover:scale-110 transition-transform">
<i class="fa-solid fa-microscope text-4xl"></i>
</div>
<h1 class="text-3xl font-bold mb-3">Initialize digitizer</h1>
<p class="text-slate-500 dark:text-slate-400 mb-2">Upload a reference plot to extract coordinate data and generate automated Matplotlib rendering scripts.</p>
<p class="text-xs font-bold text-slate-400 dark:text-slate-500 mb-6 uppercase tracking-wider">Supported formats: PNG, JPEG, WEBP</p>
<button class="bg-indigo-600 hover:bg-indigo-700 text-white px-8 py-3 rounded-xl font-medium shadow-sm transition-colors">Select graph image</button>
</section>
<div id="pythonModal" class="fixed inset-0 bg-slate-900/90 z-[60] hidden flex-col items-center justify-center p-6 backdrop-blur-sm">
<div class="bg-slate-950 p-6 rounded-3xl max-w-4xl w-full flex flex-col shadow-2xl border border-slate-800">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-black text-white"><i class="fa-brands fa-python text-blue-400 mr-2"></i>Matplotlib rendering script</h3>
<button id="closePythonModal" class="text-slate-400 hover:text-white transition-colors"><i class="fa-solid fa-xmark text-xl"></i></button>
</div>
<div class="relative flex-grow">
<pre><code id="pythonCodeBlock" class="block bg-slate-900 text-emerald-400 p-4 rounded-xl text-sm font-mono overflow-y-auto max-h-[60vh] border border-slate-800 custom-scroll"></code></pre>
<button id="copyPythonBtn" class="absolute top-4 right-4 bg-slate-800 hover:bg-slate-700 text-white px-3 py-1.5 rounded-lg text-xs font-bold transition-colors shadow-lg">Copy code</button>
</div>
<p class="text-xs text-slate-500 mt-4">Save this script in the same directory as your exported CSV to reproduce the plot.</p>
</div>
</div>
<section id="workspace" class="hidden w-full flex-col lg:flex-row gap-4 h-full relative">
<div id="canvasContainer" class="flex-grow bg-slate-200 dark:bg-slate-800 rounded-3xl overflow-auto relative flex items-center justify-center border border-slate-300 dark:border-slate-700 shadow-inner p-4">
<canvas id="plotCanvas" class="max-w-full max-h-full object-contain drop-shadow-2xl"></canvas>
<canvas id="loupeCanvas" width="120" height="120" class="absolute pointer-events-none border-2 border-indigo-500 rounded-full shadow-xl hidden bg-white dark:bg-slate-900 z-50"></canvas>
<div class="absolute top-4 left-4 z-40 flex items-center gap-1 bg-white/90 dark:bg-slate-900/90 backdrop-blur border border-slate-200 dark:border-slate-700 rounded-xl shadow-lg px-1.5 py-1">
<button id="btnZoomOut" title="Zoom out" aria-label="Zoom out" class="w-7 h-7 rounded-lg text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors flex items-center justify-center"><i class="fa-solid fa-minus text-xs"></i></button>
<span id="zoomLevel" class="text-xs font-mono font-bold text-slate-700 dark:text-slate-200 w-12 text-center select-none">100%</span>
<button id="btnZoomIn" title="Zoom in" aria-label="Zoom in" class="w-7 h-7 rounded-lg text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors flex items-center justify-center"><i class="fa-solid fa-plus text-xs"></i></button>
</div>
</div>
<div class="w-full lg:w-[420px] bg-white dark:bg-slate-900 rounded-3xl border border-slate-200 dark:border-slate-800 flex flex-col overflow-y-auto shadow-sm flex-shrink-0 custom-scroll">
<div class="p-6 pb-2 border-b border-slate-100 dark:border-slate-800 flex justify-between items-center bg-slate-50 dark:bg-slate-950 sticky top-0 z-10 rounded-t-3xl">
<h2 class="text-lg font-black tracking-tight">Digitization protocol</h2>
<div class="flex gap-2">
<div class="relative group">
<button id="btnUndo" class="w-8 h-8 rounded-lg bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 text-slate-600 dark:text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 hover:border-indigo-300 transition-colors flex items-center justify-center"><i class="fa-solid fa-rotate-left"></i></button>
<div class="absolute right-0 top-full mt-2 hidden group-hover:block bg-slate-800 text-white text-xs px-2 py-1 rounded shadow-lg whitespace-nowrap z-50">Undo last point</div>
</div>
<label class="relative group cursor-pointer w-8 h-8 rounded-lg bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 flex items-center justify-center hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors">
<input type="checkbox" id="toggleBackground" checked class="hidden">
<i id="eyeIcon" class="fa-solid fa-eye text-slate-600 dark:text-slate-400"></i>
<div class="absolute right-0 top-full mt-2 hidden group-hover:block bg-slate-800 text-white text-xs px-2 py-1 rounded shadow-lg whitespace-nowrap z-50">Toggle background image</div>
</label>
</div>
</div>
<div class="p-4 flex flex-col gap-2">
<div class="border border-slate-200 dark:border-slate-700 rounded-xl overflow-hidden bg-slate-50 dark:bg-slate-800/50">
<button class="accordion-btn w-full px-4 py-3 flex justify-between items-center text-left focus:outline-none" aria-expanded="false" data-target="guideContent">
<span class="text-sm font-bold text-slate-800 dark:text-slate-200"><i class="fa-solid fa-circle-info text-indigo-500 mr-2"></i>How to digitize</span>
<i class="fa-solid fa-chevron-down accordion-icon text-slate-400 text-xs"></i>
</button>
<div id="guideContent" class="accordion-content bg-white dark:bg-slate-900 px-4 pb-3">
<ol class="list-decimal pl-4 text-xs text-slate-600 dark:text-slate-400 space-y-1 mt-2">
<li><strong>Calibrate:</strong> Click "Set X1", then click the known point on the X-axis. Type its value. Repeat for X2, Y1, Y2.</li>
<li><strong>Add datasets:</strong> Use the "+" button to create distinct curves. Pick colors to match the literature plot.</li>
<li><strong>Extract:</strong> Use <em>Manual</em> to click individual points, or hold and drag along the curve to trace it continuously.</li>
<li><strong>Clean up:</strong> Select <em>Erase</em> to brush over and remove erroneous points from the active dataset.</li>
</ol>
</div>
</div>
<div class="border border-slate-200 dark:border-slate-700 rounded-xl overflow-hidden bg-white dark:bg-slate-900 mt-2">
<button class="accordion-btn w-full px-4 py-3 flex justify-between items-center text-left focus:outline-none" aria-expanded="true" data-target="calibContent">
<span class="text-sm font-bold text-slate-800 dark:text-slate-200">1. Spatial calibration</span>
<i class="fa-solid fa-chevron-down accordion-icon text-slate-400 text-xs"></i>
</button>
<div id="calibContent" class="accordion-content expanded px-4 pb-4">
<div class="flex items-center gap-4 mb-3">
<label class="flex items-center gap-1 text-xs font-bold text-slate-500 dark:text-slate-400 cursor-pointer">
<input type="checkbox" id="isLogX" class="w-3 h-3 text-indigo-600 bg-slate-100 dark:bg-slate-800 border-slate-300 dark:border-slate-600 rounded"> Log X
</label>
<label class="flex items-center gap-1 text-xs font-bold text-slate-500 dark:text-slate-400 cursor-pointer">
<input type="checkbox" id="isLogY" class="w-3 h-3 text-indigo-600 bg-slate-100 dark:bg-slate-800 border-slate-300 dark:border-slate-600 rounded"> Log Y
</label>
</div>
<div class="grid grid-cols-2 gap-2">
<button id="btnCalibX1" class="calib-btn bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 hover:border-indigo-400 text-xs py-1.5 rounded-lg font-medium text-slate-700 dark:text-slate-300 transition-colors">Set X1</button>
<input type="number" id="valX1" placeholder="Value" class="border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-950 rounded-lg px-2 text-xs">
<button id="btnCalibX2" class="calib-btn bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 hover:border-indigo-400 text-xs py-1.5 rounded-lg font-medium text-slate-700 dark:text-slate-300 transition-colors">Set X2</button>
<input type="number" id="valX2" placeholder="Value" class="border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-950 rounded-lg px-2 text-xs">
<button id="btnCalibY1" class="calib-btn bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 hover:border-indigo-400 text-xs py-1.5 rounded-lg font-medium text-slate-700 dark:text-slate-300 transition-colors mt-2">Set Y1</button>
<input type="number" id="valY1" placeholder="Value" class="border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-950 rounded-lg px-2 text-xs mt-2">
<button id="btnCalibY2" class="calib-btn bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 hover:border-indigo-400 text-xs py-1.5 rounded-lg font-medium text-slate-700 dark:text-slate-300 transition-colors mt-2">Set Y2</button>
<input type="number" id="valY2" placeholder="Value" class="border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-950 rounded-lg px-2 text-xs mt-2">
</div>
<p id="resolutionNote" class="hidden mt-3 text-[11px] leading-relaxed text-slate-500 dark:text-slate-400 border-t border-slate-200 dark:border-slate-700 pt-2" aria-live="polite"></p>
</div>
</div>
<div class="border border-slate-200 dark:border-slate-700 rounded-xl overflow-hidden bg-white dark:bg-slate-900">
<button class="accordion-btn w-full px-4 py-3 flex justify-between items-center text-left focus:outline-none" aria-expanded="true" data-target="extractContent">
<span class="text-sm font-bold text-slate-800 dark:text-slate-200">2. Data extraction</span>
<i class="fa-solid fa-chevron-down accordion-icon text-slate-400 text-xs"></i>
</button>
<div id="extractContent" class="accordion-content expanded px-4 pb-4">
<div class="flex justify-between items-center mb-2">
<span class="text-xs font-bold text-slate-500 dark:text-slate-400 uppercase">Dataset mapping</span>
<button id="btnAddDataset" class="text-indigo-600 dark:text-indigo-400 hover:text-indigo-800 dark:hover:text-indigo-300 text-xs font-bold bg-indigo-50 dark:bg-indigo-900/30 px-2 py-0.5 rounded transition-colors"><i class="fa-solid fa-plus mr-1"></i> Add series</button>
</div>
<div id="datasetList" class="flex flex-col gap-1.5 max-h-24 overflow-y-auto mb-3 pr-1 custom-scroll"></div>
<div class="grid grid-cols-2 gap-2 mb-2">
<button id="btnManualMode" class="border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 hover:bg-slate-100 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-200 font-bold py-1.5 rounded-lg text-[11px] transition-all duration-200"><i class="fa-solid fa-pen-nib mr-1"></i> Manual</button>
<button id="btnEraseMode" class="border border-red-200 dark:border-red-800 bg-red-50 dark:bg-red-900/20 hover:bg-red-100 dark:hover:bg-red-900/40 text-red-700 dark:text-red-400 font-bold py-1.5 rounded-lg text-[11px] transition-all duration-200"><i class="fa-solid fa-eraser mr-1"></i> Erase</button>
</div>
<div id="eraseControls" class="hidden flex-col gap-1 mt-2 p-2 bg-red-50 dark:bg-red-900/20 rounded-lg border border-red-100 dark:border-red-800">
<div class="flex justify-between text-[10px] font-bold text-red-800 dark:text-red-300">
<span>Eraser radius (px)</span>
<span id="eraseRadiusVal">10</span>
</div>
<input type="range" id="eraseRadius" min="5" max="50" value="10" class="w-full accent-red-600 dark:accent-red-400">
</div>
</div>
</div>
<div class="border border-slate-200 dark:border-slate-700 rounded-xl overflow-hidden bg-indigo-50/30 dark:bg-indigo-900/10">
<button class="accordion-btn w-full px-4 py-3 flex justify-between items-center text-left focus:outline-none" aria-expanded="false" data-target="plotContent">
<span class="text-sm font-bold text-indigo-900 dark:text-indigo-300"><i class="fa-brands fa-python mr-2"></i>3. Plot configurations</span>
<i class="fa-solid fa-chevron-down accordion-icon text-indigo-400 text-xs"></i>
</button>
<div id="plotContent" class="accordion-content px-4 pb-4">
<div class="grid grid-cols-2 gap-2 mb-2">
<div>
<span class="text-[10px] font-bold text-slate-500 uppercase">X-Axis Label</span>
<input type="text" id="pyXLabel" value="Distance (Å)" class="w-full bg-white dark:bg-slate-950 border border-slate-200 dark:border-slate-700 rounded p-1.5 text-xs">
</div>
<div>
<span class="text-[10px] font-bold text-slate-500 uppercase">Y-Axis Label</span>
<input type="text" id="pyYLabel" value="Density (g/cm³)" class="w-full bg-white dark:bg-slate-950 border border-slate-200 dark:border-slate-700 rounded p-1.5 text-xs">
</div>
</div>
<div class="grid grid-cols-2 gap-2 mb-2">
<div>
<span class="text-[10px] font-bold text-slate-500 uppercase">Dimensions (Inches)</span>
<div class="flex items-center gap-1">
<input type="number" id="pyFigWidth" value="8" class="w-full bg-white dark:bg-slate-950 border border-slate-200 dark:border-slate-700 rounded p-1.5 text-xs text-center" placeholder="W">
<span class="text-xs text-slate-400">x</span>
<input type="number" id="pyFigHeight" value="6" class="w-full bg-white dark:bg-slate-950 border border-slate-200 dark:border-slate-700 rounded p-1.5 text-xs text-center" placeholder="H">
</div>
</div>
<div>
<span class="text-[10px] font-bold text-slate-500 uppercase">Plot Layout</span>
<select id="pyPlotLayout" class="w-full bg-white dark:bg-slate-950 border border-slate-200 dark:border-slate-700 rounded p-1.5 text-xs">
<option value="single">Single plot</option>
<option value="subplots">Separate subplots</option>
</select>
</div>
</div>
<div class="grid grid-cols-2 gap-2 mb-3">
<label class="flex items-center gap-2 text-xs font-bold text-slate-600 dark:text-slate-400 cursor-pointer">
<input type="checkbox" id="pyShowGrid" class="w-3 h-3 text-indigo-600 bg-white dark:bg-slate-800 border-slate-300 dark:border-slate-600 rounded">
Show grid lines
</label>
<label class="flex items-center gap-2 text-xs font-bold text-slate-600 dark:text-slate-400 cursor-pointer">
<input type="checkbox" id="pyBgTrans" class="w-3 h-3 text-indigo-600 bg-white dark:bg-slate-800 border-slate-300 dark:border-slate-600 rounded">
Transparent bg
</label>
</div>
<div class="flex gap-2 mt-4">
<button id="btnPreviewPlot" class="flex-1 bg-white dark:bg-slate-800 border border-indigo-200 dark:border-indigo-700 hover:bg-indigo-50 dark:hover:bg-slate-700 text-indigo-600 dark:text-indigo-400 font-bold py-2 rounded-lg text-xs transition-transform transform hover:-translate-y-0.5 shadow-sm hover:shadow-md">Live Preview</button>
<button id="btnGeneratePython" class="flex-1 bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-2 rounded-lg text-xs transition-transform transform hover:-translate-y-0.5 shadow-sm hover:shadow-md"><i class="fa-brands fa-python mr-1"></i>Script</button>
</div>
</div>
</div>
</div>
<div class="mt-auto p-4 bg-slate-50 dark:bg-slate-950 border-t border-slate-100 dark:border-slate-800 flex flex-col gap-3 rounded-b-3xl">
<div>
<span class="text-[10px] font-bold text-slate-500 uppercase">Output filename</span>
<div class="flex items-center mt-1 relative">
<input type="text" id="csvFilename" value="extracted_data" class="w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-lg p-2.5 text-sm pr-12 focus:ring-2 focus:ring-indigo-500 outline-none transition-shadow">
<span class="absolute right-3 text-slate-400 text-sm font-mono">.csv</span>
</div>
</div>
<button id="exportCsvBtn" class="w-full bg-slate-800 hover:bg-slate-700 dark:bg-indigo-600 dark:hover:bg-indigo-500 text-white font-bold py-3 rounded-xl transition-all duration-300 transform hover:-translate-y-1 hover:shadow-xl active:scale-95 shadow-md text-sm flex justify-center items-center">
<i class="fa-solid fa-file-csv mr-2 text-lg"></i> Download dataset
</button>
</div>
</div>
</section>
</main>
<!-- ================= HOW TO USE ================= -->
<section class="stk-section" id="how-to-use" aria-label="How to use this tool">
<div class="stk-wrap">
<span class="stk-chip"><i class="fa-solid fa-crosshairs"></i> How-To Guide</span>
<h2 class="stk-h2">How to extract data from a published plot</h2>
<p class="stk-lead">Upload a figure, tell the tool where two known values sit on each axis, then trace the curve. You get a CSV of real coordinates plus a ready-to-run Matplotlib script.</p>
<div class="stk-grid stk-grid-2">
<div class="stk-step"><span class="stk-num">1</span><div><h3>Upload the figure</h3><p>Drop in a PNG, JPG or any raster image of the plot. Use the highest resolution you can find, a screenshot of a PDF at full zoom beats a thumbnail, because every pixel of the original is a pixel of precision you keep.</p></div></div>
<div class="stk-step"><span class="stk-num">2</span><div><h3>Calibrate both axes</h3><p>Click <strong>X1</strong>, then click that position on the image, and type the value it represents. Repeat for <strong>X2</strong>, <strong>Y1</strong> and <strong>Y2</strong>. Pick two points that are <em>far apart</em> on each axis, usually the first and last labelled gridline. Tick <strong>Log</strong> if an axis is logarithmic.</p></div></div>
<div class="stk-step"><span class="stk-num">3</span><div><h3>Trace the curve</h3><p>Select <strong>Manual</strong>, then either click individual points or hold and drag along the curve to lay down a trail. The magnifier follows your cursor so you can land on the line rather than near it. Add a dataset for each series and give it a name and colour.</p></div></div>
<div class="stk-step"><span class="stk-num">4</span><div><h3>Clean up and export</h3><p>Use <strong>Erase</strong> to brush away stray points, or <strong>Undo</strong> to drop the last one. Then export the CSV, generate a Matplotlib script, or open the interactive preview to check the extracted series before you use it.</p></div></div>
</div>
<div class="stk-note" style="margin-top:1.4rem;"><i class="fa-solid fa-circle-info" style="margin-top:.15rem;"></i><div>Toggle the background image off at any point to see only your extracted points. It is the quickest way to spot a stray click or a gap in a trace.</div></div>
</div>
</section>
<!-- ================= HOW IT WORKS ================= -->
<section class="stk-section" id="how-it-works" aria-label="How it works">
<div class="stk-wrap">
<h2 class="stk-h2">What the digitizer does, and what limits it</h2>
<div class="stk-grid stk-grid-2">
<div class="stk-card"><h3 style="font-weight:700;margin-bottom:.4rem;">Two-point affine calibration</h3><p style="font-size:.88rem;color:#64748b;line-height:1.6;">Each axis is mapped from pixels to data by a straight line through your two calibration marks. On a log axis the same interpolation is done in log<sub>10</sub> space and then exponentiated, which is why both log values must be strictly positive.</p></div>
<div class="stk-card"><h3 style="font-weight:700;margin-bottom:.4rem;">You place every point</h3><p style="font-size:.88rem;color:#64748b;line-height:1.6;">Coordinates come from where you click, so every point in your dataset is one you saw on the curve. Where a series doubles back on itself or crosses another, you decide which branch to follow rather than leaving it to a threshold.</p></div>
<div class="stk-card"><h3 style="font-weight:700;margin-bottom:.4rem;">Exports stay consistent</h3><p style="font-size:.88rem;color:#64748b;line-height:1.6;">The CSV, the Matplotlib script and the interactive preview all sort points by <em>x</em> and use the same values, so the curve you see on screen is the curve you get in the file, even if you traced right-to-left or patched a gap in the middle.</p></div>
<div class="stk-card"><h3 style="font-weight:700;margin-bottom:.4rem;">Everything stays local</h3><p style="font-size:.88rem;color:#64748b;line-height:1.6;">The image never leaves your machine. It is read with the browser's <code>FileReader</code> and drawn to a canvas, nothing is uploaded, so unpublished figures and confidential data are safe to work with.</p></div>
</div>
<div class="stk-note" style="margin-top:1rem;"><i class="fa-solid fa-triangle-exclamation" style="margin-top:.15rem;"></i><div>Digitized data is an <em>estimate</em> of the original, never a substitute for it. Where the authors have published their numbers, or you can request them, use those instead. If you do digitize, say so in your methods and cite the source figure.</div></div>
</div>
</section>
<!-- ================= METHOD & REFERENCES ================= -->
<section class="stk-section" id="method" aria-label="Method">
<div class="stk-wrap">
<span class="stk-chip"><i class="fa-solid fa-square-root-variable"></i> Method</span>
<h2 class="stk-h2">The mapping, and where the error comes from</h2>
<p class="stk-lead">Both axes use the same two-point interpolation. Understanding it tells you exactly how to keep your extraction accurate.</p>
<div class="stk-math">
<div class="stk-eqn-label">Linear axis</div>
<p style="font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.9rem;">value = v<sub>1</sub> + (p − p<sub>1</sub>) × (v<sub>2</sub> − v<sub>1</sub>) / (p<sub>2</sub> − p<sub>1</sub>)</p>
<div class="stk-eqn-label" style="margin-top:.9rem;">Logarithmic axis</div>
<p style="font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.9rem;">value = 10<sup> log<sub>10</sub>v<sub>1</sub> + (p − p<sub>1</sub>)(log<sub>10</sub>v<sub>2</sub> − log<sub>10</sub>v<sub>1</sub>) / (p<sub>2</sub> − p<sub>1</sub>)</sup></p>
<p class="stk-where"><strong>where</strong> <em>p</em> is the pixel coordinate of your point, <em>p</em><sub>1</sub> / <em>p</em><sub>2</sub> are the pixel positions of your two calibration marks, and <em>v</em><sub>1</sub> / <em>v</em><sub>2</sub> are the data values you typed for them.</p>
</div>
<h3 style="font-weight:700;margin:1.6rem 0 .6rem;">Where the error actually comes from</h3>
<div class="stk-grid stk-grid-3">
<div class="stk-card"><h3 style="font-weight:700;margin-bottom:.4rem;">Calibration span</h3><p style="font-size:.88rem;color:#64748b;line-height:1.6;">Error scales with 1 / (p<sub>2</sub>−p<sub>1</sub>). Two marks a few pixels apart amplify every subsequent click; marks at opposite ends of the axis divide the error down. This is the single biggest lever you control.</p></div>
<div class="stk-card"><h3 style="font-weight:700;margin-bottom:.4rem;">Image resolution</h3><p style="font-size:.88rem;color:#64748b;line-height:1.6;">One screen pixel is the floor on precision. A 400 px-wide figure spanning 0–100 gives you no better than ±0.25 per pixel, before any human error. Start from the largest version of the figure available.</p></div>
<div class="stk-card"><h3 style="font-weight:700;margin-bottom:.4rem;">Line thickness</h3><p style="font-size:.88rem;color:#64748b;line-height:1.6;">A curve drawn 3 px thick is ambiguous by ±1.5 px wherever you click on it. Aim consistently for the centre of the stroke; the magnifier exists to make that possible.</p></div>
</div>
<div class="stk-note" style="margin-top:1.2rem;"><i class="fa-solid fa-lightbulb" style="margin-top:.15rem;"></i><div><strong>Sanity check your calibration.</strong> After calibrating, hover over a gridline whose value you already know and see whether the readout matches. If it does not, one of your four marks or values is wrong, and every point you trace afterwards will inherit that error.</div></div>
</div>
</section>
<!-- ================= FAQ ================= -->
<section class="stk-section" id="faq" aria-label="Frequently asked questions">
<div class="stk-wrap">
<h2 class="stk-h2">Frequently asked questions</h2>
<div style="margin-top:1rem;">
<details class="stk-faq"><summary>How accurate is digitized data?</summary><div>With a high-resolution figure, well-separated calibration marks and careful clicking, agreement with the underlying values is typically within a percent or so, published reliability studies of digitizing tools report high agreement between independent extractors. But accuracy is bounded by the image itself: pixel size, line thickness and axis span all set a floor you cannot beat. Treat digitized values as good estimates, state in your methods that they were extracted from a figure, and cite the original.</div></details>
<details class="stk-faq"><summary>Where should I put the calibration points?</summary><div>As far apart as the axis allows, on positions whose values you know exactly, normally the first and last labelled tick. The error in every extracted point scales inversely with the pixel distance between your two marks, so two marks near each other magnify every later click. Placing both on the same pixel is rejected outright, because the scale would be undefined.</div></details>
<details class="stk-faq"><summary>Why was my log axis rejected?</summary><div>A logarithmic mapping interpolates in log<sub>10</sub> space, which is undefined for zero or negative numbers. Both calibration values on that axis must therefore be strictly greater than zero. If your axis genuinely crosses zero it is not logarithmic, it is probably linear, or a symlog scale, which this two-point mapping does not model.</div></details>
<details class="stk-faq"><summary>How many points should I trace?</summary><div>Enough to capture the shape, not the whole line. Place points densely where the curve bends, turns or peaks, and sparsely along straight runs, a smooth section is reconstructed perfectly well from its endpoints. Holding and dragging lays down a trail automatically as you follow the curve, which is usually the fastest way to cover a long sweep.</div></details>
<details class="stk-faq"><summary>Can I trace several curves from one figure?</summary><div>Yes. Add a dataset per series with <strong>+ Add</strong>, give each a name and colour, and make sure the right one is selected before you trace, points always go to the active dataset. Names are carried through to the CSV's <code>Dataset</code> column and become the legend entries in the generated Matplotlib script.</div></details>
<details class="stk-faq"><summary>Does the order I click in matter?</summary><div>Not for the output. Points are sorted by <em>x</em> when exported and when drawn, so tracing right-to-left or going back to fill a gap still yields a properly ordered series. It does matter for <strong>Undo</strong>, which removes the most recently added point rather than the right-most one.</div></details>
<details class="stk-faq"><summary>Why does my CSV show values like 2.3e-5?</summary><div>Because the value is genuinely that small. Values are written with significant digits rather than a fixed number of decimals, so small magnitudes survive the round trip, a fixed four-decimal format would have written that number as <code>0.0000</code> and destroyed it. Very large and very small magnitudes use exponential notation, which <code>pandas.read_csv</code> and every spreadsheet read without special handling.</div></details>
<details class="stk-faq"><summary>What do the generated Python script and preview give me?</summary><div>The script loads your exported CSV with pandas and rebuilds the figure in Matplotlib (one axes with all series, or stacked subplots) carrying over your labels, colours, log scales, grid and figure size. The preview does the same thing instantly with Plotly in a new tab, so you can check the extraction before writing any files. Save the CSV first: the script reads it by the filename you chose.</div></details>
<details class="stk-faq"><summary>Is my figure uploaded anywhere?</summary><div>No. The image is read locally by your browser and drawn to a canvas. Nothing is transmitted, stored or logged, so it is safe to digitize unpublished figures, confidential results or anything under embargo.</div></details>
</div>
</div>
</section>
<footer class="mt-auto border-t border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-950 py-12">
<div class="max-w-7xl mx-auto px-6">
<div class="stk-dir mb-12">
<div>
<h4><i class="fa-solid fa-chart-pie text-indigo-500 text-xs"></i> Data & Stats</h4>
<ul>
<li><a href="plot-digitizer.html">Plot Digitizer</a></li>
<li><a href="data-cleaner.html">Data Cleaner</a></li>
<li><a href="stats-calculator.html">Stat Calculator</a></li>
<li><a href="error-bar-generator.html">Error Bar Gen</a></li>
<li><a href="outlier-detector.html">Outlier Detector</a></li>
<li><a href="curve-fitter.html">Curve Fitter</a></li>
<li><a href="plot-builder.html">Plot Builder</a></li>
<li><a href="xvg-visualizer.html">XVG Visualizer</a></li>
</ul>
</div>
<div>
<h4><i class="fa-solid fa-microchip text-orange-500 text-xs"></i> Compute</h4>
<ul>
<li><a href="structure-inspector.html">3D Inspector</a></li>
<li><a href="coordinate-manipulator.html">Coordinate Manipulator</a></li>
<li><a href="scientific-converter.html">Energy Conversions</a></li>
<li><a href="script-generator.html">HPC Script Gen</a></li>
</ul>
</div>
<div>
<h4><i class="fa-solid fa-book-open text-teal-500 text-xs"></i> Writing & Citations</h4>
<ul>
<li><a href="latex-formatter.html">Equation Formatter</a></li>
<li><a href="latex-tables.html">Visual LaTeX Tables</a></li>
<li><a href="doi-fetcher.html">DOI to BibTeX</a></li>
<li><a href="bibtex-deduplicator.html">BibTeX Deduplicator</a></li>
<li><a href="bibtex-sanitizer.html">BibTeX Sanitizer</a></li>
<li><a href="journal-abbreviator.html">Journal Abbrev</a></li>
</ul>
</div>
<div>
<h4><i class="fa-solid fa-headphones text-fuchsia-500 text-xs"></i> Focus & Flow</h4>
<ul>
<li><a href="pomodoro.html">Ambient Pomodoro</a></li>
<li><a href="decision.html">Decision Matrix</a></li>
<li><a href="sandbox.html">Kinetic Sandbox</a></li>
</ul>
</div>
</div>
<div class="border-t border-slate-200 dark:border-slate-800 pt-8 flex flex-col md:flex-row items-center justify-between text-slate-500 dark:text-slate-400 gap-4">
<div class="flex items-center gap-2">
<i class="fa-solid fa-flask text-xl text-slate-300 dark:text-slate-700"></i>
<span class="font-bold text-slate-700 dark:text-slate-300">STEMKit</span>
</div>
<div class="text-center text-sm font-medium">© 2026 STEMKit. Built by <a href="https://github.com/LD-Shell" target="_blank" rel="noopener" class="font-semibold hover:text-indigo-500 transition-colors">Olanrewaju M. Daramola</a>.</div>
<div class="flex gap-6 text-sm font-medium">
<a href="privacy.html#privacy" class="hover:text-indigo-500 transition-colors">Privacy Policy</a>
<a href="privacy.html#terms" class="hover:text-indigo-500 transition-colors">Terms of Service</a>
</div>
</div>
</div>
</footer>
</div>
<script>
// 1. Theme toggle logic (Mobile & Desktop)
document.querySelectorAll('.themeToggle').forEach(btn => {
btn.addEventListener('click', () => {
document.documentElement.classList.toggle('dark');
localStorage.theme = document.documentElement.classList.contains('dark') ? 'dark' : 'light';
});
});
// 2. Mobile menu logic
const mobileMenuBtn = document.getElementById('mobile-menu-btn');
const mobileMenu = document.getElementById('mobile-menu');
const menuIcon = document.getElementById('menu-icon');
if(mobileMenuBtn) {
mobileMenuBtn.addEventListener('click', () => {
mobileMenu.classList.toggle('hidden');
if(mobileMenu.classList.contains('hidden')) {
menuIcon.classList.remove('fa-xmark');
menuIcon.classList.add('fa-bars');
} else {
menuIcon.classList.remove('fa-bars');
menuIcon.classList.add('fa-xmark');
}
});
document.querySelectorAll('.mobile-link').forEach(link => {
link.addEventListener('click', () => {
mobileMenu.classList.add('hidden');
menuIcon.classList.remove('fa-xmark');
menuIcon.classList.add('fa-bars');
});
});
}
</script>
<script type="module" src="js/plot-digitizer.js"></script>
<div id="toastContainer" class="fixed bottom-6 right-6 z-50 flex flex-col gap-3"></div>
</body>
</html>