geotechcli 0.4.4 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,850 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { createRequire } from 'node:module';
4
+ import { tmpdir } from 'node:os';
5
+ import { dirname, join, resolve } from 'node:path';
6
+ import { pathToFileURL } from 'node:url';
7
+ const require = createRequire(import.meta.url);
8
+ function slugify(value) {
9
+ return value
10
+ .toLowerCase()
11
+ .replace(/[^a-z0-9]+/g, '-')
12
+ .replace(/^-+|-+$/g, '')
13
+ .slice(0, 80) || 'plot';
14
+ }
15
+ function serializeChart(chart) {
16
+ if (chart.kind === 'xy') {
17
+ return {
18
+ id: chart.id,
19
+ title: chart.title,
20
+ note: chart.note ?? null,
21
+ kind: 'xy',
22
+ xLabel: chart.xLabel,
23
+ yLabel: chart.yLabel,
24
+ xScale: chart.xScale ?? 'linear',
25
+ invertY: Boolean(chart.invertY),
26
+ xDomain: chart.xDomain ?? null,
27
+ yDomain: chart.yDomain ?? null,
28
+ series: (chart.xySeries ?? []).map((series) => ({
29
+ label: series.label,
30
+ style: series.style ?? 'line',
31
+ points: series.points
32
+ .filter((point) => Number.isFinite(point.x) && Number.isFinite(point.y))
33
+ .map((point) => ({ x: point.x, y: point.y })),
34
+ })),
35
+ };
36
+ }
37
+ return {
38
+ id: chart.id,
39
+ title: chart.title,
40
+ note: chart.note ?? null,
41
+ kind: 'series',
42
+ xLabel: chart.xLabel,
43
+ yLabel: chart.yLabel,
44
+ xScale: chart.xScale ?? 'linear',
45
+ invertY: Boolean(chart.invertY),
46
+ xDomain: chart.xDomain ?? null,
47
+ yDomain: chart.yDomain ?? null,
48
+ xValues: (chart.xValues ?? []).filter((value) => Number.isFinite(value)),
49
+ series: (chart.series ?? []).map((values, index) => ({
50
+ label: chart.labels?.[index] ?? `Series ${index + 1}`,
51
+ values: values.filter((value) => Number.isFinite(value)),
52
+ })),
53
+ };
54
+ }
55
+ function serializePayload(source, options) {
56
+ const charts = source.charts.map(serializeChart);
57
+ return {
58
+ headline: options.headline ?? 'Interactive Plot Viewer',
59
+ sourceLabel: options.sourceLabel ?? source.sourceName,
60
+ sourceType: source.sourceType.toUpperCase(),
61
+ activeChartId: options.focusChartId && charts.some((chart) => chart.id === options.focusChartId)
62
+ ? options.focusChartId
63
+ : charts[0]?.id ?? null,
64
+ charts,
65
+ };
66
+ }
67
+ function serializeForHtml(payload) {
68
+ return JSON.stringify(payload)
69
+ .replace(/</g, '\\u003c')
70
+ .replace(/>/g, '\\u003e')
71
+ .replace(/&/g, '\\u0026');
72
+ }
73
+ function getEChartsBundle() {
74
+ const path = require.resolve('echarts/dist/echarts.min.js');
75
+ return readFileSync(path, 'utf-8');
76
+ }
77
+ function getOutputPath(sourceName, outputPath) {
78
+ if (outputPath) {
79
+ return resolve(outputPath);
80
+ }
81
+ const tempDir = mkdtempSync(join(tmpdir(), 'geotechcli-plot-'));
82
+ return join(tempDir, `${slugify(sourceName)}.html`);
83
+ }
84
+ function openInBrowser(filePath) {
85
+ if (process.env.GEOTECHCLI_PLOT_NO_OPEN === '1') {
86
+ return false;
87
+ }
88
+ const target = pathToFileURL(filePath).href;
89
+ try {
90
+ if (process.platform === 'win32') {
91
+ const child = spawn('cmd', ['/c', 'start', '', target], {
92
+ detached: true,
93
+ stdio: 'ignore',
94
+ });
95
+ child.unref();
96
+ return true;
97
+ }
98
+ if (process.platform === 'darwin') {
99
+ const child = spawn('open', [target], {
100
+ detached: true,
101
+ stdio: 'ignore',
102
+ });
103
+ child.unref();
104
+ return true;
105
+ }
106
+ const child = spawn('xdg-open', [target], {
107
+ detached: true,
108
+ stdio: 'ignore',
109
+ });
110
+ child.unref();
111
+ return true;
112
+ }
113
+ catch {
114
+ return false;
115
+ }
116
+ }
117
+ function buildHtml(payload) {
118
+ const embeddedPayload = serializeForHtml(payload);
119
+ const echartsSource = getEChartsBundle();
120
+ return `<!doctype html>
121
+ <html lang="en">
122
+ <head>
123
+ <meta charset="utf-8" />
124
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
125
+ <title>${payload.headline}</title>
126
+ <style>
127
+ :root {
128
+ --bg: #07111d;
129
+ --bg-panel: rgba(10, 17, 29, 0.78);
130
+ --bg-panel-strong: rgba(8, 14, 24, 0.96);
131
+ --bg-soft: rgba(15, 23, 42, 0.72);
132
+ --line: rgba(148, 163, 184, 0.16);
133
+ --line-strong: rgba(125, 211, 252, 0.22);
134
+ --text: #e6edf8;
135
+ --muted: #93a4bb;
136
+ --accent: #2dd4bf;
137
+ --accent-alt: #60a5fa;
138
+ --accent-warm: #f59e0b;
139
+ --shadow: 0 24px 80px rgba(2, 8, 23, 0.45);
140
+ }
141
+
142
+ * {
143
+ box-sizing: border-box;
144
+ }
145
+
146
+ body {
147
+ margin: 0;
148
+ min-height: 100vh;
149
+ background:
150
+ radial-gradient(circle at top right, rgba(45, 212, 191, 0.12), transparent 22rem),
151
+ radial-gradient(circle at top left, rgba(96, 165, 250, 0.12), transparent 20rem),
152
+ linear-gradient(180deg, #081220 0%, #040a12 100%);
153
+ color: var(--text);
154
+ font-family: "Aptos", "Segoe UI Variable", "Segoe UI", sans-serif;
155
+ }
156
+
157
+ .shell {
158
+ max-width: 1480px;
159
+ margin: 0 auto;
160
+ padding: 28px;
161
+ display: flex;
162
+ flex-direction: column;
163
+ gap: 22px;
164
+ min-height: 100vh;
165
+ }
166
+
167
+ .masthead {
168
+ display: grid;
169
+ gap: 16px;
170
+ padding: 22px 24px;
171
+ border: 1px solid var(--line-strong);
172
+ border-radius: 24px;
173
+ background: linear-gradient(135deg, rgba(12, 19, 31, 0.9), rgba(7, 12, 21, 0.78));
174
+ box-shadow: var(--shadow);
175
+ animation: rise 420ms ease-out both;
176
+ }
177
+
178
+ .eyebrow {
179
+ display: inline-flex;
180
+ align-items: center;
181
+ gap: 10px;
182
+ color: var(--muted);
183
+ text-transform: uppercase;
184
+ letter-spacing: 0.14em;
185
+ font-size: 11px;
186
+ font-weight: 700;
187
+ }
188
+
189
+ .eyebrow::before {
190
+ content: "";
191
+ width: 42px;
192
+ height: 1px;
193
+ background: linear-gradient(90deg, transparent, var(--accent));
194
+ }
195
+
196
+ .headline {
197
+ margin: 0;
198
+ font-size: clamp(32px, 4vw, 54px);
199
+ line-height: 1.02;
200
+ letter-spacing: -0.04em;
201
+ font-weight: 650;
202
+ }
203
+
204
+ .subhead {
205
+ margin: 0;
206
+ color: var(--muted);
207
+ max-width: 72ch;
208
+ font-size: 15px;
209
+ line-height: 1.7;
210
+ }
211
+
212
+ .summary-strip {
213
+ display: grid;
214
+ gap: 12px;
215
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
216
+ }
217
+
218
+ .summary-card {
219
+ padding: 14px 16px;
220
+ border-radius: 18px;
221
+ border: 1px solid var(--line);
222
+ background: rgba(255, 255, 255, 0.02);
223
+ }
224
+
225
+ .summary-card span {
226
+ display: block;
227
+ color: var(--muted);
228
+ font-size: 11px;
229
+ letter-spacing: 0.12em;
230
+ text-transform: uppercase;
231
+ margin-bottom: 8px;
232
+ font-weight: 700;
233
+ }
234
+
235
+ .summary-card strong {
236
+ display: block;
237
+ font-size: 16px;
238
+ color: var(--text);
239
+ font-weight: 620;
240
+ word-break: break-word;
241
+ }
242
+
243
+ .workspace {
244
+ display: grid;
245
+ grid-template-columns: minmax(260px, 320px) minmax(0, 1fr);
246
+ gap: 20px;
247
+ min-height: 0;
248
+ flex: 1;
249
+ }
250
+
251
+ .sidebar,
252
+ .canvas {
253
+ border-radius: 24px;
254
+ border: 1px solid var(--line);
255
+ background: var(--bg-panel);
256
+ box-shadow: var(--shadow);
257
+ backdrop-filter: blur(18px);
258
+ }
259
+
260
+ .sidebar {
261
+ padding: 22px 18px;
262
+ display: flex;
263
+ flex-direction: column;
264
+ gap: 18px;
265
+ animation: rise 520ms ease-out both;
266
+ }
267
+
268
+ .section-label {
269
+ margin: 0;
270
+ color: var(--muted);
271
+ text-transform: uppercase;
272
+ letter-spacing: 0.12em;
273
+ font-size: 11px;
274
+ font-weight: 700;
275
+ }
276
+
277
+ .chart-list {
278
+ display: flex;
279
+ flex-direction: column;
280
+ gap: 10px;
281
+ }
282
+
283
+ .chart-button {
284
+ padding: 14px 15px;
285
+ border-radius: 18px;
286
+ border: 1px solid var(--line);
287
+ background: rgba(15, 23, 42, 0.46);
288
+ color: var(--text);
289
+ text-align: left;
290
+ cursor: pointer;
291
+ transition: 180ms ease;
292
+ }
293
+
294
+ .chart-button:hover {
295
+ transform: translateY(-1px);
296
+ border-color: rgba(96, 165, 250, 0.35);
297
+ background: rgba(15, 23, 42, 0.72);
298
+ }
299
+
300
+ .chart-button.active {
301
+ border-color: rgba(45, 212, 191, 0.45);
302
+ background:
303
+ linear-gradient(135deg, rgba(45, 212, 191, 0.12), rgba(96, 165, 250, 0.1)),
304
+ rgba(12, 18, 29, 0.92);
305
+ box-shadow: inset 0 0 0 1px rgba(45, 212, 191, 0.1);
306
+ }
307
+
308
+ .chart-button strong {
309
+ display: block;
310
+ font-size: 14px;
311
+ line-height: 1.45;
312
+ margin-bottom: 5px;
313
+ font-weight: 620;
314
+ }
315
+
316
+ .chart-button span {
317
+ display: block;
318
+ color: var(--muted);
319
+ font-size: 12px;
320
+ line-height: 1.55;
321
+ }
322
+
323
+ .hint {
324
+ color: var(--muted);
325
+ font-size: 13px;
326
+ line-height: 1.7;
327
+ margin: 0;
328
+ }
329
+
330
+ .canvas {
331
+ padding: 18px;
332
+ display: grid;
333
+ gap: 14px;
334
+ animation: rise 620ms ease-out both;
335
+ }
336
+
337
+ .canvas-header {
338
+ display: flex;
339
+ gap: 16px;
340
+ align-items: flex-start;
341
+ justify-content: space-between;
342
+ padding: 8px 4px 0;
343
+ }
344
+
345
+ .canvas-header h2 {
346
+ margin: 0 0 6px;
347
+ font-size: clamp(22px, 2.6vw, 34px);
348
+ line-height: 1.08;
349
+ letter-spacing: -0.03em;
350
+ font-weight: 640;
351
+ }
352
+
353
+ .canvas-header p {
354
+ margin: 0;
355
+ color: var(--muted);
356
+ font-size: 14px;
357
+ line-height: 1.7;
358
+ max-width: 64ch;
359
+ }
360
+
361
+ .status-chip {
362
+ flex-shrink: 0;
363
+ display: inline-flex;
364
+ align-items: center;
365
+ gap: 8px;
366
+ padding: 10px 12px;
367
+ border-radius: 999px;
368
+ border: 1px solid rgba(45, 212, 191, 0.28);
369
+ background: rgba(45, 212, 191, 0.08);
370
+ color: var(--accent);
371
+ font-size: 12px;
372
+ font-weight: 700;
373
+ letter-spacing: 0.08em;
374
+ text-transform: uppercase;
375
+ }
376
+
377
+ .status-chip::before {
378
+ content: "";
379
+ width: 8px;
380
+ height: 8px;
381
+ border-radius: 999px;
382
+ background: var(--accent);
383
+ box-shadow: 0 0 16px rgba(45, 212, 191, 0.55);
384
+ }
385
+
386
+ #chartHost {
387
+ min-height: 540px;
388
+ height: 62vh;
389
+ border-radius: 24px;
390
+ overflow: hidden;
391
+ border: 1px solid rgba(96, 165, 250, 0.16);
392
+ background:
393
+ linear-gradient(180deg, rgba(8, 13, 22, 0.94), rgba(6, 11, 18, 0.88)),
394
+ radial-gradient(circle at top, rgba(96, 165, 250, 0.1), transparent 18rem);
395
+ }
396
+
397
+ .details-grid {
398
+ display: grid;
399
+ grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
400
+ gap: 12px;
401
+ }
402
+
403
+ .detail-card {
404
+ padding: 14px 16px;
405
+ border-radius: 18px;
406
+ border: 1px solid var(--line);
407
+ background: rgba(255, 255, 255, 0.02);
408
+ }
409
+
410
+ .detail-card span {
411
+ display: block;
412
+ color: var(--muted);
413
+ font-size: 11px;
414
+ letter-spacing: 0.12em;
415
+ text-transform: uppercase;
416
+ margin-bottom: 8px;
417
+ font-weight: 700;
418
+ }
419
+
420
+ .detail-card strong {
421
+ display: block;
422
+ font-size: 14px;
423
+ line-height: 1.5;
424
+ font-weight: 620;
425
+ color: var(--text);
426
+ word-break: break-word;
427
+ }
428
+
429
+ .note-panel {
430
+ padding: 16px 18px;
431
+ border-radius: 20px;
432
+ border: 1px solid rgba(245, 158, 11, 0.18);
433
+ background: rgba(245, 158, 11, 0.06);
434
+ color: #f9d787;
435
+ font-size: 13px;
436
+ line-height: 1.75;
437
+ }
438
+
439
+ @media (max-width: 1080px) {
440
+ .workspace {
441
+ grid-template-columns: 1fr;
442
+ }
443
+
444
+ #chartHost {
445
+ height: 54vh;
446
+ }
447
+ }
448
+
449
+ @media (max-width: 720px) {
450
+ .shell {
451
+ padding: 14px;
452
+ }
453
+
454
+ .masthead,
455
+ .sidebar,
456
+ .canvas {
457
+ border-radius: 20px;
458
+ }
459
+
460
+ .canvas-header {
461
+ flex-direction: column;
462
+ }
463
+
464
+ #chartHost {
465
+ min-height: 360px;
466
+ height: 48vh;
467
+ }
468
+ }
469
+
470
+ @keyframes rise {
471
+ from {
472
+ opacity: 0;
473
+ transform: translateY(10px);
474
+ }
475
+ to {
476
+ opacity: 1;
477
+ transform: translateY(0);
478
+ }
479
+ }
480
+ </style>
481
+ </head>
482
+ <body>
483
+ <div class="shell">
484
+ <section class="masthead">
485
+ <div class="eyebrow">geotechCLI plot studio</div>
486
+ <h1 class="headline">${payload.headline}</h1>
487
+ <p class="subhead">Interactive engineering plots with zoom, pan, image export, and chart-to-chart switching. Shift + mouse wheel zooms horizontally, and touchpad or mouse drag pans across the active chart.</p>
488
+ <div class="summary-strip">
489
+ <div class="summary-card">
490
+ <span>Source</span>
491
+ <strong id="sourceLabel"></strong>
492
+ </div>
493
+ <div class="summary-card">
494
+ <span>Format</span>
495
+ <strong id="sourceType"></strong>
496
+ </div>
497
+ <div class="summary-card">
498
+ <span>Charts</span>
499
+ <strong id="chartCount"></strong>
500
+ </div>
501
+ <div class="summary-card">
502
+ <span>Active</span>
503
+ <strong id="activeChartName"></strong>
504
+ </div>
505
+ </div>
506
+ </section>
507
+
508
+ <section class="workspace">
509
+ <aside class="sidebar">
510
+ <h2 class="section-label">Chart catalog</h2>
511
+ <div id="chartList" class="chart-list"></div>
512
+ <h2 class="section-label">Controls</h2>
513
+ <p class="hint">Use the viewer tools to save a PNG, reset zoom, and inspect exact values. Each chart keeps geotechnical axis labels and depth inversion where the source marked it.</p>
514
+ </aside>
515
+
516
+ <section class="canvas">
517
+ <div class="canvas-header">
518
+ <div>
519
+ <h2 id="chartTitle"></h2>
520
+ <p id="chartSubtitle"></p>
521
+ </div>
522
+ <div class="status-chip">interactive render</div>
523
+ </div>
524
+ <div id="chartHost"></div>
525
+ <div id="chartMetrics" class="details-grid"></div>
526
+ <div id="chartNote" class="note-panel"></div>
527
+ </section>
528
+ </section>
529
+ </div>
530
+
531
+ <script>${echartsSource}</script>
532
+ <script>
533
+ const payload = ${embeddedPayload};
534
+ const palette = ['#2dd4bf', '#60a5fa', '#f59e0b', '#fb7185', '#a78bfa', '#22c55e'];
535
+ const sourceLabel = document.getElementById('sourceLabel');
536
+ const sourceType = document.getElementById('sourceType');
537
+ const chartCount = document.getElementById('chartCount');
538
+ const activeChartName = document.getElementById('activeChartName');
539
+ const chartList = document.getElementById('chartList');
540
+ const chartTitle = document.getElementById('chartTitle');
541
+ const chartSubtitle = document.getElementById('chartSubtitle');
542
+ const chartMetrics = document.getElementById('chartMetrics');
543
+ const chartNote = document.getElementById('chartNote');
544
+ const chartHost = document.getElementById('chartHost');
545
+ const instance = echarts.init(chartHost, null, { renderer: 'canvas' });
546
+
547
+ sourceLabel.textContent = payload.sourceLabel;
548
+ sourceType.textContent = payload.sourceType;
549
+ chartCount.textContent = String(payload.charts.length);
550
+
551
+ function escapeHtml(value) {
552
+ return String(value).replace(/[&<>"']/g, (char) => ({
553
+ '&': '&amp;',
554
+ '<': '&lt;',
555
+ '>': '&gt;',
556
+ '"': '&quot;',
557
+ "'": '&#39;'
558
+ })[char]);
559
+ }
560
+
561
+ function numberOrDash(value) {
562
+ return typeof value === 'number' && Number.isFinite(value)
563
+ ? value.toLocaleString(undefined, { maximumFractionDigits: 2 })
564
+ : 'auto';
565
+ }
566
+
567
+ function buildSeries(chart) {
568
+ if (chart.kind === 'xy') {
569
+ return chart.series.map((series, index) => ({
570
+ type: series.style === 'scatter' ? 'scatter' : 'line',
571
+ name: series.label,
572
+ data: series.points.map((point) => [point.x, point.y]),
573
+ smooth: series.style !== 'scatter',
574
+ showSymbol: series.style === 'scatter' || series.points.length <= 40,
575
+ symbolSize: series.style === 'scatter' ? 9 : 7,
576
+ lineStyle: {
577
+ width: 2.6,
578
+ shadowBlur: 14,
579
+ shadowColor: palette[index % palette.length] + '55'
580
+ },
581
+ itemStyle: {
582
+ color: palette[index % palette.length]
583
+ },
584
+ emphasis: {
585
+ focus: 'series'
586
+ }
587
+ }));
588
+ }
589
+
590
+ return chart.series.map((series, index) => ({
591
+ type: 'line',
592
+ name: series.label,
593
+ data: series.values.map((value, pointIndex) => [chart.xValues[pointIndex] ?? pointIndex + 1, value]),
594
+ smooth: true,
595
+ showSymbol: series.values.length <= 40,
596
+ symbolSize: 7,
597
+ lineStyle: {
598
+ width: 2.6,
599
+ shadowBlur: 14,
600
+ shadowColor: palette[index % palette.length] + '55'
601
+ },
602
+ itemStyle: {
603
+ color: palette[index % palette.length]
604
+ },
605
+ emphasis: {
606
+ focus: 'series'
607
+ }
608
+ }));
609
+ }
610
+
611
+ function buildOption(chart) {
612
+ return {
613
+ backgroundColor: 'transparent',
614
+ animationDuration: 480,
615
+ color: palette,
616
+ legend: {
617
+ top: 10,
618
+ right: 18,
619
+ textStyle: {
620
+ color: '#a9b6c7',
621
+ fontSize: 12
622
+ }
623
+ },
624
+ tooltip: {
625
+ trigger: 'axis',
626
+ axisPointer: {
627
+ type: 'cross',
628
+ label: {
629
+ backgroundColor: '#09111d'
630
+ }
631
+ },
632
+ backgroundColor: 'rgba(3, 8, 16, 0.94)',
633
+ borderColor: 'rgba(96, 165, 250, 0.26)',
634
+ borderWidth: 1,
635
+ textStyle: {
636
+ color: '#e6edf8'
637
+ },
638
+ padding: [10, 12],
639
+ extraCssText: 'box-shadow:0 18px 48px rgba(2,8,23,0.48);border-radius:14px;'
640
+ },
641
+ grid: {
642
+ top: 68,
643
+ right: 28,
644
+ bottom: 82,
645
+ left: 72
646
+ },
647
+ toolbox: {
648
+ right: 18,
649
+ feature: {
650
+ saveAsImage: {
651
+ title: 'Save PNG',
652
+ pixelRatio: 2
653
+ },
654
+ restore: {
655
+ title: 'Reset'
656
+ }
657
+ },
658
+ iconStyle: {
659
+ borderColor: '#7dd3fc'
660
+ }
661
+ },
662
+ xAxis: {
663
+ type: chart.xScale === 'log10' ? 'log' : 'value',
664
+ name: chart.xLabel,
665
+ nameLocation: 'middle',
666
+ nameGap: 42,
667
+ min: chart.xDomain ? chart.xDomain[0] : null,
668
+ max: chart.xDomain ? chart.xDomain[1] : null,
669
+ axisLabel: {
670
+ color: '#90a2b7'
671
+ },
672
+ nameTextStyle: {
673
+ color: '#cdd8e7',
674
+ fontWeight: 600
675
+ },
676
+ axisLine: {
677
+ lineStyle: {
678
+ color: 'rgba(148,163,184,0.26)'
679
+ }
680
+ },
681
+ splitLine: {
682
+ lineStyle: {
683
+ color: 'rgba(148,163,184,0.10)'
684
+ }
685
+ },
686
+ minorSplitLine: chart.xScale === 'log10'
687
+ ? {
688
+ show: true,
689
+ lineStyle: {
690
+ color: 'rgba(148,163,184,0.06)'
691
+ }
692
+ }
693
+ : undefined
694
+ },
695
+ yAxis: {
696
+ type: 'value',
697
+ name: chart.yLabel,
698
+ nameGap: 54,
699
+ inverse: Boolean(chart.invertY),
700
+ min: chart.yDomain ? chart.yDomain[0] : null,
701
+ max: chart.yDomain ? chart.yDomain[1] : null,
702
+ axisLabel: {
703
+ color: '#90a2b7'
704
+ },
705
+ nameTextStyle: {
706
+ color: '#cdd8e7',
707
+ fontWeight: 600
708
+ },
709
+ axisLine: {
710
+ lineStyle: {
711
+ color: 'rgba(148,163,184,0.26)'
712
+ }
713
+ },
714
+ splitLine: {
715
+ lineStyle: {
716
+ color: 'rgba(148,163,184,0.10)'
717
+ }
718
+ }
719
+ },
720
+ dataZoom: [
721
+ {
722
+ type: 'inside',
723
+ zoomOnMouseWheel: 'shift',
724
+ moveOnMouseMove: true
725
+ },
726
+ {
727
+ type: 'inside',
728
+ orient: 'vertical'
729
+ },
730
+ {
731
+ type: 'slider',
732
+ bottom: 22,
733
+ height: 14,
734
+ borderColor: 'rgba(148,163,184,0.12)',
735
+ fillerColor: 'rgba(45,212,191,0.16)',
736
+ backgroundColor: 'rgba(15,23,42,0.52)',
737
+ textStyle: {
738
+ color: '#6c7d92'
739
+ }
740
+ }
741
+ ],
742
+ series: buildSeries(chart)
743
+ };
744
+ }
745
+
746
+ function buildMetric(label, value) {
747
+ return '<div class="detail-card"><span>' + escapeHtml(label) + '</span><strong>' + escapeHtml(value) + '</strong></div>';
748
+ }
749
+
750
+ function buildChartSummary(chart) {
751
+ const seriesCount = chart.series.length;
752
+ const pointCount = chart.kind === 'xy'
753
+ ? chart.series.reduce((total, series) => total + series.points.length, 0)
754
+ : chart.series.reduce((total, series) => total + series.values.length, 0);
755
+
756
+ chartMetrics.innerHTML = [
757
+ buildMetric('Series', String(seriesCount)),
758
+ buildMetric('Points', String(pointCount)),
759
+ buildMetric('X range', chart.xDomain ? numberOrDash(chart.xDomain[0]) + ' to ' + numberOrDash(chart.xDomain[1]) : 'auto'),
760
+ buildMetric('Y range', chart.yDomain ? numberOrDash(chart.yDomain[0]) + ' to ' + numberOrDash(chart.yDomain[1]) : 'auto'),
761
+ buildMetric('X scale', chart.xScale === 'log10' ? 'logarithmic' : 'linear'),
762
+ buildMetric('Y direction', chart.invertY ? 'inverted' : 'standard')
763
+ ].join('');
764
+ }
765
+
766
+ function buildChartSubtitle(chart) {
767
+ const parts = [
768
+ chart.xLabel + ' on X',
769
+ chart.yLabel + ' on Y',
770
+ chart.xScale === 'log10' ? 'logarithmic X scale' : 'linear X scale',
771
+ chart.invertY ? 'inverted Y axis' : 'standard Y axis'
772
+ ];
773
+ return parts.join(' | ');
774
+ }
775
+
776
+ let activeIndex = Math.max(
777
+ 0,
778
+ payload.charts.findIndex((chart) => chart.id === payload.activeChartId)
779
+ );
780
+
781
+ function renderCatalog() {
782
+ chartList.innerHTML = '';
783
+
784
+ payload.charts.forEach((chart, index) => {
785
+ const button = document.createElement('button');
786
+ button.type = 'button';
787
+ button.className = 'chart-button';
788
+ if (index === activeIndex) {
789
+ button.classList.add('active');
790
+ }
791
+ button.innerHTML =
792
+ '<strong>' + escapeHtml(chart.title) + '</strong>' +
793
+ '<span>' + escapeHtml(chart.kind === 'xy' ? 'XY chart' : 'Series chart') + '</span>';
794
+ button.addEventListener('click', () => {
795
+ activeIndex = index;
796
+ renderActiveChart();
797
+ renderCatalog();
798
+ });
799
+ chartList.appendChild(button);
800
+ });
801
+ }
802
+
803
+ function renderActiveChart() {
804
+ const chart = payload.charts[activeIndex];
805
+ if (!chart) {
806
+ return;
807
+ }
808
+
809
+ activeChartName.textContent = chart.title;
810
+ chartTitle.textContent = chart.title;
811
+ chartSubtitle.textContent = buildChartSubtitle(chart);
812
+ chartNote.textContent = chart.note || 'No additional engineering note was attached to this chart.';
813
+ buildChartSummary(chart);
814
+ instance.setOption(buildOption(chart), true);
815
+ }
816
+
817
+ renderCatalog();
818
+ renderActiveChart();
819
+
820
+ window.addEventListener('resize', () => instance.resize());
821
+ </script>
822
+ </body>
823
+ </html>`;
824
+ }
825
+ export function getPreferredPlotMode() {
826
+ const explicit = (process.env.GEOTECHCLI_PLOT_MODE ?? '').trim().toLowerCase();
827
+ if (explicit === 'ascii' || explicit === 'terminal' || explicit === 'text') {
828
+ return 'terminal';
829
+ }
830
+ if (explicit === 'browser' || explicit === 'interactive' || explicit === 'html') {
831
+ return 'browser';
832
+ }
833
+ return process.env.CI ? 'terminal' : 'browser';
834
+ }
835
+ export function shouldUseBrowserPlots(options) {
836
+ return !options?.terminal && getPreferredPlotMode() === 'browser';
837
+ }
838
+ export function renderInteractiveVisualization(source, options = {}) {
839
+ const payload = serializePayload(source, options);
840
+ const html = buildHtml(payload);
841
+ const outputPath = getOutputPath(source.sourceName, options.outputPath);
842
+ mkdirSync(dirname(outputPath), { recursive: true });
843
+ writeFileSync(outputPath, html, 'utf-8');
844
+ const opened = options.open === false ? false : openInBrowser(outputPath);
845
+ return {
846
+ htmlPath: outputPath,
847
+ opened,
848
+ };
849
+ }
850
+ //# sourceMappingURL=plot-viewer.js.map