sqlmath 2025.8.30 → 2025.9.30

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,4768 @@
1
+ /*jslint beta, bitwise, browser, devel, nomen*/
2
+ import {
3
+ assertOrThrow,
4
+ dbCloseAsync,
5
+ dbExecAsync,
6
+ dbFileSaveAsync,
7
+ dbFileLoadAsync,
8
+ dbOpenAsync,
9
+ dbTableImportAsync,
10
+ debugInline,
11
+ noop,
12
+ sqlmathWebworkerInit
13
+ } from "./sqlmath.mjs";
14
+ let BLOB_SAVE;
15
+ let CRISPX = -0.5;
16
+ let CRISPY = 0.5;
17
+ let {
18
+ CodeMirror
19
+ } = window;
20
+ let DBTABLE_DICT = new Map();
21
+ let DB_CHART;
22
+ let DB_DICT = new Map();
23
+ let DB_INIT = Promise.resolve();
24
+ let DB_MAIN;
25
+ let DB_QUERY;
26
+ let DEBOUNCE_DICT = Object.create(null);
27
+ let UI_ANIMATE_DATENOW;
28
+ let UI_ANIMATE_DURATION = 250;
29
+ let UI_ANIMATE_DURATION_INV = 1 / UI_ANIMATE_DURATION;
30
+ let UI_ANIMATE_LIST = [];
31
+ let UI_CONTEXTMENU = document.getElementById("contextmenu1");
32
+ let UI_CONTEXTMENU_BATON;
33
+ let UI_CRUD = document.getElementById("crudPanel1");
34
+ let UI_EDITOR;
35
+ let UI_FILE_OPEN = document.createElement("input");
36
+ let UI_FILE_SAVE = document.createElement("a");
37
+ let UI_LOADING = document.getElementById("loadingPanel1");
38
+ let UI_LOADING_COUNTER = 0;
39
+ let UI_PAGE_SIZE = 256;
40
+ let UI_ROW_HEIGHT = 16;
41
+ let UI_VIEW_SIZE = 20;
42
+
43
+ noop(debugInline);
44
+
45
+ async function dbFileAttachAsync({
46
+ db,
47
+ dbData
48
+ }) {
49
+ // this function will attach database <dbData> to <db>
50
+ let dbAttached;
51
+ let dbName = dbNameNext("attach{{ii}}", new Set(DB_DICT.keys()));
52
+ dbAttached = await dbOpenAsync({
53
+ dbData,
54
+ filename: `file:${dbName}?mode=memory&cache=shared`
55
+ });
56
+ dbAttached.dbName = dbName;
57
+ await dbExecAsync({
58
+ db,
59
+ sql: `ATTACH DATABASE '${dbAttached.filename}' AS ${dbName}`
60
+ });
61
+ DB_DICT.set(dbName, dbAttached);
62
+ // normalize order
63
+ DB_DICT = Array.from(DB_DICT.values());
64
+ DB_DICT = new Map([
65
+ DB_DICT.slice(0, 3),
66
+ DB_DICT.slice(3).sort(function (aa, bb) {
67
+ aa = aa.dbName;
68
+ bb = bb.dbName;
69
+ return (
70
+ aa < bb
71
+ ? -1
72
+ : 1
73
+ );
74
+ })
75
+ ].flat().map(function (db) {
76
+ return [
77
+ db.dbName, db
78
+ ];
79
+ }));
80
+ }
81
+
82
+ function dbNameNext(template, bag) {
83
+ // this function will get next incremental name in <bag> from given <template>
84
+ let ii = 0;
85
+ let name;
86
+ while (true) {
87
+ ii += 1;
88
+ name = template.replace("{{ii}}", String(ii).padStart(2, "0"));
89
+ if (!bag.has(name)) {
90
+ return name;
91
+ }
92
+ }
93
+ }
94
+
95
+ function debounce(key, func, ...argList) {
96
+ // this function will debounce <func> with given <key>
97
+ let val = DEBOUNCE_DICT[key];
98
+ if (val) {
99
+ val.func = func;
100
+ return;
101
+ }
102
+ val = {
103
+ func: noop,
104
+ timerTimeout: setTimeout(function () {
105
+ delete DEBOUNCE_DICT[key];
106
+ val.func(...argList);
107
+ }, 250)
108
+ };
109
+ DEBOUNCE_DICT[key] = val;
110
+ // if first-time, then immediately call <func>
111
+ func(...argList);
112
+ }
113
+
114
+ async function demoDefault() {
115
+ // this function will run demo-default
116
+ // attach demo-db
117
+ await dbFileAttachAsync({
118
+ db: DB_MAIN,
119
+ dbData: new ArrayBuffer(0)
120
+ });
121
+ UI_EDITOR.setValue(String(`
122
+ DROP TABLE IF EXISTS __stock_historical;
123
+ CREATE TABLE __stock_historical(sym TEXT, date TEXT, price REAL);
124
+ INSERT INTO __stock_historical (sym, date, price) VALUES
125
+ ('aapl', '2020-01-01', 77.37), ('aapl', '2020-02-01', 68.33),
126
+ ('aapl', '2020-03-01', 63.57), ('aapl', '2020-04-01', 73.44),
127
+ ('aapl', '2020-05-01', 79.48), ('aapl', '2020-06-01', 91.19),
128
+ ('aapl', '2020-07-01', 106.26), ('aapl', '2020-08-01', 129.03),
129
+ ('aapl', '2020-09-01', 115.80), ('aapl', '2020-10-01', 108.86),
130
+ ('aapl', '2020-11-01', 119.05), ('aapl', '2020-12-01', 132.69),
131
+ ('goog', '2020-01-01', 1434.23), ('goog', '2020-02-01', 1339.33),
132
+ ('goog', '2020-03-01', 1162.81), ('goog', '2020-04-01', 1348.66),
133
+ ('goog', '2020-05-01', 1428.92), ('goog', '2020-06-01', 1413.61),
134
+ ('goog', '2020-07-01', 1482.96), ('goog', '2020-08-01', 1634.18),
135
+ ('goog', '2020-09-01', 1469.60), ('goog', '2020-10-01', 1621.01),
136
+ ('goog', '2020-11-01', 1760.74), ('goog', '2020-12-01', 1751.88);
137
+
138
+ DROP TABLE IF EXISTS __test1;
139
+ CREATE TABLE __test1(
140
+ col1,
141
+ col2,
142
+ column_long_name_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
143
+ );
144
+
145
+ DROP TABLE IF EXISTS __test2;
146
+ CREATE TABLE __test2 AS
147
+ SELECT * FROM __test1
148
+ UNION ALL SELECT 1, 2, 3
149
+ UNION ALL SELECT 4, 5, 6;
150
+
151
+ DROP TABLE IF EXISTS attach01.__test3;
152
+ CREATE TABLE attach01.__test3 AS SELECT * FROM __test2;
153
+
154
+ SELECT
155
+ *,
156
+ random() AS c1,
157
+ random() AS c2,
158
+ random() AS c3,
159
+ random() AS c4,
160
+ random(),
161
+ random(),
162
+ random(),
163
+ random(),
164
+ 1 AS sentinel
165
+ FROM __stock_historical
166
+ LEFT JOIN __test1 ON __test1.col1 = __stock_historical.sym
167
+ CROSS JOIN (SELECT random() FROM __stock_historical);
168
+
169
+ DROP TABLE IF EXISTS chart.__stock_chart;
170
+ CREATE TABLE chart.__stock_chart (
171
+ datatype TEXT NOT NULL,
172
+ series_index INTEGER,
173
+ xx REAL,
174
+ yy REAL,
175
+ series_label REAL,
176
+ xx_label TEXT,
177
+ options TEXT
178
+ );
179
+ INSERT INTO chart.__stock_chart (datatype, options)
180
+ SELECT
181
+ 'options' AS datatype,
182
+ '{
183
+ "title": "price vs. date comparison of multiple stocks",
184
+ "xaxisTitle": "date",
185
+ "yaxisTitle": "percent gain",
186
+ "yvalueSuffix": " %"
187
+ }' AS options;
188
+ INSERT INTO chart.__stock_chart (datatype, series_index, series_label)
189
+ SELECT
190
+ 'series_label' AS datatype,
191
+ rownum AS series_index,
192
+ sym AS series_label
193
+ FROM (
194
+ SELECT
195
+ ROW_NUMBER() OVER (ORDER BY sym) AS rownum,
196
+ sym
197
+ FROM (SELECT DISTINCT sym FROM __stock_historical)
198
+ WHERE
199
+ sym IS NOT NULL
200
+ );
201
+ INSERT INTO chart.__stock_chart (datatype, xx, xx_label)
202
+ SELECT
203
+ 'xx_label' AS datatype,
204
+ rownum AS xx,
205
+ date AS xx_label
206
+ FROM (
207
+ SELECT
208
+ ROW_NUMBER() OVER (ORDER BY date) AS rownum,
209
+ date
210
+ FROM (SELECT DISTINCT date FROM __stock_historical)
211
+ );
212
+ INSERT INTO chart.__stock_chart (datatype, series_index, xx, yy)
213
+ SELECT
214
+ 'yy_value' AS datatype,
215
+ series_index,
216
+ xx,
217
+ price AS yy
218
+ FROM (
219
+ SELECT
220
+ series_index,
221
+ series_label,
222
+ xx,
223
+ xx_label
224
+ FROM (
225
+ SELECT
226
+ series_index,
227
+ series_label
228
+ FROM chart.__stock_chart
229
+ WHERE
230
+ datatype = 'series_label'
231
+ )
232
+ JOIN (
233
+ SELECT
234
+ xx,
235
+ xx_label
236
+ FROM chart.__stock_chart
237
+ WHERE
238
+ datatype = 'xx_label'
239
+ )
240
+ )
241
+ LEFT JOIN __stock_historical ON sym = series_label AND date = xx_label;
242
+ UPDATE chart.__stock_chart
243
+ SET
244
+ yy = yy * inv - 1
245
+ FROM (
246
+ --
247
+ SELECT
248
+ 1.0 / yy AS inv,
249
+ series_index
250
+ FROM (
251
+ SELECT
252
+ ROW_NUMBER() OVER (
253
+ PARTITION BY series_index ORDER BY xx
254
+ ) AS rownum,
255
+ yy,
256
+ series_index
257
+ FROM chart.__stock_chart
258
+ WHERE
259
+ datatype = 'yy_value'
260
+ AND yy > 0
261
+ )
262
+ WHERE
263
+ rownum = 1
264
+ --
265
+ ) AS __join1 WHERE __join1.series_index = __stock_chart.series_index;
266
+ `).trim() + "\n");
267
+ // exec demo-sql-query
268
+ await onDbExec({});
269
+ return true;
270
+ }
271
+
272
+ async function demoTradebot() {
273
+ // this function will run demo-tradebot
274
+ let tmp;
275
+ let tradebotState;
276
+ try {
277
+ tmp = await fetch(`.tradebot_public.sqlite?aa=${Date.now()}`);
278
+ if (tmp.status !== 200) {
279
+ return;
280
+ }
281
+ } catch (ignore) {
282
+ return;
283
+ }
284
+ tmp = await tmp.arrayBuffer();
285
+ await dbFileLoadAsync({
286
+ db: DB_MAIN,
287
+ dbData: tmp
288
+ });
289
+ tradebotState = noop(
290
+ await dbExecAsync({
291
+ db: DB_MAIN,
292
+ sql: "SELECT * FROM tradebot_state;"
293
+ })
294
+ )[0][0];
295
+ UI_EDITOR.setValue([
296
+ (`
297
+ -- table - tradebot_intraday_day - insert
298
+ DROP TABLE IF EXISTS tradebot_intraday_day;
299
+ CREATE TABLE tradebot_intraday_day AS
300
+ SELECT
301
+ sym,
302
+ xdate,
303
+ price
304
+ FROM tradebot_intraday_all
305
+ WHERE xdate >= (SELECT datemkt0_beg FROM tradebot_state)
306
+ --
307
+ UNION ALL
308
+ --
309
+ SELECT
310
+ sym,
311
+ DATETIME(datemkt0_beg, '-1 MINUTE') AS xdate,
312
+ price
313
+ FROM tradebot_historical
314
+ JOIN tradebot_state
315
+ WHERE tradebot_historical.xdate = datemkt0_lag;
316
+
317
+ -- table - tradebot_intraday_week - insert
318
+ DROP TABLE IF EXISTS tradebot_intraday_week;
319
+ CREATE TABLE tradebot_intraday_week AS
320
+ SELECT
321
+ sym,
322
+ xdate,
323
+ price
324
+ FROM tradebot_intraday_all
325
+ WHERE xdate = xdate2;
326
+
327
+ -- table - tradebot_technical_day - insert - lmt
328
+ DROP TABLE IF EXISTS tradebot_technical_day;
329
+ CREATE TABLE tradebot_technical_day(tname TEXT, tt REAL, tval REAL);
330
+ INSERT INTO tradebot_technical_day
331
+ SELECT
332
+ *
333
+ FROM (
334
+ SELECT
335
+ tname,
336
+ datemkt0_beg AS tt,
337
+ stk_beg0 AS tval
338
+ FROM tradebot_state
339
+ JOIN (
340
+ SELECT '1b_stk_lmt' AS tname
341
+ UNION ALL SELECT '1c_stk_pct'
342
+ UNION ALL SELECT '1d_stk_lmb'
343
+ )
344
+ --
345
+ UNION ALL
346
+ --
347
+ SELECT '1a_spy_cls', xdate, spy_cls FROM tradebot_technical_all
348
+ --
349
+ UNION ALL
350
+ --
351
+ SELECT '1b_stk_lmt', xdate, stk_lmt FROM tradebot_technical_all
352
+ --
353
+ UNION ALL
354
+ --
355
+ SELECT '1c_stk_pct', xdate, stk_pct FROM tradebot_technical_all
356
+ --
357
+ UNION ALL
358
+ --
359
+ SELECT '1d_stk_lmb', xdate, stk_lmb FROM tradebot_technical_all
360
+ --
361
+ UNION ALL
362
+ --
363
+ SELECT '1e_stk_pnl', xdate, stk_pnl FROM tradebot_technical_all
364
+ --
365
+ UNION ALL
366
+ --
367
+ SELECT '2a_spy_sin', xdate, spy_sin FROM tradebot_technical_all
368
+ --
369
+ UNION ALL
370
+ --
371
+ SELECT '2b_spy_cos', xdate, spy_cos FROM tradebot_technical_all
372
+ )
373
+ WHERE tt >= (SELECT datemkt0 FROM tradebot_state);
374
+
375
+ -- table - tradebot_technical_week - insert - lmt
376
+ DROP TABLE IF EXISTS tradebot_technical_week;
377
+ CREATE TABLE tradebot_technical_week(tname TEXT, tt REAL, tval REAL);
378
+ INSERT INTO tradebot_technical_week
379
+ SELECT
380
+ *
381
+ FROM (
382
+ SELECT
383
+ tname,
384
+ datemkt0_beg AS tt,
385
+ stk_beg0 AS tval
386
+ FROM tradebot_state
387
+ JOIN (
388
+ SELECT '1b_stk_lmt' AS tname
389
+ UNION ALL SELECT '1c_stk_pct'
390
+ UNION ALL SELECT '1d_stk_lmb'
391
+ )
392
+ --
393
+ UNION ALL
394
+ --
395
+ SELECT '1a_spy_cls', xdate, spy_cls FROM tradebot_technical_all
396
+ --
397
+ UNION ALL
398
+ --
399
+ SELECT '1b_stk_lmt', xdate, stk_lmt FROM tradebot_technical_all
400
+ --
401
+ UNION ALL
402
+ --
403
+ SELECT '1c_stk_pct', xdate, stk_pct FROM tradebot_technical_all
404
+ --
405
+ UNION ALL
406
+ --
407
+ SELECT '1d_stk_lmb', xdate, stk_lmb FROM tradebot_technical_all
408
+ --
409
+ UNION ALL
410
+ --
411
+ SELECT '1e_stk_pnl', xdate, stk_pnl FROM tradebot_technical_all
412
+ --
413
+ UNION ALL
414
+ --
415
+ SELECT '2a_spy_sin', xdate, spy_sin FROM tradebot_technical_all
416
+ --
417
+ UNION ALL
418
+ --
419
+ SELECT '2b_spy_cos', xdate, spy_cos FROM tradebot_technical_all
420
+ )
421
+ WHERE tt;
422
+ `),
423
+ [
424
+ "1 day",
425
+ "1 week",
426
+ "1 month",
427
+ "6 month",
428
+ "ytd",
429
+ "1 year",
430
+ "5 year",
431
+ "5 year reverse timeline"
432
+ ].map(function (dateInterval) {
433
+ let optionDict;
434
+ let tableChart;
435
+ let tableData;
436
+ tableData = (
437
+ dateInterval === "1 day"
438
+ ? "tradebot_intraday_day"
439
+ : dateInterval === "1 week"
440
+ ? "tradebot_intraday_week"
441
+ : "tradebot_historical"
442
+ );
443
+ tableChart = (
444
+ "chart._{{ii}}_tradebot_historical_"
445
+ + dateInterval.replace((
446
+ /\W/g
447
+ ), "_")
448
+ );
449
+ optionDict = {
450
+ title: (
451
+ "tradebot historical performance vs market - "
452
+ + dateInterval
453
+ + (
454
+ dateInterval === "1 day"
455
+ ? "\n[ updated " + new Date(
456
+ tradebotState.datenow + "Z"
457
+ ).toUTCString() + " ]"
458
+ : ""
459
+ )
460
+ ),
461
+ xaxisTitle: "date",
462
+ xstep: (
463
+ dateInterval === "1 day"
464
+ ? 60
465
+ : dateInterval === "1 week"
466
+ ? 15 * 60
467
+ : 1
468
+ ),
469
+ xvalueConvert: (
470
+ (dateInterval === "1 day" || dateInterval === "1 week")
471
+ ? "unixepochToTimeutc"
472
+ : "juliandayToDate"
473
+ ),
474
+ yaxisTitle: "percent gain",
475
+ yvalueSuffix: " %"
476
+ };
477
+ return (`
478
+ -- chart - ${tableChart} - create
479
+ DROP TABLE IF EXISTS ${tableChart};
480
+ CREATE TABLE ${tableChart} (
481
+ datatype TEXT NOT NULL,
482
+ series_index INTEGER,
483
+ xx REAL,
484
+ yy REAL,
485
+ series_label REAL,
486
+ xx_label TEXT,
487
+ options TEXT
488
+ );
489
+ INSERT INTO ${tableChart} (datatype, options)
490
+ SELECT
491
+ 'options' AS datatype,
492
+ '${JSON.stringify(optionDict)}' AS options;
493
+ INSERT INTO ${tableChart} (datatype, options, series_index, series_label)
494
+ SELECT
495
+ 'series_label' AS datatype,
496
+ JSON_OBJECT(
497
+ 'isDummy', is_dummy,
498
+ 'isHidden', NOT sym IN ('11_mybot', 'spy', 'qqq', 'dia')
499
+ ) AS options,
500
+ rownum AS series_index,
501
+ sym AS series_label
502
+ FROM (
503
+ SELECT
504
+ sym LIKE '-%' AS is_dummy,
505
+ ROW_NUMBER() OVER (
506
+ ORDER BY
507
+ sym = '11_mybot' DESC,
508
+ sym = '----' DESC,
509
+ sym = 'spy' DESC,
510
+ sym = 'qqq' DESC,
511
+ sym = 'dia' DESC,
512
+ sym = '---- ' DESC,
513
+ sym
514
+ ) AS rownum,
515
+ sym
516
+ FROM (
517
+ SELECT DISTINCT sym FROM ${tableData}
518
+ --
519
+ UNION ALL
520
+ --
521
+ SELECT '----'
522
+ --
523
+ UNION ALL
524
+ --
525
+ SELECT '---- '
526
+ )
527
+ );
528
+ DROP TABLE IF EXISTS __tmp1;
529
+ CREATE TEMP TABLE __tmp1 AS
530
+ SELECT
531
+ *
532
+ FROM (SELECT DISTINCT xdate FROM ${tableData})
533
+ JOIN (SELECT MIN(xdate) AS aa, MAX(xdate) AS bb FROM ${tableData});
534
+ UPDATE __tmp1
535
+ SET
536
+ aa = aa2
537
+ FROM (
538
+ SELECT
539
+ xdate AS aa2
540
+ FROM __tmp1
541
+ JOIN (
542
+ SELECT
543
+ MAX(aa,
544
+ ${
545
+ (
546
+ dateInterval === "5 year reverse timeline"
547
+ ? "DATE(bb, '-5 YEAR')"
548
+ : dateInterval === "ytd"
549
+ ? "DATE(STRFTIME('%Y', bb) || '-01-01', '-1 DAY')"
550
+ : "DATE(bb, '-" + dateInterval + "')"
551
+ )
552
+ }
553
+ ) AS aa2
554
+ FROM (SELECT aa, bb FROM __tmp1 LIMIT 1)
555
+ )
556
+ WHERE
557
+ xdate <= aa2
558
+ ORDER BY
559
+ xdate DESC
560
+ LIMIT 1
561
+ );
562
+ INSERT INTO ${tableChart} (datatype, xx, xx_label)
563
+ SELECT
564
+ 'xx_label' AS datatype,
565
+ rownum AS xx,
566
+ xdate AS xx_label
567
+ FROM (
568
+ SELECT
569
+ ROW_NUMBER() OVER (ORDER BY xdate ASC) AS rownum,
570
+ xdate
571
+ FROM __tmp1
572
+ WHERE
573
+ aa <= xdate AND xdate <= bb
574
+ );
575
+ INSERT INTO ${tableChart} (datatype, series_index, xx, yy)
576
+ SELECT
577
+ 'yy_value' AS datatype,
578
+ series_index,
579
+ xx,
580
+ price AS yy
581
+ FROM (
582
+ SELECT
583
+ series_index,
584
+ series_label,
585
+ xx,
586
+ xx_label
587
+ FROM (
588
+ SELECT
589
+ series_index,
590
+ series_label
591
+ FROM ${tableChart}
592
+ WHERE
593
+ datatype = 'series_label'
594
+ )
595
+ JOIN (
596
+ SELECT
597
+ xx,
598
+ xx_label
599
+ FROM ${tableChart}
600
+ WHERE
601
+ datatype = 'xx_label'
602
+ )
603
+ )
604
+ LEFT JOIN ${tableData} ON sym = series_label AND xdate = xx_label;
605
+ UPDATE ${tableChart}
606
+ SET
607
+ ${
608
+ (
609
+ dateInterval === "5 year reverse timeline"
610
+ ? "yy = ROUND(100 * (1.0 / (yy * inv) - 1), 4)"
611
+ : "yy = ROUND(100 * (yy * inv - 1), 4)"
612
+ )
613
+ }
614
+ FROM (
615
+ --
616
+ SELECT
617
+ 1.0 / yy AS inv,
618
+ series_index
619
+ FROM (
620
+ SELECT
621
+ ROW_NUMBER() OVER (
622
+ PARTITION BY series_index ORDER BY xx
623
+ ${
624
+ (
625
+ dateInterval === "5 year reverse timeline"
626
+ ? "DESC"
627
+ : "ASC"
628
+ )
629
+ }
630
+ ) AS rownum,
631
+ yy,
632
+ series_index
633
+ FROM ${tableChart}
634
+ WHERE
635
+ datatype = 'yy_value'
636
+ AND yy > 0
637
+ )
638
+ WHERE
639
+ rownum = 1
640
+ --
641
+ ) AS __join1 WHERE __join1.series_index = ${tableChart}.series_index;
642
+ UPDATE ${tableChart}
643
+ SET
644
+ series_label = printf(
645
+ '%+06.2f%% - %s%s',
646
+ yy_today,
647
+ series_label,
648
+ IIF(CASTTEXTOREMPTY(company_name) = '', '', ' - ' || company_name)
649
+ )
650
+ FROM (
651
+ --
652
+ SELECT
653
+ ${tableChart}.rowid,
654
+ --
655
+ company_name,
656
+ yy_today
657
+ FROM ${tableChart}
658
+ --
659
+ LEFT JOIN (
660
+ SELECT
661
+ series_index,
662
+ yy_today
663
+ FROM (
664
+ SELECT
665
+ ROW_NUMBER() OVER (
666
+ PARTITION BY series_index ORDER BY xx DESC
667
+ ) AS rownum,
668
+ series_index,
669
+ yy AS yy_today
670
+ FROM ${tableChart}
671
+ WHERE
672
+ datatype = 'yy_value'
673
+ )
674
+ WHERE
675
+ rownum = 1
676
+ ) USING (series_index)
677
+ LEFT JOIN tradebot_stock_basket ON sym = series_label
678
+ WHERE
679
+ datatype = 'series_label'
680
+ --
681
+ ) AS __join1 WHERE __join1.rowid = ${tableChart}.rowid;
682
+ -- chart - tradebot_historical - normalize xx to unixepoch
683
+ UPDATE ${tableChart}
684
+ SET
685
+ xx = ${(
686
+ (dateInterval === "1 day" || dateInterval === "1 week")
687
+ ? "UNIXEPOCH(tt)"
688
+ : "JULIANDAY(tt)"
689
+ )}
690
+ FROM (
691
+ --
692
+ SELECT
693
+ ${tableChart}.rowid,
694
+ --
695
+ tt
696
+ FROM ${tableChart}
697
+ --
698
+ JOIN (
699
+ SELECT
700
+ xx,
701
+ xx_label AS tt
702
+ FROM ${tableChart}
703
+ WHERE
704
+ datatype = 'xx_label'
705
+ ) USING (xx)
706
+ WHERE
707
+ datatype = 'yy_value'
708
+ --
709
+ ) AS __join1 WHERE __join1.rowid = ${tableChart}.rowid;
710
+ INSERT INTO ${tableChart} (datatype, series_index, xx, yy)
711
+ SELECT
712
+ 'yy_value' AS datatype,
713
+ series_index,
714
+ xx0 AS xx,
715
+ 0 AS yy
716
+ FROM (
717
+ SELECT
718
+ series_index
719
+ FROM ${tableChart}
720
+ WHERE
721
+ datatype = 'series_label'
722
+ )
723
+ JOIN (
724
+ SELECT
725
+ MIN(xx) AS xx0
726
+ FROM ${tableChart}
727
+ WHERE
728
+ datatype = 'yy_value'
729
+ )
730
+ WHERE
731
+ ${(dateInterval === "1 day" || dateInterval === "1 week")};
732
+ DELETE FROM ${tableChart} WHERE datatype = 'xx_label';
733
+ `);
734
+ }),
735
+ [
736
+ "sector",
737
+ "subsector",
738
+ "stock"
739
+ ].map(function (grouping) {
740
+ return [
741
+ "performance",
742
+ "holding"
743
+ ].map(function (ptype) {
744
+ let columnData;
745
+ let optionDict;
746
+ let sqlSelect;
747
+ let tableChart;
748
+ columnData = (
749
+ ptype === "performance"
750
+ ? "perc_gain_today"
751
+ : "perc_holding"
752
+ );
753
+ optionDict = {
754
+ isBarchart: true,
755
+ title: `tradebot ${ptype} today by ${grouping}`,
756
+ xaxisTitle: grouping,
757
+ yaxisTitle: (
758
+ ptype === "performance"
759
+ ? "percent gain"
760
+ : "percent holding"
761
+ ),
762
+ yvalueSuffix: " %"
763
+ };
764
+ sqlSelect = (
765
+ grouping === "sector"
766
+ ? (`
767
+ SELECT
768
+ (CASE
769
+ WHEN (category LIKE 'index%') THEN 3
770
+ WHEN (category LIKE 'short%') THEN 1
771
+ WHEN (grouping = 'sector') THEN 4
772
+ ELSE grouping_index
773
+ END) AS series_color,
774
+ category LIKE '-%' AS is_dummy,
775
+ -- 0 AS is_hidden,
776
+ grouping IN ('account', 'exchange') AS is_hidden,
777
+ printf(
778
+ '%05.4f%% - %s - %s',
779
+ ${columnData},
780
+ grouping,
781
+ category
782
+ ) AS series_label,
783
+ ROW_NUMBER() OVER (
784
+ ORDER BY
785
+ grouping_index,
786
+ category != '----' DESC,
787
+ ${columnData} DESC
788
+ ) AS xx,
789
+ category AS xx_label,
790
+ ${columnData} AS yy
791
+ FROM (
792
+ SELECT
793
+ category,
794
+ grouping,
795
+ grouping_index,
796
+ ${columnData},
797
+ perc_holding
798
+ FROM tradebot_position_category
799
+ WHERE
800
+ grouping != 'subsector'
801
+ --
802
+ UNION ALL
803
+ --
804
+ SELECT
805
+ '----' AS category,
806
+ '----' AS grouping,
807
+ grouping_index,
808
+ NULL AS ${columnData},
809
+ NULL perc_holding
810
+ FROM (
811
+ SELECT DISTINCT
812
+ grouping,
813
+ grouping_index
814
+ FROM tradebot_position_category
815
+ )
816
+ )
817
+ `)
818
+ : grouping === "subsector"
819
+ ? (`
820
+ SELECT
821
+ (CASE
822
+ WHEN (category LIKE 'index%') THEN 3
823
+ WHEN (category LIKE 'short%') THEN 1
824
+ ELSE 5
825
+ END) AS series_color,
826
+ category LIKE '-%' AS is_dummy,
827
+ 0 AS is_hidden,
828
+ printf('%05.4f%% - %s', ${columnData}, category) AS series_label,
829
+ ROW_NUMBER() OVER (
830
+ ORDER BY
831
+ grouping_index,
832
+ category != '----' DESC,
833
+ ${columnData} DESC
834
+ ) AS xx,
835
+ SUBSTR(category, INSTR(category, '____') + 4) AS xx_label,
836
+ ${columnData} AS yy
837
+ FROM (
838
+ SELECT
839
+ category,
840
+ grouping,
841
+ grouping_index,
842
+ ${columnData},
843
+ perc_holding
844
+ FROM tradebot_position_category
845
+ WHERE
846
+ grouping = 'subsector'
847
+ )
848
+ `)
849
+ : (`
850
+ SELECT
851
+ (CASE
852
+ WHEN (sector = 'index') THEN 3
853
+ WHEN (sector = 'short') THEN 1
854
+ ELSE 2
855
+ END) AS series_color,
856
+ 0 AS is_dummy,
857
+ 0 AS is_hidden,
858
+ printf(
859
+ '%+.4f%% - %s - %s - %s',
860
+ ${columnData},
861
+ 'long',
862
+ sym,
863
+ company_name
864
+ ) AS series_label,
865
+ ROW_NUMBER() OVER (ORDER BY ${columnData} DESC) AS xx,
866
+ sym AS xx_label,
867
+ ${columnData} AS yy
868
+ FROM tradebot_position_stock
869
+ `)
870
+ );
871
+ tableChart = `chart._{{ii}}_tradebot_${grouping}_${ptype}`;
872
+ return (`
873
+ -- chart - ${tableChart} - create
874
+ DROP TABLE IF EXISTS __tmp1;
875
+ CREATE TEMP TABLE __tmp1 AS SELECT * FROM (${sqlSelect}) ORDER BY xx;
876
+ DROP TABLE IF EXISTS ${tableChart};
877
+ CREATE TABLE ${tableChart} (
878
+ datatype TEXT NOT NULL,
879
+ series_index INTEGER,
880
+ xx REAL,
881
+ yy REAL,
882
+ series_label REAL,
883
+ xx_label TEXT,
884
+ options TEXT
885
+ );
886
+ INSERT INTO ${tableChart} (datatype, options)
887
+ SELECT
888
+ 'options' AS datatype,
889
+ '${JSON.stringify(optionDict)}' AS options;
890
+ INSERT INTO ${tableChart} (
891
+ datatype,
892
+ options,
893
+ series_index,
894
+ series_label
895
+ )
896
+ SELECT
897
+ 'series_label' AS datatype,
898
+ JSON_OBJECT(
899
+ 'isDummy', is_dummy,
900
+ 'isHidden', is_hidden,
901
+ 'seriesColor', series_color
902
+ ) AS options,
903
+ xx AS series_index,
904
+ series_label
905
+ FROM __tmp1;
906
+ INSERT INTO ${tableChart} (datatype, xx, xx_label)
907
+ SELECT
908
+ 'xx_label' AS datatype,
909
+ xx,
910
+ xx_label
911
+ FROM __tmp1;
912
+ INSERT INTO ${tableChart} (
913
+ datatype,
914
+ series_index,
915
+ xx,
916
+ yy
917
+ )
918
+ SELECT
919
+ 'yy_value' AS datatype,
920
+ xx AS series_index,
921
+ xx,
922
+ ROUNDORZERO(yy, 4) AS yy
923
+ FROM __tmp1;
924
+ `);
925
+ });
926
+ }),
927
+ (`
928
+ -- chart - tradebot_buysell_history
929
+ DROP TABLE IF EXISTS __tmp1;
930
+ CREATE TEMP TABLE __tmp1 AS
931
+ SELECT
932
+ series_color,
933
+ printf(
934
+ '%s %s',
935
+ xx_label,
936
+ series_label
937
+ ) AS series_label,
938
+ xx,
939
+ xx_label,
940
+ yy
941
+ FROM (
942
+ SELECT
943
+ (CASE
944
+ WHEN (sector LIKE 'index%') THEN 3
945
+ WHEN (sector LIKE 'short%') THEN 1
946
+ ELSE 2
947
+ END) AS series_color,
948
+ (
949
+ buy_or_sell
950
+ || ' '
951
+ || sym
952
+ || ' - '
953
+ || company_name
954
+ ) AS series_label,
955
+ UNIXEPOCH(time_filled) AS xx,
956
+ TIME(time_filled) AS xx_label,
957
+ ROUNDORZERO(
958
+ (
959
+ 100 * 1.0 / asset_under_mgmt
960
+ * IIF(buy_or_sell = 'buy', 1, -1)
961
+ * order_value
962
+ ),
963
+ 4
964
+ ) AS yy
965
+ FROM tradebot_account
966
+ JOIN tradebot_buysell_history
967
+ ORDER BY
968
+ time_filled
969
+ );
970
+ DROP TABLE IF EXISTS chart._{{ii}}_tradebot_buysell_history;
971
+ CREATE TABLE chart._{{ii}}_tradebot_buysell_history (
972
+ datatype TEXT NOT NULL,
973
+ series_index INTEGER,
974
+ xx REAL,
975
+ yy REAL,
976
+ series_label REAL,
977
+ xx_label TEXT,
978
+ options TEXT
979
+ );
980
+ INSERT INTO chart._{{ii}}_tradebot_buysell_history (datatype, options)
981
+ SELECT
982
+ 'options' AS datatype,
983
+ '{
984
+ "isBarchart": true,
985
+ "title": "tradebot buy/sell history today\\n[ updated '
986
+ || '${new Date(tradebotState.datenow + "Z").toUTCString()}'
987
+ || ' ]",
988
+ "xaxisTitle": "time",
989
+ "xvalueConvert": "unixepochToTimeutc",
990
+ "yaxisTitle": "buy/sell value as percentage of account",
991
+ "yvalueSuffix": " %"
992
+ }' AS options
993
+ FROM tradebot_state;
994
+ INSERT INTO chart._{{ii}}_tradebot_buysell_history (
995
+ datatype,
996
+ options,
997
+ series_index,
998
+ series_label
999
+ )
1000
+ SELECT
1001
+ 'series_label' AS datatype,
1002
+ JSON_OBJECT('seriesColor', series_color) AS options,
1003
+ __tmp1.rowid AS series_index,
1004
+ series_label
1005
+ FROM __tmp1;
1006
+ INSERT INTO chart._{{ii}}_tradebot_buysell_history (
1007
+ datatype,
1008
+ series_index,
1009
+ xx,
1010
+ yy
1011
+ )
1012
+ SELECT
1013
+ 'yy_value' AS datatype,
1014
+ __tmp1.rowid AS series_index,
1015
+ xx,
1016
+ yy
1017
+ FROM __tmp1;
1018
+ `),
1019
+ [
1020
+ "1 day",
1021
+ "1 week"
1022
+ ].map(function (dateInterval) {
1023
+ let optionDict;
1024
+ let tableChart;
1025
+ let tableData;
1026
+ tableData = (
1027
+ dateInterval === "1 day"
1028
+ ? "tradebot_technical_day"
1029
+ : dateInterval === "1 week"
1030
+ ? "tradebot_technical_week"
1031
+ : "tradebot_technical"
1032
+ );
1033
+ tableChart = (
1034
+ "chart._{{ii}}_tradebot_technical_"
1035
+ + dateInterval.replace((
1036
+ /\W/g
1037
+ ), "_")
1038
+ );
1039
+ optionDict = {
1040
+ title: (
1041
+ "tradebot technical - "
1042
+ + dateInterval
1043
+ + (
1044
+ dateInterval === "1 day"
1045
+ ? "\n[ updated " + new Date(
1046
+ tradebotState.datenow + "Z"
1047
+ ).toUTCString() + " ]"
1048
+ : ""
1049
+ )
1050
+ ),
1051
+ xaxisTitle: "date",
1052
+ xstep: (
1053
+ dateInterval === "1 day"
1054
+ ? 60
1055
+ : dateInterval === "1 week"
1056
+ ? 60
1057
+ // ? 15 * 60
1058
+ : 1
1059
+ ),
1060
+ xvalueConvert: (
1061
+ (dateInterval === "1 day" || dateInterval === "1 week")
1062
+ ? "unixepochToTimeutc"
1063
+ : "juliandayToDate"
1064
+ ),
1065
+ yaxisTitle: "percent holding",
1066
+ yvalueSuffix: " %"
1067
+ };
1068
+ return (`
1069
+ -- table - ${tableData} - normalize
1070
+ UPDATE ${tableData}
1071
+ SET
1072
+ tval = (CASE
1073
+ WHEN (tname = '1a_spy_cls') THEN
1074
+ (lmt_eee * cls_inv) * (tval - cls_avg) + lmt_avg
1075
+ WHEN (tname = '1e_stk_pnl') THEN
1076
+ (lmt_eee * pnl_inv) * (tval - pnl_avg) + lmt_avg
1077
+ WHEN (tname = '2a_spy_sin') THEN
1078
+ (lmt_eee * sin_inv) * (tval - sin_avg) + lmt_avg
1079
+ WHEN (tname = '2b_spy_cos') THEN
1080
+ (lmt_eee * cos_inv) * (tval - cos_avg) + lmt_avg
1081
+ END)
1082
+ FROM (
1083
+ SELECT
1084
+ lmt_avg,
1085
+ lmt_eee,
1086
+ --
1087
+ pnl_avg,
1088
+ pnl_inv,
1089
+ --
1090
+ cls_avg,
1091
+ cls_inv,
1092
+ --
1093
+ cos_avg,
1094
+ cos_inv,
1095
+ --
1096
+ sin_avg,
1097
+ sin_inv
1098
+ FROM (SELECT 0)
1099
+ JOIN (
1100
+ SELECT
1101
+ MEDIAN(tval) AS cls_avg,
1102
+ 1.0 / STDEV(tval) AS cls_inv
1103
+ FROM ${tableData}
1104
+ WHERE tname = '1a_spy_cls'
1105
+ )
1106
+ JOIN (
1107
+ SELECT
1108
+ MEDIAN(tval) AS lmt_avg,
1109
+ STDEV(tval) AS lmt_eee
1110
+ FROM ${tableData}
1111
+ WHERE tname = '1b_stk_lmt'
1112
+ )
1113
+ JOIN (
1114
+ SELECT
1115
+ MEDIAN(tval) AS pnl_avg,
1116
+ 1.0 / STDEV(tval) AS pnl_inv
1117
+ FROM ${tableData}
1118
+ WHERE tname = '1e_stk_pnl'
1119
+ )
1120
+ JOIN (
1121
+ SELECT
1122
+ MEDIAN(tval) AS cos_avg,
1123
+ 1.0 / STDEV(tval) AS cos_inv
1124
+ FROM ${tableData}
1125
+ WHERE tname = '2a_spy_sin'
1126
+ )
1127
+ JOIN (
1128
+ SELECT
1129
+ MEDIAN(tval) AS sin_avg,
1130
+ 1.0 / STDEV(tval) AS sin_inv
1131
+ FROM ${tableData}
1132
+ WHERE tname = '2b_spy_cos'
1133
+ )
1134
+ ) AS __join1
1135
+ WHERE
1136
+ tname IN ('1a_spy_cls', '1e_stk_pnl', '2a_spy_sin', '2b_spy_cos');
1137
+ UPDATE ${tableData}
1138
+ SET
1139
+ tt = UNIXEPOCH(tt),
1140
+ tval = ROUNDORZERO(tval, 4);
1141
+
1142
+ -- chart - ${tableChart} - create
1143
+ DROP TABLE IF EXISTS ${tableChart};
1144
+ CREATE TABLE ${tableChart} (
1145
+ datatype TEXT NOT NULL,
1146
+ series_index INTEGER,
1147
+ xx REAL,
1148
+ yy REAL,
1149
+ series_label REAL,
1150
+ xx_label TEXT,
1151
+ options TEXT
1152
+ );
1153
+ INSERT INTO ${tableChart} (datatype, options)
1154
+ SELECT
1155
+ 'options' AS datatype,
1156
+ '${JSON.stringify(optionDict)}' AS options;
1157
+
1158
+ INSERT INTO ${tableChart} (datatype, options, series_index, series_label)
1159
+ SELECT
1160
+ 'series_label' AS datatype,
1161
+ JSON_OBJECT(
1162
+ 'isHidden', NOT tname IN (
1163
+ '1a_spy_cls', '1b_stk_lmt', '1c_stk_pct'
1164
+ ),
1165
+ 'seriesColor', (CASE
1166
+ WHEN (tname = '1d_stk_lmb') THEN
1167
+ '#999'
1168
+ ELSE
1169
+ NULL
1170
+ -- (
1171
+ -- '#'
1172
+ -- || printf('%x', 12 - 2 * rownum)
1173
+ -- || printf('%x', 0 + 2 * rownum)
1174
+ -- || printf('%x', 16 - 2 * rownum)
1175
+ -- )
1176
+ END)
1177
+ ) AS options,
1178
+ rownum AS series_index,
1179
+ tname AS series_label
1180
+ FROM (
1181
+ SELECT
1182
+ ROW_NUMBER() OVER (ORDER BY tname) AS rownum,
1183
+ tname
1184
+ FROM (SELECT DISTINCT tname FROM ${tableData})
1185
+ WHERE
1186
+ tname IS NOT NULL
1187
+ );
1188
+ INSERT INTO ${tableChart} (datatype, xx, xx_label)
1189
+ SELECT
1190
+ 'xx_label' AS datatype,
1191
+ rownum AS xx,
1192
+ tt AS xx_label
1193
+ FROM (
1194
+ SELECT
1195
+ ROW_NUMBER() OVER (ORDER BY tt) AS rownum,
1196
+ tt
1197
+ FROM (SELECT DISTINCT tt FROM ${tableData})
1198
+ );
1199
+ INSERT INTO ${tableChart} (datatype, series_index, xx, yy)
1200
+ SELECT
1201
+ 'yy_value' AS datatype,
1202
+ series_index,
1203
+ xx_label AS xx,
1204
+ tval AS yy
1205
+ FROM (
1206
+ SELECT
1207
+ series_index,
1208
+ series_label,
1209
+ xx,
1210
+ xx_label
1211
+ FROM (
1212
+ SELECT
1213
+ series_index,
1214
+ series_label
1215
+ FROM ${tableChart}
1216
+ WHERE
1217
+ datatype = 'series_label'
1218
+ )
1219
+ JOIN (
1220
+ SELECT
1221
+ xx,
1222
+ xx_label
1223
+ FROM ${tableChart}
1224
+ WHERE
1225
+ datatype = 'xx_label'
1226
+ )
1227
+ )
1228
+ LEFT JOIN ${tableData} ON tname = series_label AND tt = xx_label;
1229
+ DELETE FROM ${tableChart} WHERE datatype = 'xx_label';
1230
+ UPDATE ${tableChart}
1231
+ SET
1232
+ series_label = (CASE
1233
+ WHEN (series_label = '1a_spy_cls') THEN '1a spy change'
1234
+ WHEN (series_label = '1b_stk_lmt') THEN '1b stk holding ideal'
1235
+ WHEN (series_label = '1c_stk_pct') THEN '1c stk holding actual'
1236
+ WHEN (series_label = '1d_stk_lmb') THEN '1d stk holding bracket min'
1237
+ WHEN (series_label = '1e_stk_pnl') THEN '1e stk gain'
1238
+ WHEN (series_label = '2a_spy_sin') THEN '2a spy sine'
1239
+ WHEN (series_label = '2b_spy_cos') THEN '2b spy cosine'
1240
+ END)
1241
+ WHERE
1242
+ datatype = 'series_label';
1243
+ `);
1244
+ }),
1245
+ (function () {
1246
+ let tableChart = `chart._{{ii}}_tradebot_technical_sinefit`;
1247
+ return (`
1248
+ CREATE TABLE ${tableChart} (
1249
+ datatype TEXT NOT NULL,
1250
+ series_index INTEGER,
1251
+ xx REAL,
1252
+ yy REAL,
1253
+ series_label REAL,
1254
+ xx_label TEXT,
1255
+ options TEXT
1256
+ );
1257
+
1258
+ -- table - ${tableChart} - insert
1259
+ INSERT INTO ${tableChart} (datatype, options)
1260
+ SELECT
1261
+ 'options' AS datatype,
1262
+ '{
1263
+ "title": "tradebot technical - sinusoidal fit of spy",
1264
+ "xaxisTitle": "date",
1265
+ "xvalueConvert": "juliandayToDate",
1266
+ "yaxisTitle": "percent gain",
1267
+ "yvalueSuffix": " %"
1268
+ }' AS options;
1269
+ INSERT INTO ${tableChart} (datatype, options, series_index, series_label)
1270
+ SELECT
1271
+ 'series_label' AS datatype,
1272
+ IIF(value <= 3, '{}', '{"isHidden": 1}') AS options,
1273
+ value AS series_index,
1274
+ (CASE
1275
+ WHEN (value = 1) THEN
1276
+ 'spy'
1277
+ WHEN (value = 2) THEN
1278
+ 'spy predicted linear - 2 month window'
1279
+ WHEN (value = 3) THEN
1280
+ 'spy predicted sine - 2 month window'
1281
+ WHEN (value = 4) THEN
1282
+ 'spy predicted linear+sine - 2 month window'
1283
+ WHEN (value = 5) THEN
1284
+ 'spy predicted linear - 6 month window'
1285
+ WHEN (value = 6) THEN
1286
+ 'spy predicted sine - 6 month window'
1287
+ WHEN (value = 7) THEN
1288
+ 'spy predicted linear+sine - 6 month window'
1289
+ END) AS series_label
1290
+ FROM GENERATE_SERIES(1, 7);
1291
+ INSERT INTO ${tableChart} (datatype, xx, xx_label)
1292
+ SELECT
1293
+ 'xx_label' AS datatype,
1294
+ JULIANDAY(xdate) AS xx,
1295
+ xdate AS xx_label
1296
+ FROM (
1297
+ SELECT DISTINCT xdate FROM tradebot_technical_sinefit ORDER BY ttt DESC
1298
+ );
1299
+ INSERT INTO ${tableChart} (datatype, series_index, xx, yy)
1300
+ SELECT
1301
+ 'yy_value' AS datatype,
1302
+ series_index,
1303
+ xx,
1304
+ yy
1305
+ FROM (SELECT xx, xx_label FROM ${tableChart} WHERE datatype = 'xx_label')
1306
+ LEFT JOIN (
1307
+ SELECT
1308
+ 1 AS series_index,
1309
+ xdate,
1310
+ price_actual AS yy
1311
+ FROM tradebot_technical_sinefit
1312
+ --
1313
+ UNION ALL
1314
+ --
1315
+ SELECT
1316
+ 2 AS series_index,
1317
+ xdate,
1318
+ price_linear_02 AS yy
1319
+ FROM tradebot_technical_sinefit
1320
+ --
1321
+ UNION ALL
1322
+ --
1323
+ SELECT
1324
+ 3 AS series_index,
1325
+ xdate,
1326
+ price_sine_02 + __offset AS yy
1327
+ FROM tradebot_technical_sinefit
1328
+ JOIN (
1329
+ SELECT
1330
+ price_actual - price_sine_02 AS __offset
1331
+ FROM tradebot_technical_sinefit
1332
+ WHERE
1333
+ ttt = 1
1334
+ )
1335
+ --
1336
+ UNION ALL
1337
+ --
1338
+ SELECT
1339
+ 4 AS series_index,
1340
+ xdate,
1341
+ price_predicted_02 AS yy
1342
+ FROM tradebot_technical_sinefit
1343
+ --
1344
+ UNION ALL
1345
+ --
1346
+ SELECT
1347
+ 5 AS series_index,
1348
+ xdate,
1349
+ price_linear_06 AS yy
1350
+ FROM tradebot_technical_sinefit
1351
+ --
1352
+ UNION ALL
1353
+ --
1354
+ SELECT
1355
+ 6 AS series_index,
1356
+ xdate,
1357
+ price_sine_06 + __offset AS yy
1358
+ FROM tradebot_technical_sinefit
1359
+ JOIN (
1360
+ SELECT
1361
+ price_actual - price_sine_06 AS __offset
1362
+ FROM tradebot_technical_sinefit
1363
+ WHERE
1364
+ ttt = 1
1365
+ )
1366
+ --
1367
+ UNION ALL
1368
+ --
1369
+ SELECT
1370
+ 7 AS series_index,
1371
+ xdate,
1372
+ price_predicted_06 AS yy
1373
+ FROM tradebot_technical_sinefit
1374
+ ) ON xdate = xx_label;
1375
+
1376
+ -- table - ${tableChart} - normalize - yy
1377
+ UPDATE ${tableChart}
1378
+ SET
1379
+ yy = yy * __inv
1380
+ FROM (
1381
+ SELECT
1382
+ 100.0 / price_actual AS __inv
1383
+ FROM tradebot_technical_sinefit
1384
+ WHERE
1385
+ ttt = (SELECT MAX(ttt) FROM tradebot_technical_sinefit)
1386
+ );
1387
+ UPDATE ${tableChart}
1388
+ SET
1389
+ yy = ROUND(yy - __avg, 4)
1390
+ FROM (
1391
+ SELECT
1392
+ MEDIAN(yy) AS __avg
1393
+ FROM ${tableChart}
1394
+ WHERE
1395
+ series_index = 1
1396
+ );
1397
+ `);
1398
+ }())
1399
+ ].flat().flat().map(function (sql, ii) {
1400
+ return sql.trim().replace((
1401
+ /\{\{ii\}\}/g
1402
+ ), String(ii).padStart(2, "0"));
1403
+ }).join("\n\n\n\n") + "\n");
1404
+ await onDbExec({});
1405
+ return true;
1406
+ }
1407
+
1408
+ function domDivCreate(innerHTML) {
1409
+ // this function will return div-element with rendered <innerHTML>
1410
+ let elem = document.createElement("div");
1411
+ elem.innerHTML = innerHTML;
1412
+ return elem;
1413
+ }
1414
+
1415
+ function fileSave({
1416
+ buf,
1417
+ filename
1418
+ }) {
1419
+ // this function will save <buf> with given <filename>
1420
+ // cleanup previous blob to prevent memory-leak
1421
+ URL.revokeObjectURL(BLOB_SAVE);
1422
+ // create new blob
1423
+ BLOB_SAVE = URL.createObjectURL(new Blob([
1424
+ buf
1425
+ ]));
1426
+ UI_FILE_SAVE.href = BLOB_SAVE;
1427
+ // cleanup blob to prevent memory-leak
1428
+ setTimeout(function () {
1429
+ URL.revokeObjectURL(UI_FILE_SAVE.href);
1430
+ }, 30000);
1431
+ UI_FILE_SAVE.download = filename.toLowerCase().replace((
1432
+ /[^0-9a-z]+/g
1433
+ ), "_").replace((
1434
+ /_([^_]+)$/
1435
+ ), (
1436
+ "_"
1437
+ + new Date().toISOString().slice(0, 10).replace((
1438
+ /-/g
1439
+ ), "")
1440
+ + ".$1"
1441
+ ));
1442
+ UI_FILE_SAVE.click();
1443
+ }
1444
+
1445
+ async function init() {
1446
+ let modeDemo = true;
1447
+ await sqlmathWebworkerInit({});
1448
+ // init DB_XXX
1449
+ [
1450
+ DB_CHART, DB_QUERY, DB_MAIN
1451
+ ] = await Promise.all([{
1452
+ dbName: "chart",
1453
+ filename: "file:dbchart?mode=memory&cache=shared",
1454
+ isDbchart: true
1455
+ }, {
1456
+ dbName: "query",
1457
+ filename: "file:dbquery?mode=memory&cache=shared",
1458
+ isDbquery: true
1459
+ }, {
1460
+ dbName: "main",
1461
+ filename: ":memory:",
1462
+ isDbmain: true
1463
+ }].map(async function (db) {
1464
+ db = Object.assign(noop(
1465
+ await dbOpenAsync({filename: db.filename})
1466
+ ), db);
1467
+ // save db
1468
+ DB_DICT.set(db.dbName, db);
1469
+ return db;
1470
+ }));
1471
+ // attach db
1472
+ await Promise.all([
1473
+ DB_CHART, DB_QUERY
1474
+ ].map(async function (db) {
1475
+ await dbExecAsync({
1476
+ db: DB_MAIN,
1477
+ sql: `ATTACH DATABASE [${db.filename}] AS ${db.dbName};`
1478
+ });
1479
+ }));
1480
+ // init UI_FILE_OPEN
1481
+ UI_FILE_OPEN.type = "file";
1482
+ // init sqlEditor
1483
+ UI_EDITOR = CodeMirror.fromTextArea(document.querySelector(
1484
+ "#sqliteEditor1"
1485
+ ), {
1486
+ extraKeys: {
1487
+ Tab: function (cm) {
1488
+ cm.replaceSelection(
1489
+ new Array(cm.getOption("indentUnit") + 1).join(" ")
1490
+ );
1491
+ }
1492
+ },
1493
+ lineNumbers: true,
1494
+ lineWrapping: true,
1495
+ matchBrackets: true,
1496
+ mode: "text/x-mysql"
1497
+ });
1498
+ // init event-handling
1499
+ [
1500
+ ["#tocPanel1", "click", onDbAction],
1501
+ [".dbExec", "click", onDbExec],
1502
+ [".dbcrudExec", "click", onDbcrudExec],
1503
+ [".modalCancel", "click", onModalClose],
1504
+ [".modalClose", "click", onModalClose],
1505
+ ["body", "click", onContextmenu],
1506
+ ["body", "contextmenu", onContextmenu],
1507
+ [UI_FILE_OPEN, "change", onDbAction],
1508
+ [document, "keydown", onKeyDown],
1509
+ [window, "hashchange", uitableInitWithinView],
1510
+ [window, "resize", onResize]
1511
+ ].forEach(function ([
1512
+ selector, evt, listener
1513
+ ]) {
1514
+ if (typeof selector !== "string") {
1515
+ selector.addEventListener(evt, listener);
1516
+ return;
1517
+ }
1518
+ selector = document.querySelectorAll(selector);
1519
+ assertOrThrow(selector.length > 0);
1520
+ selector.forEach(function (elem) {
1521
+ elem.addEventListener(evt, listener);
1522
+ });
1523
+ });
1524
+ // init event-handling - override window.onscroll
1525
+ window.onscroll = uitableInitWithinView;
1526
+ window.scroll({
1527
+ behavior: "smooth",
1528
+ top: 0
1529
+ });
1530
+ // init location.search
1531
+ await Promise.all(Array.from(
1532
+ location.search.slice(1).split("&")
1533
+ ).map(async function (elem) {
1534
+ let [
1535
+ key, val
1536
+ ] = elem.split("=");
1537
+ switch (key) {
1538
+ case "demo":
1539
+ switch (val) {
1540
+ case "demoDefault":
1541
+ modeDemo = undefined;
1542
+ await demoDefault();
1543
+ return;
1544
+ case "demoTradebot":
1545
+ modeDemo = undefined;
1546
+ await demoTradebot();
1547
+ return;
1548
+ }
1549
+ return;
1550
+ case "jsScript":
1551
+ modeDemo = undefined;
1552
+ key = document.createElement("script");
1553
+ key.src = val;
1554
+ if (val.endsWith(".mjs")) {
1555
+ key.type = "module";
1556
+ }
1557
+ document.head.appendChild(key);
1558
+ return;
1559
+ case "modeExpert":
1560
+ if (val === "1") {
1561
+ document.head.appendChild(domDivCreate(`
1562
+ <style>
1563
+ #contentPanel1 th {
1564
+ max-width: 48px;
1565
+ }
1566
+ </style>
1567
+ `).firstElementChild);
1568
+ }
1569
+ return;
1570
+ case "sqlDb":
1571
+ modeDemo = undefined;
1572
+ DB_INIT = new Promise(async function (resolve) {
1573
+ val = await fetch(val);
1574
+ val = await val.arrayBuffer();
1575
+ await dbFileLoadAsync({
1576
+ db: DB_MAIN,
1577
+ dbData: val
1578
+ });
1579
+ resolve();
1580
+ });
1581
+ return;
1582
+ case "sqlScript":
1583
+ modeDemo = undefined;
1584
+ await DB_INIT;
1585
+ val = await fetch(val);
1586
+ val = await val.text();
1587
+ UI_EDITOR.setValue(val);
1588
+ return;
1589
+ }
1590
+ }));
1591
+ // init UI_ANIMATE_TIMER_INTERVAL
1592
+ setInterval(function () {
1593
+ UI_ANIMATE_DATENOW = Date.now();
1594
+ UI_ANIMATE_LIST = UI_ANIMATE_LIST.filter(function (
1595
+ svgAnimateStep
1596
+ ) {
1597
+ return !svgAnimateStep();
1598
+ });
1599
+ }, 16);
1600
+ if (!modeDemo) {
1601
+ await DB_INIT;
1602
+ await onDbExec({});
1603
+ return;
1604
+ }
1605
+ // init demo
1606
+ if (
1607
+ await demoTradebot()
1608
+ ) {
1609
+ return;
1610
+ }
1611
+ await demoDefault();
1612
+ }
1613
+
1614
+ function jsonHtmlSafe(obj) {
1615
+ // this function will make <obj> html-safe
1616
+ // https://stackoverflow.com/questions/7381974
1617
+ return JSON.parse(JSON.stringify(obj).replace((
1618
+ /&/gu
1619
+ ), "&amp;").replace((
1620
+ /</gu
1621
+ ), "&lt;").replace((
1622
+ />/gu
1623
+ ), "&gt;"));
1624
+ }
1625
+
1626
+ function onContextmenu(evt) {
1627
+ // this function will handle contextmenu-event
1628
+ let baton;
1629
+ let {
1630
+ clientX,
1631
+ clientY,
1632
+ ctrlKey,
1633
+ metaKey,
1634
+ shiftKey,
1635
+ target,
1636
+ type
1637
+ } = evt;
1638
+ // contextmenu - left-click
1639
+ if (type !== "contextmenu") {
1640
+ // contextmenu - hide
1641
+ uiFadeOut(UI_CONTEXTMENU);
1642
+ // contextmenu - action
1643
+ if (target.closest(".contextmenuElem")) {
1644
+ onDbAction(evt);
1645
+ }
1646
+ return;
1647
+ }
1648
+ // contextmenu - right-click
1649
+ // contextmenu - enable default
1650
+ if (ctrlKey || metaKey || shiftKey) {
1651
+ return;
1652
+ }
1653
+ // contextmenu - disable default
1654
+ evt.preventDefault();
1655
+ evt.stopPropagation();
1656
+ // init target
1657
+ target = target.closest(`.tocElemA[data-dbtype], tr[data-dbtype="row"]`);
1658
+ // contextmenu - hide
1659
+ if (!target) {
1660
+ uiFadeOut(UI_CONTEXTMENU);
1661
+ return;
1662
+ }
1663
+ // init UI_CONTEXTMENU_BATON
1664
+ UI_CONTEXTMENU_BATON = DBTABLE_DICT.get(target.dataset.hashtag) || {};
1665
+ baton = UI_CONTEXTMENU_BATON;
1666
+ baton.rowid = target.dataset.rowid;
1667
+ // show / hide .contextmenuElem
1668
+ UI_CONTEXTMENU.querySelectorAll(
1669
+ ".contextmenuDivider, .contextmenuElem"
1670
+ ).forEach(function ({
1671
+ dataset,
1672
+ style
1673
+ }) {
1674
+ style.display = "none";
1675
+ if (dataset.dbtype !== target.dataset.dbtype) {
1676
+ return;
1677
+ }
1678
+ switch (dataset.action) {
1679
+ case "dbDetach":
1680
+ if (baton.isDbmain) {
1681
+ return;
1682
+ }
1683
+ break;
1684
+ case "dbrowDelete":
1685
+ case "dbrowUpdate":
1686
+ if (target.dataset.rowid === undefined) {
1687
+ return;
1688
+ }
1689
+ break;
1690
+ }
1691
+ style.display = "block";
1692
+ });
1693
+ // contextmenu - show
1694
+ UI_CONTEXTMENU.children[0].innerHTML = (
1695
+ "crud operation for:<br>"
1696
+ + stringHtmlSafe(baton.dbtableFullname || "script editor")
1697
+ );
1698
+ uiFadeIn(UI_CONTEXTMENU);
1699
+ UI_CONTEXTMENU.style.left = Math.max(0, Math.min(
1700
+ clientX,
1701
+ window.innerWidth - UI_CONTEXTMENU.offsetWidth - 10
1702
+ )) + "px";
1703
+ UI_CONTEXTMENU.style.top = Math.max(0, Math.min(
1704
+ clientY,
1705
+ window.innerHeight - UI_CONTEXTMENU.offsetHeight - 20
1706
+ )) + "px";
1707
+ }
1708
+
1709
+ async function onDbAction(evt) {
1710
+ // this function will open db from file
1711
+ let action;
1712
+ let baton = UI_CONTEXTMENU_BATON;
1713
+ let columntypeList;
1714
+ let data;
1715
+ let target;
1716
+ let title;
1717
+ target = evt.target.closest("[data-action]") || evt.target;
1718
+ action = target.dataset.action;
1719
+ if (!action) {
1720
+ return;
1721
+ }
1722
+ // fast actions that do not require loading
1723
+ switch (target !== UI_FILE_OPEN && action) {
1724
+ case "dbAttach":
1725
+ case "dbOpen":
1726
+ case "dbscriptOpen":
1727
+ case "dbtableImportCsv":
1728
+ case "dbtableImportJson":
1729
+ case "dbtableImportTsv":
1730
+ UI_FILE_OPEN.dataset.action = action;
1731
+ UI_FILE_OPEN.value = "";
1732
+ UI_FILE_OPEN.click();
1733
+ return;
1734
+ case "dbcolumnAdd":
1735
+ case "dbcolumnDrop":
1736
+ case "dbcolumnRename":
1737
+ case "dbrowInsert":
1738
+ case "dbrowUpdate":
1739
+ case "dbtableRename":
1740
+ title = target.textContent.trim().replace(/\s+/g, " ");
1741
+ UI_CRUD.querySelector(".modalTitle").innerHTML = (
1742
+ `${stringHtmlSafe(baton.dbtableFullname)}<br>${title}`
1743
+ );
1744
+ UI_CRUD.querySelector("tbody").innerHTML = (
1745
+ (`
1746
+ <tr class="crudInput-new_table" style="display: none;">
1747
+ <td><span class="crudLabel">{{new_table}}</span></td>
1748
+ <td class="tdInput">
1749
+ <input class="crudInput" type="text" value="new_table_1">
1750
+ </td>
1751
+ </tr>
1752
+ <tr class="crudInput-selected_column" style="display: none;">
1753
+ <td><span class="crudLabel">{{selected_column}}</span></td>
1754
+ <td class="tdInput">
1755
+ <select class="crudInput">
1756
+ `)
1757
+ + baton.colList.slice(1).map(function (col) {
1758
+ return (
1759
+ `<option>${stringHtmlSafe(col)}</option>`
1760
+ );
1761
+ }).join("")
1762
+ + (`
1763
+ </select>
1764
+ </td>
1765
+ </tr>
1766
+ <tr class="crudInput-new_column" style="display: none;">
1767
+ <td><span class="crudLabel">{{new_column}}</span></td>
1768
+ <td class="tdInput">
1769
+ <input class="crudInput" type="text" value="new_column_1">
1770
+ </td>
1771
+ </tr>
1772
+ `)
1773
+ );
1774
+ UI_CRUD.querySelector("textarea").value = String(
1775
+ `
1776
+
1777
+ -- column - add
1778
+ ALTER TABLE
1779
+ ${baton.dbtableFullname}
1780
+ ADD
1781
+ "{{new_column}}" TEXT NOT NULL DEFAULT '';
1782
+
1783
+ -- column - drop
1784
+ ALTER TABLE
1785
+ ${baton.dbtableFullname}
1786
+ DROP COLUMN
1787
+ "{{selected_column}}";
1788
+
1789
+ -- column - rename
1790
+ ALTER TABLE
1791
+ ${baton.dbtableFullname}
1792
+ RENAME
1793
+ "{{selected_column}}"
1794
+ TO
1795
+ "{{new_column}}";
1796
+
1797
+ -- row - insert
1798
+ INSERT INTO ${baton.dbtableFullname} (`
1799
+ + JSON.stringify(baton.colList.slice(1), undefined, 4).slice(1, -1)
1800
+ + `) VALUES (\n`
1801
+ + `${" NULL,\n".repeat(baton.colList.length - 2)} NULL`
1802
+ + `
1803
+ );
1804
+
1805
+ -- row - update
1806
+ UPDATE
1807
+ ${baton.dbtableFullname}
1808
+ SET
1809
+ `
1810
+ + baton.colList.slice(1).map(function (col) {
1811
+ return ` "${col}" = NULL`;
1812
+ }).join(",\n")
1813
+ + `
1814
+ WHERE
1815
+ rowid = ${baton.rowid};
1816
+
1817
+ -- table - rename
1818
+ ALTER TABLE
1819
+ ${baton.dbtableFullname}
1820
+ RENAME TO
1821
+ "{{new_table}}";
1822
+ `
1823
+ ).trim().split("\n\n").filter(function (sql) {
1824
+ return sql.indexOf(title) === 3;
1825
+ })[0] + "\n";
1826
+ switch (action) {
1827
+ case "dbcolumnAdd":
1828
+ UI_CRUD.querySelectorAll(
1829
+ ".crudInput-new_column"
1830
+ ).forEach(function (elem) {
1831
+ elem.style.display = "table-row";
1832
+ });
1833
+ break;
1834
+ case "dbcolumnDrop":
1835
+ UI_CRUD.querySelectorAll(
1836
+ ".crudInput-selected_column"
1837
+ ).forEach(function (elem) {
1838
+ elem.style.display = "table-row";
1839
+ });
1840
+ break;
1841
+ case "dbcolumnRename":
1842
+ UI_CRUD.querySelectorAll(
1843
+ ".crudInput-new_column, .crudInput-selected_column"
1844
+ ).forEach(function (elem) {
1845
+ elem.style.display = "table-row";
1846
+ });
1847
+ break;
1848
+ case "dbtableRename":
1849
+ UI_CRUD.querySelectorAll(
1850
+ ".crudInput-new_table"
1851
+ ).forEach(function (elem) {
1852
+ elem.style.display = "table-row";
1853
+ });
1854
+ break;
1855
+ }
1856
+ uiFadeIn(UI_CRUD);
1857
+ UI_CRUD.style.zIndex = 1;
1858
+ // ergonomy - auto-focus first input
1859
+ Array.from(UI_CRUD.querySelectorAll(
1860
+ ".modalContent input"
1861
+ )).every(function (elem) {
1862
+ elem.focus();
1863
+ });
1864
+ return;
1865
+ }
1866
+ // slow actions that require loading
1867
+ if (!evt.modeTryCatch) {
1868
+ evt.modeTryCatch = true;
1869
+ await uiTryCatch(onDbAction, evt);
1870
+ return;
1871
+ }
1872
+ evt.preventDefault();
1873
+ evt.stopPropagation();
1874
+ switch (target === UI_FILE_OPEN && action) {
1875
+ case "dbAttach":
1876
+ if (target.files.length === 0) {
1877
+ return;
1878
+ }
1879
+ await dbFileAttachAsync({
1880
+ db: DB_MAIN,
1881
+ dbData: (
1882
+ await target.files[0].arrayBuffer()
1883
+ )
1884
+ });
1885
+ await uiRenderDb();
1886
+ return;
1887
+ case "dbOpen":
1888
+ if (target.files.length === 0) {
1889
+ return;
1890
+ }
1891
+ await dbFileLoadAsync({
1892
+ db: DB_MAIN,
1893
+ dbData: (
1894
+ await target.files[0].arrayBuffer()
1895
+ )
1896
+ });
1897
+ await uiRenderDb();
1898
+ return;
1899
+ case "dbscriptOpen":
1900
+ UI_EDITOR.setValue(
1901
+ await target.files[0].text()
1902
+ );
1903
+ await uiRenderDb();
1904
+ return;
1905
+ case "dbtableImportCsv":
1906
+ case "dbtableImportJson":
1907
+ case "dbtableImportTsv":
1908
+ if (target.files.length === 0) {
1909
+ return;
1910
+ }
1911
+ await dbTableImportAsync({
1912
+ db: DB_MAIN,
1913
+ mode: (
1914
+ action === "dbtableImportCsv"
1915
+ ? "csv"
1916
+ : action === "dbtableImportTsv"
1917
+ ? "tsv"
1918
+ : "json"
1919
+ ),
1920
+ tableName: dbNameNext(
1921
+ "[__file{{ii}}]",
1922
+ new Set(DB_MAIN.dbtableList.keys())
1923
+ ).slice(1, -1),
1924
+ textData: (
1925
+ await target.files[0].text()
1926
+ )
1927
+ });
1928
+ await uiRenderDb();
1929
+ return;
1930
+ }
1931
+ switch (action) {
1932
+ case "dbDetach":
1933
+ if (!window.confirm(
1934
+ "are you sure you want to detach and close database"
1935
+ + ` ${baton.dbName} ?`
1936
+ )) {
1937
+ return;
1938
+ }
1939
+ await dbExecAsync({
1940
+ db: DB_MAIN,
1941
+ sql: `DETACH ${baton.dbName};`
1942
+ });
1943
+ await dbCloseAsync(baton.db);
1944
+ await uiRenderDb();
1945
+ return;
1946
+ case "dbExec":
1947
+ await onDbExec({});
1948
+ return;
1949
+ case "dbExport":
1950
+ data = await dbFileSaveAsync({db: baton.db});
1951
+ fileSave({
1952
+ buf: data,
1953
+ filename: `sqlite_database_${baton.dbName}.sqlite`
1954
+ });
1955
+ return;
1956
+ case "dbExportMain":
1957
+ data = await dbFileSaveAsync({db: DB_MAIN});
1958
+ fileSave({
1959
+ buf: data,
1960
+ filename: `sqlite_database_${DB_MAIN.dbName}.sqlite`
1961
+ });
1962
+ return;
1963
+ case "dbRefresh":
1964
+ await uiRenderDb();
1965
+ return;
1966
+ case "dbrowDelete":
1967
+ if (!window.confirm(
1968
+ `are you sure you want to delete row with rowid = ${baton.rowid}`
1969
+ + ` in table ${baton.dbtableFullname} ?`
1970
+ )) {
1971
+ return;
1972
+ }
1973
+ await dbExecAsync({
1974
+ db: baton.db,
1975
+ sql: (`
1976
+ DELETE FROM ${baton.dbtableName} WHERE rowid = ${baton.rowid};
1977
+ `)
1978
+ });
1979
+ await uiRenderDb();
1980
+ return;
1981
+ case "dbscriptSave":
1982
+ fileSave({
1983
+ buf: UI_EDITOR.getValue(),
1984
+ filename: `sqlite_script.sql`
1985
+ });
1986
+ return;
1987
+ case "dbtableDrop":
1988
+ if (!window.confirm(
1989
+ `are you sure you want to drop table ${baton.dbtableFullname} ?`
1990
+ )) {
1991
+ return;
1992
+ }
1993
+ await dbExecAsync({
1994
+ db: baton.db,
1995
+ sql: `DROP TABLE ${baton.dbtableName};`
1996
+ });
1997
+ await uiRenderDb();
1998
+ return;
1999
+ case "dbtableSaveCsv":
2000
+ data = await dbExecAsync({
2001
+ db: baton.db,
2002
+ responseType: "list",
2003
+ sql: `SELECT rowid, * FROM ${baton.dbtableName};`
2004
+ });
2005
+ data = data[0] || [];
2006
+ data.shift();
2007
+ data = rowListToCsv({
2008
+ colList: baton.colList,
2009
+ rowList: data
2010
+ });
2011
+ fileSave({
2012
+ buf: data || "",
2013
+ filename: `sqlite_table_${baton.dbtableName}.csv`
2014
+ });
2015
+ return;
2016
+ case "dbtableSaveJson":
2017
+ data = await dbExecAsync({
2018
+ db: baton.db,
2019
+ sql: `SELECT rowid, * FROM ${baton.dbtableName};`
2020
+ });
2021
+ fileSave({
2022
+ buf: JSON.stringify(data[0] || []),
2023
+ filename: `sqlite_table_${baton.dbtableName}.json`
2024
+ });
2025
+ return;
2026
+ case "dbtableSaveSql":
2027
+ columntypeList = await dbExecAsync({
2028
+ db: baton.db,
2029
+ responseType: "list",
2030
+ sql: (
2031
+ `SELECT `
2032
+ + baton.colList.map(function (col) {
2033
+ return `COLUMNTYPE(${col}) AS ${col}`;
2034
+ }).join(",")
2035
+ + ` FROM ${baton.dbtableName};`
2036
+ )
2037
+ });
2038
+ columntypeList = columntypeList[0][1];
2039
+ data = await dbExecAsync({
2040
+ db: baton.db,
2041
+ responseType: "list",
2042
+ sql: `SELECT rowid, * FROM ${baton.dbtableName};`
2043
+ });
2044
+ data = data[0] || [];
2045
+ data.shift();
2046
+ data = (
2047
+ String(`
2048
+ -- DROP TABLE __sqlite_table_01;
2049
+ -- SELECT * FROM __sqlite_table_01;
2050
+ -- ALTER TABLE __sqlite_table_01 RENAME TO __sqlite_table_02;
2051
+ -- EXEC sp_rename '__sqlite_table_01', '__sqlite_table_02';
2052
+ `).trim()
2053
+ + `\nCREATE TABLE __sqlite_table_01 (\n`
2054
+ + baton.colList.map(function (col, ii) {
2055
+ col = col.replace((/\W/g), "_");
2056
+ col = col.replace((/^\d/), "_$&");
2057
+ // #define SQLITE_INTEGER 1
2058
+ // #define SQLITE_COLUMNTYPE_INTEGER_BIG 11
2059
+ // #define SQLITE_FLOAT 2
2060
+ // #define SQLITE_TEXT 3
2061
+ // #define SQLITE_COLUMNTYPE_TEXT_BIG 13
2062
+ // #define SQLITE_BLOB 4
2063
+ // #define SQLITE_NULL 5
2064
+ switch (columntypeList[ii]) {
2065
+ case 2: // SQLITE_FLOAT
2066
+ return ` ${col} FLOAT(53)`;
2067
+ case 3: // SQLITE_TEXT
2068
+ return ` ${col} VARCHAR(255)`;
2069
+ case 11: // SQLITE_COLUMNTYPE_INTEGER_BIG
2070
+ return ` ${col} BIGINT`;
2071
+ case 13: // SQLITE_COLUMNTYPE_TEXT_BIG
2072
+ return ` ${col} TEXT`;
2073
+ default:
2074
+ return ` ${col} INTEGER`;
2075
+ }
2076
+ }).join(",\n")
2077
+ + `\n);\n`
2078
+ + data.map(function (rowList) {
2079
+ return (
2080
+ `INSERT INTO __sqlite_table_01 VALUES (`
2081
+ + rowList.map(function (val, ii) {
2082
+ switch (val !== null && columntypeList[ii]) {
2083
+ case 1: // SQLITE_INTEGER
2084
+ case 2: // SQLITE_FLOAT
2085
+ case 11: // SQLITE_COLUMNTYPE_INTEGER_BIG
2086
+ return val;
2087
+ case 3: // SQLITE_TEXT
2088
+ case 13: // SQLITE_COLUMNTYPE_TEXT_BIG
2089
+ return "'" + val.replace((/'/g), "''") + "'";
2090
+ // case 4: // SQLITE_BLOB
2091
+ // case 5: // SQLITE_NULL
2092
+ default:
2093
+ return "NULL";
2094
+ }
2095
+ }).join(",")
2096
+ + `);\n`
2097
+ );
2098
+ }).join("")
2099
+ );
2100
+ fileSave({
2101
+ buf: data,
2102
+ filename: `sqlite_table_${baton.dbtableName}.sql`
2103
+ });
2104
+ return;
2105
+ }
2106
+ throw new Error(`onDbAction - invalid action ${action}`);
2107
+ }
2108
+
2109
+ async function onDbExec({
2110
+ modeTryCatch
2111
+ }) {
2112
+ // this function will
2113
+ // 1. exec sql-command in webworker
2114
+ // 2. save query-result
2115
+ // 3. ui-render sql-queries to html
2116
+ let dbqueryList;
2117
+ if (!modeTryCatch) {
2118
+ await uiTryCatch(onDbExec, {
2119
+ modeTryCatch: true
2120
+ });
2121
+ return;
2122
+ }
2123
+ // close error modal
2124
+ uiFadeOut(document.querySelector("#errorPanel1"));
2125
+ // DBTABLE_DICT - cleanup old uitable
2126
+ DBTABLE_DICT.forEach(function ({
2127
+ isDbchart,
2128
+ isDbquery
2129
+ }, key) {
2130
+ if (isDbchart || isDbquery) {
2131
+ DBTABLE_DICT.delete(key);
2132
+ }
2133
+ });
2134
+ await Promise.all([
2135
+ DB_CHART, DB_QUERY
2136
+ ].map(async function (db) {
2137
+ let sqlCleanup = noop(
2138
+ await dbExecAsync({
2139
+ db,
2140
+ sql: (`
2141
+ BEGIN TRANSACTION;
2142
+ SELECT
2143
+ group_concat('DROP TABLE [' || name || '];', '') AS sql
2144
+ FROM sqlite_master
2145
+ WHERE
2146
+ type = 'table';
2147
+ END TRANSACTION;
2148
+ `)
2149
+ })
2150
+ )[0][0].sql || "";
2151
+ await dbExecAsync({
2152
+ db,
2153
+ sql: sqlCleanup
2154
+ });
2155
+ }));
2156
+ // 1. exec sql-command in webworker
2157
+ dbqueryList = await dbExecAsync({
2158
+ db: DB_MAIN,
2159
+ responseType: "list",
2160
+ sql: UI_EDITOR.getValue()
2161
+ });
2162
+ // 2. save query-result
2163
+ await Promise.all(dbqueryList.map(async function (rowList, ii) {
2164
+ let colList = rowList.shift().map(function (col, ii) {
2165
+ // bugfix - escape special-character in col
2166
+ return `value->>${ii} AS "${col.replace((/"/g), "\"\"")}"`;
2167
+ }).join(",");
2168
+ await dbExecAsync({
2169
+ bindList: {
2170
+ tmp1: JSON.stringify(rowList)
2171
+ },
2172
+ db: DB_QUERY,
2173
+ sql: (`
2174
+ BEGIN TRANSACTION;
2175
+ CREATE TABLE result_${String(ii).padStart(2, "0")} AS
2176
+ SELECT
2177
+ ${colList}
2178
+ FROM JSON_EACH($tmp1);
2179
+ END TRANSACTION;
2180
+ `)
2181
+ });
2182
+ }));
2183
+ // 3. ui-render sql-queries to html
2184
+ await uiRenderDb();
2185
+ }
2186
+
2187
+ async function onDbcrudExec({
2188
+ modeTryCatch
2189
+ }) {
2190
+ // this function will exec crud operation
2191
+ let sql;
2192
+ if (!modeTryCatch) {
2193
+ await uiTryCatch(onDbcrudExec, {
2194
+ modeTryCatch: true
2195
+ });
2196
+ return;
2197
+ }
2198
+ sql = document.querySelector("#crudPanel1 textarea").value.replace((
2199
+ /\{\{\w+?\}\}/g
2200
+ ), function (match0) {
2201
+ let val = document.querySelector(
2202
+ `#crudPanel1 .crudInput-${match0.slice(2, -2)} .crudInput`
2203
+ );
2204
+ if (!val) {
2205
+ return match0;
2206
+ }
2207
+ if (val.tagName === "SELECT") {
2208
+ return val.selectedOptions[0].textContent;
2209
+ }
2210
+ return val.value.trim();
2211
+ });
2212
+ await dbExecAsync({
2213
+ db: DB_MAIN,
2214
+ sql
2215
+ });
2216
+ await uiRenderDb();
2217
+ uiFadeOut(UI_CRUD);
2218
+ UI_CRUD.style.zIndex = -1;
2219
+ }
2220
+
2221
+ function onKeyDown(evt) {
2222
+ // this function will handle event keyup
2223
+ switch (evt.key) {
2224
+ case "Escape":
2225
+ // close error-modal
2226
+ uiFadeOut(document.querySelector("#errorPanel1"));
2227
+ // close contextmenu
2228
+ uiFadeOut(UI_CONTEXTMENU);
2229
+ return;
2230
+ }
2231
+ switch ((evt.ctrlKey || evt.metaKey) && evt.key) {
2232
+ // database - execute
2233
+ case "Enter":
2234
+ onDbExec({});
2235
+ return;
2236
+ // database - open
2237
+ case "o":
2238
+ evt.preventDefault();
2239
+ evt.stopPropagation();
2240
+ document.querySelector("button[data-action='dbOpen']").click();
2241
+ return;
2242
+ // database - save
2243
+ case "s":
2244
+ evt.preventDefault();
2245
+ evt.stopPropagation();
2246
+ document.querySelector("button[data-action='dbExportMain']").click();
2247
+ return;
2248
+ }
2249
+ }
2250
+
2251
+ function onModalClose({
2252
+ currentTarget
2253
+ }) {
2254
+ // this function will close current modal
2255
+ uiFadeOut(currentTarget.closest(".modalPanel"));
2256
+ }
2257
+
2258
+ function onResize() {
2259
+ // this function will handle resize-event
2260
+ document.querySelectorAll(
2261
+ "#dbchartList1 .contentElem"
2262
+ ).forEach(function (elem) {
2263
+ elem.dataset.init = "0";
2264
+ });
2265
+ uitableInitWithinView({});
2266
+ }
2267
+
2268
+ function rowListToCsv({
2269
+ colList,
2270
+ rowList
2271
+ }) {
2272
+ // this function will convert json <rowList> to csv with given <colList>
2273
+ let data = JSON.stringify([[colList], rowList].flat(), undefined, 1);
2274
+ // convert data to csv
2275
+ data = data.replace((
2276
+ /\n /g
2277
+ ), "");
2278
+ data = data.replace((
2279
+ /\n \[/g
2280
+ ), "");
2281
+ data = data.replace((
2282
+ /\n \],?/g
2283
+ ), "\r\n");
2284
+ data = data.slice(1, -2);
2285
+ // sqlite-strings are c-strings which should never contain null-char
2286
+ data = data.replace((
2287
+ /\u0000/g
2288
+ ), "");
2289
+ // hide double-backslash `\\\\` as null-char
2290
+ data = data.replace((
2291
+ /\\\\/g
2292
+ ), "\u0000");
2293
+ // escape double-quote `\\"` to `""`
2294
+ data = data.replace((
2295
+ /\\"/g
2296
+ ), "\"\"");
2297
+ // replace newline with space
2298
+ data = data.replace((
2299
+ /\\r\\n|\\r|\\n/g
2300
+ ), " ");
2301
+ // restore double-backslash `\\\\` from null-char
2302
+ data = data.replace((
2303
+ /\u0000/g
2304
+ ), "\\\\");
2305
+ return data;
2306
+ }
2307
+
2308
+ function stringHtmlSafe(str) {
2309
+ // this function will make <str> html-safe
2310
+ // https://stackoverflow.com/questions/7381974
2311
+ if (typeof str !== "string") {
2312
+ str = String(str);
2313
+ }
2314
+ return str.replace((
2315
+ /&/gu
2316
+ ), "&amp;").replace((
2317
+ /</gu
2318
+ ), "&lt;").replace((
2319
+ />/gu
2320
+ ), "&gt;").replace((
2321
+ /"/gu
2322
+ ), "&quot;");
2323
+ }
2324
+
2325
+ function svgAnimate(elem, attrDict, mode) {
2326
+ // this function will animate <elem> with given <fxattr> in <attrDict>
2327
+ let {
2328
+ childNodes,
2329
+ fx_rotate,
2330
+ fx_seriesShape,
2331
+ nodeName
2332
+ } = elem;
2333
+ let datebeg = UI_ANIMATE_DATENOW;
2334
+ let fxstateDict = {};
2335
+ let {
2336
+ translateY
2337
+ } = attrDict;
2338
+ // optimization - skip animation if hidden
2339
+ if (attrDict.visibility === "hidden") {
2340
+ svgAttrSet(elem, attrDict);
2341
+ return;
2342
+ }
2343
+ // init fxstateDict
2344
+ Object.entries(attrDict).forEach(function ([
2345
+ fxattr, fxend
2346
+ ]) {
2347
+ let dpathList;
2348
+ let fxattr2 = "fx_" + fxattr;
2349
+ let fxbeg;
2350
+ let fxstate;
2351
+ // crispify height, width, x, y
2352
+ switch (fxattr) {
2353
+ case "height":
2354
+ case "width":
2355
+ case "x":
2356
+ case "y":
2357
+ fxend = Math.round(fxend);
2358
+ switch (fxattr) {
2359
+ case "x":
2360
+ fxend += CRISPX;
2361
+ break;
2362
+ case "y":
2363
+ fxend += CRISPY;
2364
+ break;
2365
+ }
2366
+ break;
2367
+ }
2368
+ fxstate = {
2369
+ fxend
2370
+ };
2371
+ switch (fxattr) {
2372
+ case "d":
2373
+ dpathList = fxend.split(" ");
2374
+ fxbeg = noop(elem.getAttribute(fxattr) ?? "").split(" ");
2375
+ if (!(fxbeg.length > 0 && fxbeg.length === dpathList.length)) {
2376
+ elem[fxattr2] = fxend;
2377
+ elem.setAttribute(fxattr, fxend);
2378
+ return;
2379
+ }
2380
+ fxstate.dpathList = dpathList;
2381
+ fxstate.fxbeg = fxbeg;
2382
+ fxstateDict[fxattr] = fxstate;
2383
+ break;
2384
+ case "height":
2385
+ case "translateX":
2386
+ case "translateY":
2387
+ case "width":
2388
+ case "x":
2389
+ case "y":
2390
+ fxstate.fxbeg = Number(
2391
+ elem[fxattr2] ?? elem.getAttribute(fxattr) ?? 0
2392
+ );
2393
+ if (fx_seriesShape) {
2394
+ fxstate.fxbeg = fxstate.fxbeg || fxend;
2395
+ }
2396
+ fxstateDict[fxattr] = fxstate;
2397
+ break;
2398
+ case "visibility":
2399
+ elem.setAttribute(fxattr, fxend);
2400
+ return;
2401
+ default:
2402
+ throw new Error(`svgAnimate - invalid attribute - ${fxattr}`);
2403
+ }
2404
+ });
2405
+ function svgAnimateStep() {
2406
+ let fxprg = 1;
2407
+ let isDone = datebeg + UI_ANIMATE_DURATION <= UI_ANIMATE_DATENOW;
2408
+ // animate - linear fxnow, fxprg
2409
+ if (!isDone) {
2410
+ fxprg = (
2411
+ UI_ANIMATE_DURATION_INV
2412
+ * (UI_ANIMATE_DATENOW - datebeg)
2413
+ );
2414
+ if (mode === "easeout") {
2415
+ fxprg = Math.sqrt(fxprg);
2416
+ }
2417
+ }
2418
+ Object.entries(fxstateDict).forEach(function ([
2419
+ fxattr, fxstate
2420
+ ]) {
2421
+ let {
2422
+ dpathList,
2423
+ fxbeg,
2424
+ fxend
2425
+ } = fxstate;
2426
+ let fxattr2 = "fx_" + fxattr;
2427
+ let fxnow = fxend;
2428
+ switch (fxattr) {
2429
+ // Perform the next step of the animation on "d"
2430
+ case "d":
2431
+ if (!dpathList) {
2432
+ return;
2433
+ }
2434
+ // interpolate fxnow from dpathList, fxbeg
2435
+ if (!isDone) {
2436
+ fxnow = fxbeg.map(function (char, ii) {
2437
+ let num;
2438
+ if ("CLMZ".indexOf(char) !== -1) {
2439
+ return char;
2440
+ }
2441
+ num = Number(char);
2442
+ return num + fxprg * (dpathList[ii] - num);
2443
+ }).join(" ");
2444
+ }
2445
+ // cache attribute
2446
+ elem[fxattr2] = fxnow;
2447
+ elem.setAttribute("d", fxnow);
2448
+ return;
2449
+ case "height":
2450
+ case "translateX":
2451
+ case "translateY":
2452
+ case "width":
2453
+ case "x":
2454
+ case "y":
2455
+ if (!isDone) {
2456
+ fxnow = fxbeg + fxprg * (fxend - fxbeg);
2457
+ }
2458
+ // cache attribute
2459
+ elem[fxattr2] = fxnow;
2460
+ if (fx_seriesShape || translateY) {
2461
+ return;
2462
+ }
2463
+ // update child tspans x values
2464
+ if (fxattr === "x" && nodeName === "text") {
2465
+ childNodes.forEach(function (child) {
2466
+ // if the x values are equal, the tspan represents a
2467
+ // linebreak
2468
+ child.setAttribute("x", fxnow);
2469
+ });
2470
+ if (fx_rotate) {
2471
+ elem.setAttribute(
2472
+ "transform",
2473
+ `rotate(-15 ${fxnow} ${elem.fx_y || 0})`
2474
+ );
2475
+ }
2476
+ }
2477
+ elem.setAttribute(fxattr, fxnow);
2478
+ return;
2479
+ }
2480
+ });
2481
+ if (fx_seriesShape) {
2482
+ elem.setAttribute("d", svgShapeDraw(
2483
+ fx_seriesShape,
2484
+ elem.fx_x,
2485
+ elem.fx_y,
2486
+ elem.fx_width,
2487
+ elem.fx_height
2488
+ ));
2489
+ }
2490
+ if (translateY) {
2491
+ elem.setAttribute(
2492
+ "transform",
2493
+ `translate(${elem.fx_translateX},${elem.fx_translateY})`
2494
+ );
2495
+ }
2496
+ return isDone;
2497
+ }
2498
+ // animate - stop existing animation for given elem
2499
+ svgAnimateStep.elem2 = elem;
2500
+ UI_ANIMATE_LIST = UI_ANIMATE_LIST.filter(function ({
2501
+ elem2
2502
+ }) {
2503
+ return elem2 !== elem;
2504
+ });
2505
+ // animate - svgAnimateStep()
2506
+ svgAnimateStep();
2507
+ // animate - setInterval()
2508
+ UI_ANIMATE_LIST.push(svgAnimateStep);
2509
+ }
2510
+
2511
+ function svgAttrSet(elem, attrDict = {}) {
2512
+ // this function will set-attribute items in <attrDict> to <elem>
2513
+ if (typeof elem === "string") {
2514
+ elem = document.createElementNS("http://www.w3.org/2000/svg", elem);
2515
+ }
2516
+ Object.entries(attrDict).forEach(function ([
2517
+ key, val
2518
+ ]) {
2519
+ if (val !== null && val !== undefined) {
2520
+ elem.setAttribute(key, val);
2521
+ switch (key) {
2522
+ case "clip-path":
2523
+ case "d":
2524
+ case "fill":
2525
+ case "stroke":
2526
+ case "stroke-linecap":
2527
+ case "stroke-linejoin":
2528
+ case "stroke-width":
2529
+ case "text-anchor":
2530
+ case "transform":
2531
+ case "visibility":
2532
+ elem.setAttribute(key, val);
2533
+ return;
2534
+ // crispify height, width, x, y
2535
+ case "height":
2536
+ case "width":
2537
+ case "x":
2538
+ case "y":
2539
+ val = Math.round(val);
2540
+ switch (key) {
2541
+ case "x":
2542
+ val += CRISPX;
2543
+ break;
2544
+ case "y":
2545
+ val += CRISPY;
2546
+ break;
2547
+ }
2548
+ elem.setAttribute(key, val);
2549
+ // cache attribute
2550
+ elem["fx_" + key] = val;
2551
+ return;
2552
+ default:
2553
+ throw new Error(`svgAttrSet - invalid attribute - ${key}`);
2554
+ }
2555
+ }
2556
+ });
2557
+ return elem;
2558
+ }
2559
+
2560
+ function svgShapeDraw(seriesShape, x, y, w, h) {
2561
+ // this function will create svg-dpath for given <seriesShape>
2562
+ let tmp;
2563
+ switch (seriesShape) {
2564
+ case "circle":
2565
+ tmp = 0.166 * w;
2566
+ return [
2567
+ "M", x + w / 2, y,
2568
+ "C", x + w + tmp, y, x + w + tmp, y + h, x + w / 2, y + h,
2569
+ "C", x - tmp, y + h, x - tmp, y, x + w / 2, y,
2570
+ "Z"
2571
+ ].join(" ");
2572
+ case "diamond":
2573
+ return [
2574
+ "M", x + w / 2, y,
2575
+ "L", x + w, y + h / 2,
2576
+ x + w / 2, y + h,
2577
+ x, y + h / 2,
2578
+ "Z"
2579
+ ].join(" ");
2580
+ case "square":
2581
+ x = Math.round(x + 0.0625 * w);
2582
+ y = Math.round(y + 0.0625 * h);
2583
+ w = Math.round(0.875 * w);
2584
+ h = Math.round(0.875 * h);
2585
+ return [
2586
+ "M", x, y,
2587
+ "L", x + w, y,
2588
+ x + w, y + h,
2589
+ x, y + h,
2590
+ "Z"
2591
+ ].join(" ");
2592
+ case "triangle":
2593
+ return [
2594
+ "M", x + w / 2, y,
2595
+ "L", x + w, y + h,
2596
+ x, y + h,
2597
+ "Z"
2598
+ ].join(" ");
2599
+ case "triangle-down":
2600
+ return [
2601
+ "M", x, y,
2602
+ "L", x + w, y,
2603
+ x + w / 2, y + h,
2604
+ "Z"
2605
+ ].join(" ");
2606
+ }
2607
+ }
2608
+
2609
+ function uiFadeIn(elem) {
2610
+ // this function will fade-in <elem>
2611
+ elem.style.opacity = (
2612
+ elem === UI_CRUD
2613
+ ? "1"
2614
+ : "0.875"
2615
+ );
2616
+ elem.style.visibility = "visible";
2617
+ }
2618
+
2619
+ function uiFadeOut(elem) {
2620
+ // this function will fade-out <elem>
2621
+ elem.style.opacity = "0";
2622
+ elem.style.visibility = "hidden";
2623
+ }
2624
+
2625
+ async function uiRenderDb() {
2626
+ // this function will render #dbtableList1
2627
+ let dbList;
2628
+ let dbtableIi = 0;
2629
+ let html = "";
2630
+ let windowScrollY;
2631
+ // reset location.hash
2632
+ location.hash = "0";
2633
+ // save window.scrollY
2634
+ windowScrollY = window.scrollY;
2635
+ // DB_DICT - sync
2636
+ dbList = await dbExecAsync({
2637
+ db: DB_MAIN,
2638
+ sql: "PRAGMA database_list;"
2639
+ });
2640
+ dbList = new Set(dbList[0].map(function ({
2641
+ name
2642
+ }) {
2643
+ return `${name}`;
2644
+ }));
2645
+ DB_DICT.forEach(function ({
2646
+ dbName,
2647
+ isDbchart,
2648
+ isDbquery
2649
+ }) {
2650
+ if (!isDbchart && !isDbquery && !dbList.has(dbName)) {
2651
+ DB_DICT.delete(dbName);
2652
+ }
2653
+ });
2654
+ // DBTABLE_DICT - cleanup old uitable
2655
+ DBTABLE_DICT.forEach(function ({
2656
+ isDbchart,
2657
+ isDbquery
2658
+ }, key) {
2659
+ if (!isDbchart && !isDbquery) {
2660
+ DBTABLE_DICT.delete(key);
2661
+ }
2662
+ });
2663
+ // DBTABLE_DICT - sync
2664
+ await Promise.all(Array.from(DB_DICT.values()).map(async function (db) {
2665
+ let baton;
2666
+ let {
2667
+ dbName,
2668
+ isDbchart,
2669
+ isDbmain,
2670
+ isDbquery
2671
+ } = db;
2672
+ let dbtableList;
2673
+ let tmp;
2674
+ db.dbtableList = new Map();
2675
+ dbtableList = noop(
2676
+ await dbExecAsync({
2677
+ db,
2678
+ sql: (`
2679
+ SELECT * FROM sqlite_schema WHERE type = 'table' ORDER BY tbl_name;
2680
+ `)
2681
+ })
2682
+ )[0];
2683
+ if (!dbtableList) {
2684
+ return;
2685
+ }
2686
+ dbtableList = new Map(dbtableList.map(function ({
2687
+ colList = [],
2688
+ dbtableFullname,
2689
+ rowCount = 0,
2690
+ rowList0,
2691
+ sql,
2692
+ tbl_name
2693
+ }, ii) {
2694
+ dbtableFullname = dbtableFullname || `${dbName}.[${tbl_name}]`;
2695
+ dbtableIi += 1;
2696
+ baton = {
2697
+ colList,
2698
+ db,
2699
+ dbName,
2700
+ dbtableFullname,
2701
+ dbtableIi,
2702
+ dbtableName: `[${tbl_name}]`,
2703
+ hashtag: `dbtable_${String(dbtableIi).padStart(2, "0")}`,
2704
+ ii,
2705
+ isDbchart,
2706
+ isDbmain,
2707
+ isDbquery,
2708
+ rowCount,
2709
+ rowList0,
2710
+ sortCol: 0,
2711
+ sortDir: "asc",
2712
+ sql,
2713
+ title: `table ${dbtableFullname}`
2714
+ };
2715
+ DBTABLE_DICT.set(baton.hashtag, baton);
2716
+ return [
2717
+ baton.dbtableName, baton
2718
+ ];
2719
+ }));
2720
+ tmp = "";
2721
+ dbtableList.forEach(function ({
2722
+ dbtableName,
2723
+ hashtag
2724
+ }) {
2725
+ tmp += (`
2726
+ SELECT '${hashtag}' AS hashtag;
2727
+ PRAGMA table_info(${dbtableName});
2728
+ SELECT COUNT(*) AS rowcount FROM ${dbtableName};
2729
+ `);
2730
+ });
2731
+ tmp = await dbExecAsync({
2732
+ db,
2733
+ sql: tmp
2734
+ });
2735
+ tmp.forEach(function (rowList) {
2736
+ let row0 = rowList[0];
2737
+ if (!row0) {
2738
+ return;
2739
+ }
2740
+ [
2741
+ "cid", "hashtag", "rowcount"
2742
+ ].forEach(function (key) {
2743
+ switch (row0.hasOwnProperty(key) && key) {
2744
+ case "cid":
2745
+ baton.colList = [
2746
+ "rowid",
2747
+ rowList.map(function ({
2748
+ name
2749
+ }) {
2750
+ return name;
2751
+ })
2752
+ ].flat();
2753
+ break;
2754
+ case "hashtag":
2755
+ baton = DBTABLE_DICT.get(row0.hashtag);
2756
+ break;
2757
+ case "rowcount":
2758
+ baton.rowCount = row0.rowcount;
2759
+ break;
2760
+ }
2761
+ });
2762
+ });
2763
+ db.dbtableList = dbtableList;
2764
+ }));
2765
+ // ui-render databases and tables to html
2766
+ document.querySelector("#dbchartList1").innerHTML = "";
2767
+ document.querySelector("#dbqueryList1").innerHTML = "";
2768
+ document.querySelector("#dbtableList1").innerHTML = "";
2769
+ DB_DICT.forEach(function ({
2770
+ dbtableList,
2771
+ isDbchart,
2772
+ isDbquery
2773
+ }) {
2774
+ dbtableList.forEach(function (baton) {
2775
+ // create uitable
2776
+ document.querySelector(
2777
+ isDbchart
2778
+ ? "#dbchartList1"
2779
+ : isDbquery
2780
+ ? "#dbqueryList1"
2781
+ : "#dbtableList1"
2782
+ ).appendChild(uitableCreate(baton));
2783
+ });
2784
+ });
2785
+ // ui-render #tocPanel1
2786
+ html = "";
2787
+ DB_DICT.forEach(function ({
2788
+ dbName,
2789
+ dbtableList,
2790
+ isDbchart,
2791
+ isDbquery
2792
+ }) {
2793
+ html += `<div class="tocTitle">` + (
2794
+ isDbchart
2795
+ ? "chart"
2796
+ : isDbquery
2797
+ ? `query result`
2798
+ : `database ${dbName}`
2799
+ ) + `</div>`;
2800
+ dbtableList.forEach(function ({
2801
+ colList,
2802
+ dbtableFullname,
2803
+ dbtableName,
2804
+ hashtag,
2805
+ ii,
2806
+ rowCount
2807
+ }) {
2808
+ html += `<a class="tocElemA"`;
2809
+ html += ` data-hashtag="${hashtag}"`;
2810
+ html += ` href="#${hashtag}"`;
2811
+ html += (
2812
+ isDbchart
2813
+ ? ` data-dbtype="chart"`
2814
+ : isDbquery
2815
+ ? ` data-dbtype="query"`
2816
+ : ` data-dbtype="table"`
2817
+ );
2818
+ html += (
2819
+ ` title="`
2820
+ + stringHtmlSafe((
2821
+ `right-click for crud operation\n\n`
2822
+ ) + JSON.stringify({
2823
+ dbtableFullname,
2824
+ rowCount,
2825
+ colList //jslint-ignore-line
2826
+ }, undefined, 4))
2827
+ + `"`
2828
+ );
2829
+ html += `>${ii + 1}. `;
2830
+ html += (
2831
+ isDbchart
2832
+ ? "chart"
2833
+ : isDbquery
2834
+ ? "query"
2835
+ : "table"
2836
+ );
2837
+ html += ` ${stringHtmlSafe(dbtableName.slice(1, -1))}</a>\n`;
2838
+ });
2839
+ });
2840
+ document.querySelector("#tocDbList1").innerHTML = html;
2841
+ // restore window.scrollY
2842
+ window.scroll({
2843
+ behavior: "smooth",
2844
+ top: windowScrollY
2845
+ });
2846
+ uitableInitWithinView({});
2847
+ }
2848
+
2849
+ async function uiTryCatch(func, ...argList) {
2850
+ // this function will call <func> in a try-catch-block
2851
+ // that will display any error thrown to user
2852
+ try {
2853
+ UI_LOADING_COUNTER += 1;
2854
+ uiFadeIn(UI_LOADING);
2855
+ await func(...argList);
2856
+ } catch (err) {
2857
+ console.error(err);
2858
+ document.querySelector(
2859
+ "#errorPanel1 .modalContent"
2860
+ ).textContent = err;
2861
+ uiFadeIn(document.querySelector(
2862
+ "#errorPanel1"
2863
+ ));
2864
+ } finally {
2865
+ await waitAsync(500);
2866
+ UI_LOADING_COUNTER -= 1;
2867
+ if (
2868
+ UI_LOADING_COUNTER === 0
2869
+ && UI_LOADING.style.visibility === "visible"
2870
+ ) {
2871
+ uiFadeOut(UI_LOADING);
2872
+ }
2873
+ }
2874
+ }
2875
+
2876
+ async function uichartCreate(baton) {
2877
+ // this function will create xy-line-chart from given sqlite table <baton>
2878
+ // init pre-var
2879
+ let {
2880
+ contentElem,
2881
+ db,
2882
+ dbtableName
2883
+ } = baton;
2884
+ let elemCanvasFlex;
2885
+ let elemLegend;
2886
+ let elemLegendWidth = 256;
2887
+ let elemUichart = contentElem.querySelector(".uichart");
2888
+ let elemUichartHeight = 384;
2889
+ let uichart = await dbExecAsync({
2890
+ db,
2891
+ sql: (`
2892
+ -- table - __chart_options1 - insert
2893
+ DROP TABLE IF EXISTS __chart_options1;
2894
+ CREATE TEMP TABLE __chart_options1 AS
2895
+ SELECT
2896
+ JSON(options) AS options,
2897
+ CASTREALORZERO(options->>'$.xstep') AS xstep,
2898
+ CASTREALORZERO(1.0 / options->>'$.xstep') AS xstep_inv,
2899
+ CASTREALORZERO(options->>'$.ystep') AS ystep,
2900
+ CASTREALORZERO(1.0 / options->>'$.ystep') AS ystep_inv
2901
+ FROM (
2902
+ SELECT options FROM ${dbtableName} WHERE datatype = 'options' LIMIT 1
2903
+ );
2904
+
2905
+ -- table - __chart_series_xy1 - insert
2906
+ DROP TABLE IF EXISTS __chart_series_xy1;
2907
+ CREATE TEMP TABLE __chart_series_xy1 AS
2908
+ SELECT
2909
+ series_index,
2910
+ xx,
2911
+ LAG(xx, 1, NULL) OVER (
2912
+ PARTITION BY series_index ORDER BY xx
2913
+ ) AS xx_lag,
2914
+ LEAD(xx, 1, NULL) OVER (
2915
+ PARTITION BY series_index ORDER BY xx
2916
+ ) AS xx_lead,
2917
+ yy
2918
+ FROM (
2919
+ SELECT
2920
+ series_index,
2921
+ xx,
2922
+ yy
2923
+ FROM (
2924
+ SELECT
2925
+ ROW_NUMBER() OVER (
2926
+ PARTITION BY series_index, xx ORDER BY rowid DESC
2927
+ ) AS rownum,
2928
+ series_index,
2929
+ xx,
2930
+ yy
2931
+ FROM (
2932
+ SELECT
2933
+ ${dbtableName}.rowid,
2934
+ --
2935
+ series_index,
2936
+ IIF(xstep_inv, ROUND(xstep_inv * xx), xx) AS xx,
2937
+ IIF(ystep_inv, ROUND(ystep_inv * yy), yy) AS yy
2938
+ FROM ${dbtableName}
2939
+ JOIN __chart_options1
2940
+ WHERE
2941
+ datatype = 'yy_value'
2942
+ AND xx IS NOT NULL
2943
+ )
2944
+ ORDER BY
2945
+ series_index,
2946
+ xx
2947
+ )
2948
+ WHERE
2949
+ rownum = 1
2950
+ );
2951
+
2952
+ -- table - __chart_series_maxmin1 - insert
2953
+ DROP TABLE IF EXISTS __chart_series_maxmin1;
2954
+ CREATE TEMP TABLE __chart_series_maxmin1 AS
2955
+ SELECT
2956
+ series_index,
2957
+ MAX(xx) AS xdataMax,
2958
+ MIN(xx) AS xdataMin,
2959
+ MAX(yy) AS ydataMax,
2960
+ MIN(yy) AS ydataMin
2961
+ FROM __chart_series_xy1
2962
+ WHERE
2963
+ yy IS NOT NULL
2964
+ GROUP BY series_index;
2965
+
2966
+ -- table - __chart_options1 - select - options
2967
+ SELECT
2968
+ JSON_SET(
2969
+ options,
2970
+ '$.seriesList', JSON(COALESCE(seriesList, '[]')),
2971
+ '$.xdataDxx', COALESCE(xdataDxx, 1),
2972
+ '$.xdataMax', xdataMax,
2973
+ '$.xdataMin', xdataMin,
2974
+ '$.xlabelList', JSON(COALESCE(xlabelList, '[]')),
2975
+ '$.ydataMax', ydataMax,
2976
+ '$.ydataMin', ydataMin
2977
+ ) AS options
2978
+ FROM __chart_options1
2979
+ JOIN (
2980
+ SELECT
2981
+ JSON_GROUP_ARRAY(xx_label) AS xlabelList
2982
+ FROM ${dbtableName}
2983
+ WHERE
2984
+ datatype = 'xx_label'
2985
+ )
2986
+ JOIN (
2987
+ SELECT
2988
+ JSON_GROUP_ARRAY(JSON(JSON_SET(
2989
+ COALESCE(options, '{}'),
2990
+ '$.seriesName', series_label,
2991
+ '$.xdata', JSON(COALESCE(xdata, '[]')),
2992
+ '$.ydata', JSON(COALESCE(ydata, '[]'))
2993
+ ))) AS seriesList
2994
+ FROM (
2995
+ SELECT
2996
+ options,
2997
+ series_index,
2998
+ (
2999
+ ROW_NUMBER() OVER (ORDER BY series_index)
3000
+ || '. '
3001
+ || series_label
3002
+ ) AS series_label
3003
+ FROM ${dbtableName}
3004
+ WHERE
3005
+ datatype = 'series_label'
3006
+ ORDER BY
3007
+ series_index
3008
+ )
3009
+ -- calculate series.xdata, series.ydata
3010
+ LEFT JOIN (
3011
+ SELECT
3012
+ series_index,
3013
+ JSON_GROUP_ARRAY(xx) AS xdata,
3014
+ JSON_GROUP_ARRAY(yy) AS ydata
3015
+ FROM __chart_series_xy1
3016
+ GROUP BY series_index
3017
+ ) USING (series_index)
3018
+ )
3019
+ JOIN (
3020
+ SELECT
3021
+ MAX(xdataMax) AS xdataMax,
3022
+ MIN(xdataMin) AS xdataMin,
3023
+ MAX(ydataMax) AS ydataMax,
3024
+ MIN(ydataMin) AS ydataMin
3025
+ FROM __chart_series_maxmin1
3026
+ )
3027
+ -- calculate uichart.xdataDxx
3028
+ JOIN (
3029
+ SELECT
3030
+ MIN(ABS(xx - xx_prv)) AS xdataDxx
3031
+ FROM (
3032
+ SELECT
3033
+ xx,
3034
+ LAG(xx, 1, NULL) OVER (ORDER BY xx) AS xx_prv
3035
+ FROM (
3036
+ SELECT DISTINCT
3037
+ xx
3038
+ FROM __chart_series_xy1
3039
+ )
3040
+ )
3041
+ );
3042
+ `)
3043
+ });
3044
+ uichart = JSON.parse(uichart[0][0].options);
3045
+ contentElem.querySelector(".uitable").style.display = "none";
3046
+ elemUichart.style.display = "flex";
3047
+ elemUichart.style.height = `${elemUichartHeight}px`;
3048
+ elemUichart.innerHTML = (`
3049
+ <div
3050
+ class="uichartNav"
3051
+ style="height: ${elemUichartHeight}px; width: ${elemLegendWidth}px;"
3052
+ >
3053
+ <button
3054
+ class="uichartAction"
3055
+ data-action="uichartZoomReset"
3056
+ >reset zoom</button>
3057
+ <button
3058
+ class="uichartAction"
3059
+ data-action="uichartSeriesHideAll"
3060
+ >hide all</button>
3061
+ <button
3062
+ class="uichartAction"
3063
+ data-action="uichartSeriesShowAll"
3064
+ >show all</button>
3065
+ <div
3066
+ class="uichartLegend"
3067
+ style="height: ${elemUichartHeight - 64}px;"
3068
+ ></div>
3069
+ </div>
3070
+ <div style="position: relative; margin-left: 16px; width: 16px;">
3071
+ <div class="uichartAxislabel1">${stringHtmlSafe(uichart.yaxisTitle)}</div>
3072
+ </div>
3073
+ <div style="display: flex; flex: 1; flex-direction: column; padding: 5px 0;">
3074
+ <div class="uichartTitle">
3075
+ ${
3076
+ stringHtmlSafe(uichart.title).replace((
3077
+ /\n/g
3078
+ ), "<br>")
3079
+ }
3080
+ </div>
3081
+ <div class="uichartCanvasFlex" style="flex: 1;">
3082
+ <svg
3083
+ class="uichartCanvasFixed"
3084
+ version="1.1"
3085
+ xmlns="http://www.w3.org/2000/svg"
3086
+ >
3087
+ <clipPath class="uichartClip" id="uichartClip${baton.dbtableIi}">
3088
+ <rect fill="none" height="0" width="0" x="0" y="0"></rect>
3089
+ </clipPath>
3090
+ <g class="uichartGridlineList"></g>
3091
+ <g class="uichartAxistickList"></g>
3092
+ <g class="uichartSeriesList"></g>
3093
+ <g class="uichartCrosshairList">
3094
+ <path stroke-width="1" stroke="#333" visibility="hidden"></path>
3095
+ <path stroke-width="1" stroke="#333" visibility="hidden"></path>
3096
+ </g>
3097
+ <g class="uichartTooltip" visibility="hidden">
3098
+ <rect
3099
+ class="uichartTooltipBorder"
3100
+ fill-opacity="0.8000"
3101
+ fill="#fff"
3102
+ rx="5"
3103
+ ry="5"
3104
+ stroke-width="3"
3105
+ x="0"
3106
+ y="0"
3107
+ >
3108
+ </rect>
3109
+ <text class="uichartTooltipText"></text>
3110
+ </g>
3111
+ <g class="uichartMousetrackerList">/g>
3112
+ </svg>
3113
+ </div>
3114
+ <div class="uichartAxislabel0">${stringHtmlSafe(uichart.xaxisTitle)}</div>
3115
+ </div>
3116
+ `);
3117
+ elemCanvasFlex = elemUichart.querySelector(".uichartCanvasFlex");
3118
+ elemLegend = elemUichart.querySelector(".uichartLegend");
3119
+ // init var
3120
+ let ELEM_GRAPH_LINE_WIDTH = 1; //jslint-ignore-line
3121
+ let ELEM_POINT_BORDER_WIDTH = 0;
3122
+ let ELEM_POINT_RADIUS = 4;
3123
+ let canvasHeight = elemCanvasFlex.clientHeight;
3124
+ let canvasWidth = elemCanvasFlex.clientWidth;
3125
+ let counterColor = 1;
3126
+ let counterShape = 1;
3127
+ let elemCanvasFixed = elemCanvasFlex.firstElementChild;
3128
+ let [
3129
+ elemClip,
3130
+ elemGridlineList,
3131
+ elemAxistickList,
3132
+ elemSeriesList,
3133
+ elemCrosshairList,
3134
+ elemTooltip,
3135
+ elemMousetrackerList
3136
+ ] = elemCanvasFixed.children;
3137
+ let [
3138
+ elemTooltipBorder,
3139
+ elemTooltipText
3140
+ ] = elemTooltip.children;
3141
+ let {
3142
+ isBarchart,
3143
+ seriesList,
3144
+ xdataDxx,
3145
+ xdataMax,
3146
+ xdataMin,
3147
+ xlabelList
3148
+ } = uichart;
3149
+ let isResizing = 0;
3150
+ let plotBottom;
3151
+ let plotHeight;
3152
+ let plotLeft;
3153
+ let plotRight;
3154
+ let plotTop;
3155
+ let plotWidth;
3156
+ let plotbarMargin;
3157
+ let plotbarYaxis;
3158
+ let pointHovered;
3159
+ let pointListBarchart = [];
3160
+ let redrawTimer;
3161
+ let seriesColorList = [
3162
+ /*
3163
+ // highcharts-v4
3164
+ "#7cb5ec", // light-blue - highcharts-v4
3165
+ "#434348", // black - highcharts-v4
3166
+ "#90ed7d", // light-green - highcharts-v4
3167
+ "#f7a35c", // orange - highcharts-v4
3168
+ "#8085e9", // lavender-blue - highcharts-v4
3169
+ "#f15c80", // pink - highcharts-v4
3170
+ "#e4d354", // yellow - highcharts-v4
3171
+ "#2b908f", // teal - highcharts-v4
3172
+ "#f45b5b", // orange-red - highcharts-v4
3173
+ "#91e8e1" // aqua - highcharts-v4
3174
+ // highcharts-v3
3175
+ "#2f7ed8", // blue - highcharts-v3
3176
+ "#0d233a", // dark-teal - highcharts-v3
3177
+ "#8bbc21", // olive - highcharts-v3
3178
+ "#910000", // maroon - highcharts-v3
3179
+ "#1aadce", // aqua - highcharts-v3
3180
+ "#492970", // purple - highcharts-v3
3181
+ "#f28f43", // orange - highcharts-v3
3182
+ "#77a1e5", // light-blue - highcharts-v3
3183
+ "#c42525", // red - highcharts-v3
3184
+ "#a6c96a" // light-olive - highcharts-v3
3185
+ // highcharts-v2
3186
+ "#4572a7", // blue - highcharts-v2
3187
+ "#aa4643", // red - highcharts-v2
3188
+ "#89a54e", // olive - highcharts-v2
3189
+ "#80699b", // purple - highcharts-v2
3190
+ "#3d96ae", // aqua - highcharts-v2
3191
+ "#db843d", // orange - highcharts-v2
3192
+ "#92a8cd", // light-navy - highcharts-v2
3193
+ "#a47d7c", // brown - highcharts-v2
3194
+ "#b5ca92" // light-olive - highcharts-v2
3195
+ */
3196
+ //
3197
+ // "#aa4643", // red - highcharts-v2
3198
+ "#a44", // red
3199
+ // "#4572a7", // blue - highcharts-v2
3200
+ "#57b", // blue
3201
+ "#7ecf6d", // light-green - highcharts-v4
3202
+ // "#80699b", // purple - highcharts-v2
3203
+ "#86a", // purple
3204
+ //
3205
+ "#3d96ae", // aqua - highcharts-v2
3206
+ "#f15c80", // pink - highcharts-v4
3207
+ "#8085e9", // lavender-blue - highcharts-v4
3208
+ // "#a47d7c", // brown - highcharts-v2
3209
+ // "#b88", // brown
3210
+ "#89a54e", // olive - highcharts-v2
3211
+ // "#0d233a", // dark-teal - highcharts-v3
3212
+ "#357", // dark-teal
3213
+ "#492970" // purple - highcharts-v3
3214
+ ];
3215
+ let seriesHovered;
3216
+ let seriesShapeList = [
3217
+ // "circle", "diamond", "square", "triangle", "triangle-down"
3218
+ // "circle",
3219
+ "square",
3220
+ "triangle",
3221
+ "diamond",
3222
+ "triangle-down"
3223
+ ];
3224
+ let xaxisMax;
3225
+ let xaxisMin;
3226
+ let xaxistickDict = {};
3227
+ let xpixelToPointDictHovered = [];
3228
+ let xtransOffset;
3229
+ let xtransWidth;
3230
+ let xzoomMax;
3231
+ let xzoomMin;
3232
+ let yaxisMax;
3233
+ let yaxisMin;
3234
+ let yaxistickDict = {};
3235
+ let ytransWidth;
3236
+ function axistickCreate(isXaxis, tickx) {
3237
+ // this function will create <elemGridline>, <elemTick>, <elemTicklabel>
3238
+ // at given <tickx> in <axis>
3239
+ let elemTick;
3240
+ let elemTicklabel;
3241
+ let elemTspan;
3242
+ // create elemTick
3243
+ elemTick = svgAttrSet("path", {
3244
+ fill: "none",
3245
+ stroke: "#33b",
3246
+ "stroke-width": 3
3247
+ });
3248
+ elemTick.isFirstDraw = true;
3249
+ elemTick.isXaxis = isXaxis;
3250
+ // if isXaxis, then draw tick
3251
+ if (isXaxis) {
3252
+ elemAxistickList.appendChild(elemTick);
3253
+ // create elemGridline
3254
+ } else {
3255
+ elemTick.elemGridline = svgAttrSet("path", {
3256
+ fill: "none",
3257
+ stroke: "#999",
3258
+ "stroke-width": 1
3259
+ });
3260
+ elemGridlineList.appendChild(elemTick.elemGridline);
3261
+ }
3262
+ // skip ticklabel, if elemTick is not contained in xlabelList
3263
+ if (
3264
+ isXaxis
3265
+ && xlabelList.length
3266
+ && !xlabelList.hasOwnProperty(tickx - 1)
3267
+ ) {
3268
+ return elemTick;
3269
+ }
3270
+ // create ticklabel
3271
+ elemTspan = svgAttrSet("tspan");
3272
+ elemTspan.textContent = (function () {
3273
+ let num;
3274
+ let numDigitList;
3275
+ let xOrY = (
3276
+ isXaxis
3277
+ ? "x"
3278
+ : "y"
3279
+ );
3280
+ num = numberFormat({
3281
+ convert: uichart[xOrY + "valueConvert"],
3282
+ modeTick: true,
3283
+ num: (
3284
+ isXaxis
3285
+ ? xlabelList[tickx - 1] ?? tickx
3286
+ : tickx
3287
+ ),
3288
+ prefix: uichart[xOrY + "valuePrefix"],
3289
+ step: uichart[xOrY + "step"],
3290
+ suffix: uichart[xOrY + "valueSuffix"]
3291
+ }).slice(0, 16);
3292
+ // number already formatted
3293
+ if (typeof num !== "number") {
3294
+ return num;
3295
+ }
3296
+ // small number
3297
+ if (Math.abs(num) < 1000) {
3298
+ return num.toLocaleString();
3299
+ }
3300
+ // large number
3301
+ num = Math.round(num).toLocaleString();
3302
+ numDigitList = num.split(
3303
+ /[^+\-0-9]/
3304
+ );
3305
+ switch (numDigitList.length) {
3306
+ // kilo
3307
+ case 2:
3308
+ return numDigitList[0] + "k";
3309
+ // mega
3310
+ case 3:
3311
+ return numDigitList[0] + "M";
3312
+ // giga
3313
+ case 4:
3314
+ return numDigitList[0] + "G";
3315
+ // tera
3316
+ case 5:
3317
+ return numDigitList[0] + "T";
3318
+ // peta
3319
+ case 6:
3320
+ return numDigitList[0] + "P";
3321
+ // exa
3322
+ case 7:
3323
+ return numDigitList[0] + "E";
3324
+ default:
3325
+ return num;
3326
+ }
3327
+ }());
3328
+ elemTicklabel = svgAttrSet("text", {
3329
+ "text-anchor": (
3330
+ isXaxis
3331
+ ? "middle"
3332
+ : "end"
3333
+ )
3334
+ });
3335
+ elemTick.elemTicklabel = elemTicklabel;
3336
+ elemTicklabel.fx_rotate = isXaxis;
3337
+ elemTicklabel.appendChild(elemTspan);
3338
+ elemAxistickList.appendChild(elemTicklabel);
3339
+ return elemTick;
3340
+ }
3341
+ function floatCorrect(num) {
3342
+ // this function will correct float-error in <num>
3343
+ return parseFloat(
3344
+ num.toPrecision(12)
3345
+ );
3346
+ }
3347
+ function numberFormat({
3348
+ convert,
3349
+ modeTick,
3350
+ num,
3351
+ prefix,
3352
+ step,
3353
+ suffix
3354
+ }) {
3355
+ // this function will format <num>
3356
+ if (step && typeof num === "number") {
3357
+ num *= step;
3358
+ }
3359
+ switch (convert) {
3360
+ case "juliandayToDate":
3361
+ num = new Date((num - 2440587.5) * 86400 * 1000);
3362
+ // num = num.toUTCString().slice(0, 16);
3363
+ num = (
3364
+ modeTick
3365
+ ? num.toUTCString().slice(5, 16)
3366
+ : num.toUTCString().slice(0, 16)
3367
+ );
3368
+ break;
3369
+ case "unixepochToTimelocal":
3370
+ num = new Date(num * 1000);
3371
+ num = (
3372
+ modeTick
3373
+ ? num.toLocaleTimeString()
3374
+ : num.toLocaleString()
3375
+ );
3376
+ break;
3377
+ case "unixepochToTimeutc":
3378
+ // num = new Date(num * 1000).toISOString().slice(11, 19) + "Z";
3379
+ num = new Date(num * 1000);
3380
+ num = (
3381
+ modeTick
3382
+ ? num.toUTCString().slice(17)
3383
+ : num.toUTCString()
3384
+ );
3385
+ break;
3386
+ }
3387
+ if (prefix) {
3388
+ num = prefix + num;
3389
+ }
3390
+ if (suffix) {
3391
+ num += suffix;
3392
+ }
3393
+ return String(num);
3394
+ }
3395
+ function onCanvasZoom(evt) {
3396
+ // this function will zoom/un-zoom at current mouse-location on canvas-area
3397
+ let xmid = 0.5;
3398
+ let xscale = 1.2500; // zoom-out
3399
+ evt.preventDefault();
3400
+ evt.stopPropagation();
3401
+ if (!evt.modeDebounce) {
3402
+ debounce("onCanvasZoom", onCanvasZoom, Object.assign(evt, {
3403
+ modeDebounce: true
3404
+ }));
3405
+ return;
3406
+ }
3407
+ if (evt.deltaY < 0) {
3408
+ xmid = (
3409
+ evt.pageX
3410
+ - elemCanvasFixed.getBoundingClientRect().left
3411
+ - window.scrollX
3412
+ + document.documentElement.clientLeft
3413
+ - plotLeft
3414
+ ) / plotWidth;
3415
+ xscale = 0.8000; // zoom-in
3416
+ }
3417
+ xmid += 0.5 * (xmid - 0.4000);
3418
+ xmid = xaxisMin + xmid * (xaxisMax - xaxisMin);
3419
+ xzoomMax = xmid + xscale * (xaxisMax - xmid);
3420
+ xzoomMin = xmid + xscale * (xaxisMin - xmid);
3421
+ // uichartRedraw - uichartZoom
3422
+ uichartRedraw();
3423
+ }
3424
+ function onPointHover(evt) {
3425
+ // this function will handle <evt> when mouse hover over point
3426
+ let mouseX;
3427
+ let pointObj;
3428
+ let rect;
3429
+ let tooltipBbox;
3430
+ let tooltipHeight;
3431
+ let tooltipMargin = 32;
3432
+ let tooltipWidth;
3433
+ let tooltipX;
3434
+ let tooltipY;
3435
+ // get mouse-position
3436
+ rect = elemCanvasFixed.getBoundingClientRect();
3437
+ mouseX = Math.round(evt.pageX - rect.left - window.scrollX - plotLeft);
3438
+ // if mouse is outside canvas-area, then return
3439
+ if (!(0 <= mouseX && mouseX <= plotWidth)) {
3440
+ return;
3441
+ }
3442
+ // get closest-series-point to mouse as pointHovered
3443
+ pointObj = xpixelToPointDictHovered[mouseX];
3444
+ if (
3445
+ !pointObj
3446
+ || pointObj === pointHovered
3447
+ || pointObj.series.isHidden
3448
+ ) {
3449
+ return;
3450
+ }
3451
+ pointHovered = pointObj;
3452
+ // redraw seriesHovered
3453
+ onSeriesHover(pointObj.series, true);
3454
+ // redraw tooltip around pointHovered
3455
+ let { //jslint-ignore-line
3456
+ pointX,
3457
+ pointY,
3458
+ xval,
3459
+ yval
3460
+ } = pointHovered;
3461
+ let xlabel;
3462
+ let ylabel;
3463
+ if (redrawTimer || pointY === undefined) {
3464
+ return;
3465
+ }
3466
+ xlabel = numberFormat({
3467
+ convert: uichart.xvalueConvert,
3468
+ num: xlabelList[xval - 1] ?? xval,
3469
+ prefix: uichart.xvaluePrefix,
3470
+ step: uichart.xstep,
3471
+ suffix: uichart.xvalueSuffix
3472
+ });
3473
+ ylabel = numberFormat({
3474
+ convert: uichart.yvalueConvert,
3475
+ num: yval,
3476
+ prefix: uichart.yvaluePrefix,
3477
+ step: uichart.ystep,
3478
+ suffix: uichart.yvalueSuffix
3479
+ });
3480
+ // update elemTooltipText
3481
+ elemTooltip.setAttribute("visibility", "visible");
3482
+ elemTooltipText.innerHTML = (`
3483
+ <tspan dy="17" x="6">${stringHtmlSafe(seriesHovered.seriesName)}</tspan>
3484
+ <tspan dy="17" x="6">x: ${stringHtmlSafe(xlabel)}</tspan>
3485
+ <tspan
3486
+ dy="19"
3487
+ style="font-size: 14px; font-weight: bold;"
3488
+ x="6"
3489
+ >y: ${stringHtmlSafe(ylabel)}</tspan>
3490
+ `);
3491
+ // update elemTooltipBorder after text-update
3492
+ tooltipBbox = elemTooltipText.getBBox();
3493
+ tooltipWidth = tooltipBbox.width + 10;
3494
+ tooltipHeight = tooltipBbox.height + 10;
3495
+ svgAttrSet(elemTooltipBorder, {
3496
+ height: tooltipHeight,
3497
+ stroke: seriesHovered.seriesColor,
3498
+ width: tooltipWidth
3499
+ });
3500
+ // calculate tooltipX
3501
+ tooltipX = pointX + plotLeft - 0.5 * tooltipWidth;
3502
+ tooltipX = Math.max(tooltipX, plotLeft + tooltipMargin);
3503
+ tooltipX = Math.min(
3504
+ tooltipX,
3505
+ canvasWidth - tooltipWidth - tooltipMargin
3506
+ );
3507
+ // calculate tooltipY
3508
+ tooltipY = pointY + plotTop - tooltipHeight - tooltipMargin;
3509
+ if (tooltipY < plotTop) {
3510
+ tooltipY = pointY + plotTop + tooltipMargin;
3511
+ }
3512
+ // animate-move tooltip to tooltipX, tooltipY
3513
+ svgAnimate(elemTooltip, {
3514
+ translateX: tooltipX,
3515
+ translateY: tooltipY
3516
+ }, "easeout");
3517
+ // animate-move crosshair
3518
+ [
3519
+ true, false
3520
+ ].forEach(function (isXaxis, ii) {
3521
+ let d;
3522
+ if (isXaxis) {
3523
+ d = xaxisTranslate(xval) + CRISPX;
3524
+ d = `M ${d} 0 L ${d} ${canvasHeight}`;
3525
+ } else {
3526
+ d = yaxisTranslate(yval) + CRISPY;
3527
+ d = `M 0 ${d} L ${canvasWidth} ${d}`;
3528
+ }
3529
+ svgAnimate(elemCrosshairList.children[ii], {
3530
+ d,
3531
+ visibility: "visible"
3532
+ }, "easeout");
3533
+ });
3534
+ }
3535
+ function onPointUnhover() {
3536
+ // this function will handle <evt> when mouse un-hover from point
3537
+ onSeriesUnhover();
3538
+ pointHovered = undefined;
3539
+ // hide elemcrosshairList, elemTooltip
3540
+ elemCrosshairList.children[0].setAttribute("visibility", "hidden");
3541
+ elemCrosshairList.children[1].setAttribute("visibility", "hidden");
3542
+ elemTooltip.setAttribute("visibility", "hidden");
3543
+ }
3544
+ function onSeriesHover(evt, scrollTo) {
3545
+ // this function will handle <evt> when mouse hover over series
3546
+ let series = (
3547
+ evt.target
3548
+ ? seriesList[
3549
+ evt.target.closest("[data-series-ii]")?.dataset.seriesIi
3550
+ ]
3551
+ : evt
3552
+ );
3553
+ let seriesColor;
3554
+ if (!series || series.isHidden) {
3555
+ onSeriesUnhover();
3556
+ return;
3557
+ }
3558
+ // un-hover previous seriesHovered
3559
+ if (series !== seriesHovered) {
3560
+ onSeriesUnhover();
3561
+ }
3562
+ seriesHovered = series;
3563
+ // darken series-color
3564
+ if (isBarchart) {
3565
+ seriesColor = series.seriesColor.replace((
3566
+ /^#(..)(..)(..)$/
3567
+ ), function (ignore, rr, gg, bb) {
3568
+ return (
3569
+ "rgb("
3570
+ + Math.round(0.5 * parseInt(rr, 16)) + ","
3571
+ + Math.round(0.5 * parseInt(gg, 16)) + ","
3572
+ + Math.round(0.5 * parseInt(bb, 16))
3573
+ + ")"
3574
+ );
3575
+ });
3576
+ seriesHovered.pointListSeries.forEach(function (pointObj) {
3577
+ pointObj.elemPoint.setAttribute("fill", seriesColor);
3578
+ });
3579
+ }
3580
+ // thicken series-line-width
3581
+ if (!isBarchart) {
3582
+ xpixelToPointDictHovered = series.xpixelToPointDict;
3583
+ svgAttrSet(seriesHovered.elemGraph, {
3584
+ "stroke-width": ELEM_GRAPH_LINE_WIDTH + 2
3585
+ });
3586
+ }
3587
+ Array.from(elemLegend.children).forEach(function (elem, seriesIi) {
3588
+ if (seriesIi !== seriesHovered.seriesIi) {
3589
+ elem.style.background = "none";
3590
+ return;
3591
+ }
3592
+ // hover series in legend
3593
+ elem.style.background = "#ccc";
3594
+ // scroll to series in legend
3595
+ if (scrollTo) {
3596
+ debounce("onSeriesScroll", function () {
3597
+ elemLegend.scroll({
3598
+ behavior: "smooth",
3599
+ top: elem.offsetTop - elemLegend.offsetHeight
3600
+ });
3601
+ });
3602
+ }
3603
+ });
3604
+ }
3605
+ function onSeriesHoveredHide() {
3606
+ // this function will handle <evt> to hide hovered-series
3607
+ if (seriesHovered) {
3608
+ uichartSeriesHideOrShow(seriesHovered, true);
3609
+ uichartRedraw();
3610
+ return;
3611
+ }
3612
+ }
3613
+ function onSeriesUnhover() {
3614
+ // this function will handle <evt> when mouse un-hover from series
3615
+ let seriesColor;
3616
+ if (!seriesHovered) {
3617
+ return;
3618
+ }
3619
+ // un-darken series-color
3620
+ if (isBarchart) {
3621
+ seriesColor = seriesHovered.seriesColor;
3622
+ seriesHovered.pointListSeries.forEach(function (pointObj) {
3623
+ pointObj.elemPoint.setAttribute("fill", seriesColor);
3624
+ });
3625
+ }
3626
+ // un-thicken series-line-width
3627
+ if (!isBarchart) {
3628
+ svgAttrSet(seriesHovered.elemGraph, {
3629
+ "stroke-width": ELEM_GRAPH_LINE_WIDTH
3630
+ });
3631
+ }
3632
+ // un-hover series in legend
3633
+ elemLegend.children[seriesHovered.seriesIi].style.background = "none";
3634
+ seriesHovered = undefined;
3635
+ }
3636
+ async function onUichartAction(evt) {
3637
+ // this function will handle uichart event <evt>
3638
+ let action;
3639
+ let modeHide;
3640
+ let target;
3641
+ evt.preventDefault();
3642
+ evt.stopPropagation();
3643
+ if (!evt.modeDebounce) {
3644
+ debounce("onUichartAction", onUichartAction, Object.assign(evt, {
3645
+ modeDebounce: true
3646
+ }));
3647
+ return;
3648
+ }
3649
+ target = evt.target.closest("[data-action]");
3650
+ if (!target) {
3651
+ return;
3652
+ }
3653
+ action = target.dataset.action;
3654
+ switch (action) {
3655
+ case "uichartSeriesHideAll":
3656
+ case "uichartSeriesShowAll":
3657
+ uiFadeIn(baton.elemLoading);
3658
+ await waitAsync(50);
3659
+ modeHide = action === "uichartSeriesHideAll";
3660
+ // hide or show series
3661
+ seriesList.forEach(function (series) {
3662
+ uichartSeriesHideOrShow(series, modeHide);
3663
+ });
3664
+ uichartRedraw();
3665
+ await waitAsync(200);
3666
+ uiFadeOut(baton.elemLoading);
3667
+ return;
3668
+ case "uichartSeriesHideOrShow":
3669
+ modeHide = target.dataset.hidden !== "1";
3670
+ // hide or show series
3671
+ uichartSeriesHideOrShow(
3672
+ seriesList[target.dataset.seriesIi],
3673
+ modeHide
3674
+ );
3675
+ uichartRedraw();
3676
+ return;
3677
+ case "uichartZoomReset":
3678
+ uiFadeIn(baton.elemLoading);
3679
+ await waitAsync(50);
3680
+ xzoomMax = undefined;
3681
+ xzoomMin = undefined;
3682
+ // uichartRedraw - uichartZoomReset
3683
+ uichartRedraw();
3684
+ await waitAsync(200);
3685
+ uiFadeOut(baton.elemLoading);
3686
+ return;
3687
+ }
3688
+ }
3689
+ // uichartRedraw - start
3690
+ function uichartRedraw(modeDebounce) {
3691
+ // this function will redraw <uichart>
3692
+ if (!modeDebounce) {
3693
+ clearTimeout(redrawTimer);
3694
+ redrawTimer = setTimeout(uichartRedraw, 0, true);
3695
+ return;
3696
+ }
3697
+ redrawTimer = undefined;
3698
+ //
3699
+ // calculate plotBottom, plotHeight, plotTop
3700
+ //
3701
+ plotTop = 16;
3702
+ plotHeight = canvasHeight - plotTop - 32;
3703
+ plotBottom = canvasHeight - plotHeight - plotTop;
3704
+ //
3705
+ // pre-calculate plotLeft, plotRight, plotWidth
3706
+ //
3707
+ plotLeft = 32;
3708
+ plotWidth = canvasWidth - plotLeft - 32;
3709
+ plotRight = canvasWidth - plotWidth - plotLeft;
3710
+ //
3711
+ // calculate axisMax, axisMin - start
3712
+ //
3713
+ [
3714
+ true, false
3715
+ ].forEach(function (isXaxis) {
3716
+ let axisMax;
3717
+ let axisMin;
3718
+ let tickExponent;
3719
+ let tickInterval;
3720
+ let tickMultiple;
3721
+ let tickx;
3722
+ let tickxDict;
3723
+ let tickxList;
3724
+ let tickxMax;
3725
+ let tickxMin;
3726
+ let tickxPrv;
3727
+ if (isResizing) {
3728
+ return;
3729
+ }
3730
+ //
3731
+ // if isXaxis, then calculate xaxismax, xaxismin
3732
+ //
3733
+ if (isXaxis) {
3734
+ axisMax = Math.min(xdataMax, xzoomMax ?? xdataMax);
3735
+ axisMin = Math.max(xdataMin, xzoomMin ?? xdataMin);
3736
+ // ensure xaxisMax - xaxisMin >= xdataDxx
3737
+ if (axisMax - axisMin < 4 * xdataDxx) {
3738
+ axisMax = Math.min(xdataMax, axisMin + 4 * xdataDxx);
3739
+ axisMin = Math.max(xdataMin, axisMax - 4 * xdataDxx);
3740
+ }
3741
+ // pad axisMax, axisMin
3742
+ axisMax += 0.02 * (axisMax - axisMin);
3743
+ axisMin -= 0.02 * (axisMax - axisMin);
3744
+ }
3745
+ //
3746
+ // if not isXaxis, then calculate yaxisMax, yaxisMin
3747
+ //
3748
+ // if isBarchart, then make sure yaxis is visible
3749
+ if (!isXaxis && isBarchart) {
3750
+ axisMax = 0;
3751
+ axisMin = 0;
3752
+ }
3753
+ seriesList.forEach(function (series) {
3754
+ let ii;
3755
+ let nn;
3756
+ let xcropEnd;
3757
+ let xcropStart;
3758
+ let {
3759
+ xdata,
3760
+ ydata
3761
+ } = series;
3762
+ let yval;
3763
+ if (isXaxis || series.isHidden) {
3764
+ return;
3765
+ }
3766
+ nn = xdata.length;
3767
+ // calculate xcropEnd, xcropStart
3768
+ // xdata inside xcrop-range
3769
+ if (xaxisMin <= xdata[0] && xdata[nn - 1] <= xaxisMax) {
3770
+ xcropEnd = nn;
3771
+ xcropStart = 0;
3772
+ // xdata outside xcrop-range
3773
+ } else if (xdata[nn - 1] < xaxisMin || xaxisMax < xdata[0]) {
3774
+ xcropEnd = 0;
3775
+ xcropStart = 0;
3776
+ // init xcropStart
3777
+ } else {
3778
+ ii = 0;
3779
+ while (ii < nn && xdata[ii] < xaxisMin) {
3780
+ ii += 1;
3781
+ }
3782
+ xcropStart = Math.max(0, ii - 1);
3783
+ // init xcropEnd
3784
+ while (ii < nn && xdata[ii] < xaxisMax) {
3785
+ ii += 1;
3786
+ }
3787
+ xcropEnd = ii;
3788
+ }
3789
+ // calculate yaxisMax, yaxisMin
3790
+ ii = xcropStart;
3791
+ while (ii < xcropEnd) {
3792
+ yval = ydata[ii];
3793
+ if (!Number.isNaN(yval)) {
3794
+ axisMax = Math.max(axisMax ?? yval, yval);
3795
+ axisMin = Math.min(axisMin ?? yval, yval);
3796
+ }
3797
+ ii += 1;
3798
+ }
3799
+ });
3800
+ axisMax = axisMax ?? 0;
3801
+ axisMin = axisMin ?? 0;
3802
+ axisMax += 0.01 * (axisMax - axisMin);
3803
+ axisMin -= 0.01 * (axisMax - axisMin);
3804
+ if (axisMax === axisMin) {
3805
+ axisMax = Math.max(0.99 * axisMax, 1.01 * axisMax);
3806
+ axisMin = Math.min(0.99 * axisMin, 1.01 * axisMin);
3807
+ }
3808
+ if (axisMax === axisMin) {
3809
+ axisMax += 1;
3810
+ axisMin -= 1;
3811
+ }
3812
+ //
3813
+ // add/remove elemGridline, elemTick, elemTicklabel
3814
+ //
3815
+ // calculate tickInterval
3816
+ tickInterval = (
3817
+ isXaxis
3818
+ ? (axisMax - axisMin) * 100 / plotWidth
3819
+ : (axisMax - axisMin) * 72 / plotHeight
3820
+ ) || 1;
3821
+ // normalize tickInterval to within 0...10
3822
+ tickExponent = Math.pow(
3823
+ 10,
3824
+ Math.floor(Math.log(tickInterval) / Math.LN10)
3825
+ );
3826
+ tickInterval = tickInterval / tickExponent;
3827
+ // round tickInterval to 1, 2, 5, or 10
3828
+ Array.from([
3829
+ 1, 2, 5, 10
3830
+ ]).some(function (multiple, ii, list) {
3831
+ tickMultiple = multiple;
3832
+ return (
3833
+ 2 * tickInterval
3834
+ <=
3835
+ tickMultiple + (list[ii + 1] || tickMultiple)
3836
+ );
3837
+ });
3838
+ // after rounding, un-normalize tickInterval from within 0...10
3839
+ tickInterval = tickMultiple * tickExponent;
3840
+ // calculate tickxList from tickInterval
3841
+ tickx = floatCorrect(
3842
+ Math.floor(axisMin / tickInterval) * tickInterval
3843
+ );
3844
+ tickxList = [];
3845
+ tickxMax = floatCorrect(
3846
+ Math.ceil(axisMax / tickInterval) * tickInterval
3847
+ );
3848
+ while (true) {
3849
+ tickxList.push(tickx);
3850
+ tickx = floatCorrect(tickx + tickInterval);
3851
+ if (tickx > tickxMax || tickx === tickxPrv) {
3852
+ break;
3853
+ }
3854
+ tickxPrv = tickx;
3855
+ }
3856
+ // sync tickxMax, tickxMin with axisMax, axisMin
3857
+ tickxMax = tickxList[tickxList.length - 1];
3858
+ tickxMin = tickxList[0];
3859
+ if (isXaxis) {
3860
+ if (tickxMax > axisMax + 0.5 * xdataDxx) {
3861
+ tickxList.pop();
3862
+ }
3863
+ if (tickxMin < axisMin - 0.5 * xdataDxx) {
3864
+ tickxList.shift();
3865
+ }
3866
+ } else {
3867
+ axisMax = tickxMax;
3868
+ axisMin = tickxMin;
3869
+ }
3870
+ // add elemGridline, elemTick, elemTicklabel
3871
+ tickxDict = (
3872
+ isXaxis
3873
+ ? xaxistickDict
3874
+ : yaxistickDict
3875
+ );
3876
+ tickxList.forEach(function (tickx) {
3877
+ tickxDict[tickx] = tickxDict[tickx] || axistickCreate(
3878
+ isXaxis,
3879
+ tickx
3880
+ );
3881
+ });
3882
+ // remove elemGridline, elemTick, elemTicklabel
3883
+ Object.entries(tickxDict).forEach(function ([
3884
+ tickx, elemTick
3885
+ ]) {
3886
+ if (
3887
+ elemTick.isXaxis !== isXaxis
3888
+ || tickxList.indexOf(Number(tickx)) !== -1
3889
+ ) {
3890
+ return;
3891
+ }
3892
+ elemTick.remove();
3893
+ if (elemTick.elemGridline) {
3894
+ elemTick.elemGridline.remove();
3895
+ delete elemTick.elemGridline;
3896
+ }
3897
+ if (elemTick.elemTicklabel) {
3898
+ elemTick.elemTicklabel.remove();
3899
+ delete elemTick.elemTicklabel;
3900
+ }
3901
+ delete tickxDict[tickx];
3902
+ });
3903
+ //
3904
+ // re-calculate plotLeft, plotRight, plotWidth
3905
+ //
3906
+ if (!isXaxis) {
3907
+ plotLeft = 0;
3908
+ tickxList.forEach(function (tickx) {
3909
+ plotLeft = Math.max(
3910
+ plotLeft,
3911
+ tickxDict[tickx].elemTicklabel.getBBox().width
3912
+ );
3913
+ });
3914
+ plotLeft = Math.round(16 + plotLeft);
3915
+ plotWidth = Math.round(canvasWidth - plotLeft - 32);
3916
+ plotRight = Math.round(canvasWidth - plotWidth - plotLeft);
3917
+ }
3918
+ // save axisMax, axisMin
3919
+ if (isXaxis) {
3920
+ xaxisMax = axisMax;
3921
+ xaxisMin = axisMin;
3922
+ } else {
3923
+ yaxisMax = axisMax;
3924
+ yaxisMin = axisMin;
3925
+ }
3926
+ });
3927
+ //
3928
+ // calculate axisMax, axisMin - end
3929
+ //
3930
+ //
3931
+ // calculate xtransOffset, xtransWidth, ytransWidth
3932
+ //
3933
+ xtransWidth = plotWidth / (xaxisMax - xaxisMin + xdataDxx);
3934
+ xtransOffset = xtransWidth * 0.5 * xdataDxx;
3935
+ ytransWidth = plotHeight / (yaxisMax - yaxisMin);
3936
+ //
3937
+ // calculate plotbarMargin, plotbarWidth, plotbarYaxis
3938
+ plotbarMargin = Math.max(2, 0.24 * xtransWidth * xdataDxx);
3939
+ plotbarYaxis = yaxisTranslate(0);
3940
+ //
3941
+ // update crop-area for
3942
+ // elemCrosshairList, elemMousetrackerList, elemSeriesList
3943
+ //
3944
+ svgAttrSet(elemClip.firstElementChild, {
3945
+ height: plotHeight,
3946
+ width: plotWidth
3947
+ });
3948
+ [
3949
+ elemCrosshairList, elemMousetrackerList, elemSeriesList
3950
+ ].forEach(function (elem) {
3951
+ svgAttrSet(elem, {
3952
+ "clip-path": `url(#${elemClip.id})`,
3953
+ transform: `translate(${plotLeft},${plotTop})`
3954
+ });
3955
+ });
3956
+ //
3957
+ // redraw elemGridline, elemTick, elemTicklabel - start
3958
+ //
3959
+ [
3960
+ Object.entries(xaxistickDict),
3961
+ Object.entries(yaxistickDict)
3962
+ ].flat().forEach(function ([
3963
+ tickx, elemTick
3964
+ ]) {
3965
+ let {
3966
+ elemGridline,
3967
+ elemTicklabel,
3968
+ isXaxis
3969
+ } = elemTick;
3970
+ let xx;
3971
+ let yy;
3972
+ // init xx, yy for first-draw
3973
+ if (elemTick.isFirstDraw) {
3974
+ xx = plotLeft + 0.5 * plotWidth;
3975
+ yy = canvasHeight - plotBottom - 0.5 * plotHeight;
3976
+ if (isXaxis) {
3977
+ yy = canvasHeight + 0.125 * plotHeight;
3978
+ } else {
3979
+ xx = -0.125 * plotWidth;
3980
+ }
3981
+ delete elemTick.isFirstDraw;
3982
+ svgAttrSet(elemTick, {
3983
+ d: `M ${xx} ${yy} L ${xx} ${yy + 5}`
3984
+ });
3985
+ if (elemGridline) {
3986
+ svgAttrSet(elemGridline, {
3987
+ d: (
3988
+ `M ${plotLeft} ${yy}`
3989
+ + ` L ${canvasWidth - plotRight} ${yy}`
3990
+ )
3991
+ });
3992
+ }
3993
+ if (elemTicklabel) {
3994
+ svgAttrSet(elemTicklabel, {
3995
+ x: xx,
3996
+ y: yy
3997
+ });
3998
+ }
3999
+ }
4000
+ // init xx, yy for animation
4001
+ if (isXaxis) {
4002
+ xx = plotLeft + xaxisTranslate(tickx);
4003
+ yy = canvasHeight - plotBottom;
4004
+ } else {
4005
+ xx = plotLeft;
4006
+ yy = plotTop + yaxisTranslate(tickx);
4007
+ }
4008
+ // redraw elemTick
4009
+ if (isXaxis) {
4010
+ svgAnimate(elemTick, {
4011
+ d: `M ${xx - 0.5} ${yy + 0.5} L ${xx - 0.5} ${yy + 5.5}`
4012
+ });
4013
+ }
4014
+ // redraw elemTicklabel
4015
+ if (elemTicklabel) {
4016
+ svgAnimate(elemTicklabel, (
4017
+ isXaxis
4018
+ ? {
4019
+ x: xx,
4020
+ y: yy + 18
4021
+ }
4022
+ : {
4023
+ x: xx - 8,
4024
+ y: yy + 14 * 0.9 - 0.5 * elemTicklabel.getBBox().height
4025
+ }
4026
+ ));
4027
+ }
4028
+ // redraw elemGridline
4029
+ if (elemGridline) {
4030
+ svgAnimate(elemGridline, {
4031
+ d: (
4032
+ `M ${plotLeft} ${yy + CRISPY}`
4033
+ + ` L ${canvasWidth - plotRight} ${yy + CRISPY}`
4034
+ )
4035
+ });
4036
+ }
4037
+ });
4038
+ //
4039
+ // redraw elemGridline, elemTick, elemTicklabel - end
4040
+ //
4041
+ //
4042
+ // redraw seriesList - start
4043
+ //
4044
+ seriesList.forEach(function (series) {
4045
+ let {
4046
+ elemGraph,
4047
+ elemGraphtracker,
4048
+ pointListSeries
4049
+ } = series;
4050
+ let elemGraphD = "";
4051
+ let pointXPrv = 0;
4052
+ let pointYPrv = 0;
4053
+ if (series.isHidden) {
4054
+ return;
4055
+ }
4056
+ // redraw pointListSeries
4057
+ pointListSeries.forEach(function (pointObj, ii) {
4058
+ let barY;
4059
+ let {
4060
+ elemPoint,
4061
+ xval,
4062
+ yval
4063
+ } = pointObj;
4064
+ let pointX;
4065
+ let pointY;
4066
+ // pixelate xval, yval to pointX, pointY
4067
+ pointX = xaxisTranslate(xval);
4068
+ pointY = (
4069
+ yval === undefined
4070
+ ? undefined
4071
+ : yaxisTranslate(yval)
4072
+ );
4073
+ // save pointX, pointY
4074
+ pointObj.pointX = pointX;
4075
+ pointObj.pointY = pointY;
4076
+ if (pointY === undefined) {
4077
+ return;
4078
+ }
4079
+ if (isBarchart) {
4080
+ // if isBarchart, then redraw point as bar
4081
+ barY = Math.min(pointY, plotbarYaxis);
4082
+ svgAnimate(elemPoint, {
4083
+ height: Math.max(
4084
+ 4,
4085
+ Math.max(pointY, plotbarYaxis) - barY
4086
+ ),
4087
+ width: 2 * plotbarMargin,
4088
+ x: pointX - plotbarMargin,
4089
+ y: barY
4090
+ });
4091
+ return;
4092
+ }
4093
+ if (
4094
+ 128 <= (
4095
+ Math.pow(pointX - pointXPrv, 2)
4096
+ + Math.pow(pointY - pointYPrv, 2)
4097
+ )
4098
+ || ii + 1 === pointListSeries.length
4099
+ ) {
4100
+ pointXPrv = pointX;
4101
+ pointYPrv = pointY;
4102
+ }
4103
+ // if not isBarchart, then redraw point as shape
4104
+ svgAnimate(elemPoint, {
4105
+ height: 2 * ELEM_POINT_RADIUS,
4106
+ // visibility: (
4107
+ // pointX === pointXPrv
4108
+ // ? "inherit"
4109
+ // : "hidden"
4110
+ // ),
4111
+ width: 2 * ELEM_POINT_RADIUS,
4112
+ x: pointXPrv - ELEM_POINT_RADIUS,
4113
+ y: pointYPrv - ELEM_POINT_RADIUS
4114
+ });
4115
+ // if not isBarchart, then calculate elemGraphD
4116
+ elemGraphD += (
4117
+ (
4118
+ elemGraphD === ""
4119
+ // moveto
4120
+ ? "M"
4121
+ // lineto
4122
+ : "L"
4123
+ )
4124
+ // + ` ${pointX} ${pointY} `
4125
+ + ` ${pointXPrv} ${pointYPrv} `
4126
+ );
4127
+ });
4128
+ // if not isBarchart, then redraw elemGraph
4129
+ if (!isBarchart) {
4130
+ elemGraphD = elemGraphD.trim();
4131
+ svgAnimate(elemGraph, {
4132
+ d: elemGraphD
4133
+ });
4134
+ elemGraphtracker.setAttribute("d", elemGraphD);
4135
+ }
4136
+ // calculate pixelToPointDict
4137
+ series.xpixelToPointDict = xpixelToPointDictCreate(
4138
+ pointListSeries
4139
+ );
4140
+ });
4141
+ // calculate xpixelToPointDictHovered
4142
+ if (isBarchart) {
4143
+ xpixelToPointDictHovered = xpixelToPointDictCreate(
4144
+ pointListBarchart
4145
+ );
4146
+ }
4147
+ // reset seriesHovered
4148
+ seriesList.forEach(function (series) {
4149
+ if (!seriesHovered || seriesHovered.isHidden) {
4150
+ onSeriesHover(series);
4151
+ onSeriesUnhover();
4152
+ }
4153
+ });
4154
+ //
4155
+ // redraw seriesList - end
4156
+ //
4157
+ }
4158
+ // uichartRedraw - end
4159
+ function uichartResize() {
4160
+ // this function will resize <uichart>
4161
+ // temporarily remove elemCanvasFixed
4162
+ // to calculate canvasHeight, canvasWidth
4163
+ elemCanvasFixed.remove();
4164
+ canvasHeight = Math.round(elemCanvasFlex.clientHeight);
4165
+ canvasWidth = Math.round(elemCanvasFlex.clientWidth);
4166
+ // restore elemCanvasFixed
4167
+ elemCanvasFlex.appendChild(elemCanvasFixed);
4168
+ // increment / decrement isResizing
4169
+ isResizing += 1;
4170
+ setTimeout(function () {
4171
+ isResizing -= 1;
4172
+ }, UI_ANIMATE_DURATION);
4173
+ svgAttrSet(elemCanvasFixed, {
4174
+ height: canvasHeight,
4175
+ width: canvasWidth
4176
+ });
4177
+ // redraw from uichartResize
4178
+ uichartRedraw();
4179
+ }
4180
+ function uichartSeriesHideOrShow(series, modeHide) {
4181
+ // this function will hide-or-show <series>
4182
+ if (!series || series.isDummy) {
4183
+ return;
4184
+ }
4185
+ // reset previously hidden points to yaxis
4186
+ if (isBarchart && !modeHide && series.isHidden) {
4187
+ series.pointListSeries.forEach(function ({
4188
+ elemPoint,
4189
+ pointY
4190
+ }) {
4191
+ if (pointY !== undefined) {
4192
+ svgAttrSet(elemPoint, {
4193
+ height: 0,
4194
+ y: plotbarYaxis
4195
+ });
4196
+ }
4197
+ });
4198
+ }
4199
+ series.isHidden = modeHide;
4200
+ if (!modeHide) {
4201
+ onSeriesHover(series);
4202
+ }
4203
+ [
4204
+ series.elemSeries, series.elemGraphtracker
4205
+ ].forEach(function (elem) {
4206
+ if (elem) {
4207
+ elem.setAttribute("visibility", (
4208
+ modeHide
4209
+ ? "hidden"
4210
+ : "visible"
4211
+ ));
4212
+ }
4213
+ });
4214
+ // hide or show legend
4215
+ elemLegend.children[series.seriesIi].dataset.hidden = Number(modeHide);
4216
+ }
4217
+ function xaxisTranslate(xval) {
4218
+ // this function will translate <xval> to xpixel position on chart
4219
+ return xtransOffset + xtransWidth * (xval - xaxisMin);
4220
+ }
4221
+ function xpixelToPointDictCreate(pointList) {
4222
+ // this function will create dict mapping <xpixel> to nearest point in
4223
+ // <pointList> along xaxis
4224
+ let dict = [];
4225
+ let ii = 0;
4226
+ let nn = pointList.length;
4227
+ let pointObj;
4228
+ let pointObjPrv;
4229
+ let xpixel = 0;
4230
+ let xpixelEnd;
4231
+ while (ii < nn) {
4232
+ pointObj = pointList[ii];
4233
+ if (pointObj.pointY !== undefined && !pointObj.series.isHidden) {
4234
+ if (pointObjPrv) {
4235
+ xpixelEnd = 0.5 * (pointObjPrv.pointX + pointObj.pointX);
4236
+ while (xpixel < xpixelEnd) {
4237
+ dict[xpixel] = pointObjPrv;
4238
+ xpixel += 1;
4239
+ }
4240
+ }
4241
+ pointObjPrv = pointObj;
4242
+ }
4243
+ ii += 1;
4244
+ }
4245
+ while (pointObjPrv && xpixel < plotWidth) {
4246
+ dict[xpixel] = pointObjPrv;
4247
+ xpixel += 1;
4248
+ }
4249
+ return dict;
4250
+ }
4251
+ function yaxisTranslate(yval) {
4252
+ // this function will translate <yval> to ypixel position on chart
4253
+ return plotHeight - ytransWidth * (yval - yaxisMin);
4254
+ }
4255
+ //
4256
+ // uichartCreate - start
4257
+ //
4258
+ // Resize the box and re-align all aligned elements
4259
+ svgAttrSet(elemCanvasFixed, {
4260
+ height: canvasHeight,
4261
+ width: canvasWidth
4262
+ });
4263
+ // init event-handling
4264
+ elemCanvasFlex.onclick = onSeriesHoveredHide;
4265
+ elemCanvasFlex.onmouseenter = onPointUnhover;
4266
+ elemCanvasFlex.onmouseleave = onPointUnhover;
4267
+ elemCanvasFlex.onmousemove = onPointHover;
4268
+ elemCanvasFlex.onwheel = onCanvasZoom;
4269
+ elemLegend.onmouseleave = onPointUnhover;
4270
+ elemLegend.onmouseover = onSeriesHover;
4271
+ elemUichart.querySelector(".uichartNav").onclick = onUichartAction;
4272
+ // init seriesList
4273
+ seriesList.forEach(function (series, seriesIi) {
4274
+ let elemGraph;
4275
+ let elemGraphtracker;
4276
+ let elemSeries = svgAttrSet("g");
4277
+ let {
4278
+ isDummy,
4279
+ seriesColor,
4280
+ xdata,
4281
+ ydata
4282
+ } = series;
4283
+ let pointListSeries;
4284
+ let seriesShape;
4285
+ elemSeries.classList.add(`elemSeries_${seriesIi}`);
4286
+ // init seriesColor
4287
+ if (isDummy) {
4288
+ seriesColor = "rgba(192,192,192,0)";
4289
+ } else if (!(seriesColor && typeof seriesColor === "string")) {
4290
+ seriesColor = seriesColorList[
4291
+ ((seriesColor ?? counterColor) - 1) % seriesColorList.length
4292
+ ];
4293
+ counterColor += 1;
4294
+ }
4295
+ seriesColor = seriesColor.replace((
4296
+ /^#(.)(.)(.)$/
4297
+ ), "#$1$1$2$2$3$3");
4298
+ // init seriesShape
4299
+ if (isBarchart || isDummy) {
4300
+ seriesShape = "square";
4301
+ } else {
4302
+ seriesShape = seriesShapeList[
4303
+ ((seriesShape ?? counterShape) - 1) % seriesShapeList.length
4304
+ ];
4305
+ counterShape += 1;
4306
+ }
4307
+ // init xdata, ydata
4308
+ xdata = new Float64Array(xdata);
4309
+ ydata = new Float64Array(ydata.map(function (yval) {
4310
+ return yval ?? NaN;
4311
+ }));
4312
+ if (!isBarchart) {
4313
+ // init elemGraph
4314
+ elemGraph = svgAttrSet("path", {
4315
+ fill: "none",
4316
+ stroke: seriesColor,
4317
+ "stroke-linecap": "round",
4318
+ "stroke-linejoin": "round",
4319
+ "stroke-width": ELEM_GRAPH_LINE_WIDTH
4320
+ });
4321
+ elemSeries.appendChild(elemGraph);
4322
+ // init elemGraphtracker
4323
+ elemGraphtracker = svgAttrSet("path", {
4324
+ fill: "none",
4325
+ stroke: "rgba(192,192,192,0.0001)",
4326
+ "stroke-linecap": "round",
4327
+ "stroke-linejoin": "round",
4328
+ "stroke-width": ELEM_GRAPH_LINE_WIDTH + 20,
4329
+ visibility: "inherit"
4330
+ });
4331
+ elemGraphtracker.classList.add(`series_${seriesIi}`);
4332
+ // init event-handling
4333
+ elemGraphtracker.dataset.seriesIi = seriesIi;
4334
+ elemGraphtracker.onmouseover = function (evt) {
4335
+ onSeriesHover(evt, true);
4336
+ };
4337
+ }
4338
+ // init pointListSeries
4339
+ pointListSeries = Array.from(ydata).map(function (yval, ii) {
4340
+ let elemPoint;
4341
+ let pointObj;
4342
+ let xval = xdata[ii];
4343
+ elemPoint = (
4344
+ isBarchart
4345
+ ? svgAttrSet("rect", {
4346
+ fill: seriesColor,
4347
+ stroke: "#333",
4348
+ "stroke-width": ELEM_POINT_BORDER_WIDTH,
4349
+ visibility: "inherit",
4350
+ y: 0.5 * canvasHeight
4351
+ })
4352
+ : svgAttrSet("path", {
4353
+ fill: seriesColor,
4354
+ stroke: "#333",
4355
+ "stroke-width": ELEM_POINT_BORDER_WIDTH,
4356
+ visibility: "inherit",
4357
+ y: 0.5 * canvasHeight
4358
+ })
4359
+ );
4360
+ // init fx_seriesShape
4361
+ if (!isBarchart) {
4362
+ elemPoint.fx_seriesShape = seriesShape;
4363
+ }
4364
+ pointObj = {
4365
+ elemPoint,
4366
+ series,
4367
+ xval,
4368
+ yval: (
4369
+ Number.isNaN(yval)
4370
+ ? undefined
4371
+ : yval
4372
+ )
4373
+ };
4374
+ if (isBarchart) {
4375
+ pointListBarchart.push(pointObj);
4376
+ }
4377
+ return pointObj;
4378
+ });
4379
+ // save series-properties
4380
+ Object.assign(series, {
4381
+ elemGraph,
4382
+ elemGraphtracker,
4383
+ elemSeries,
4384
+ isHidden: series.isDummy || series.isHidden,
4385
+ pointListSeries,
4386
+ seriesColor,
4387
+ seriesIi,
4388
+ seriesShape,
4389
+ xdata,
4390
+ xpixelToPointDict: [],
4391
+ ydata
4392
+ });
4393
+ });
4394
+ if (isBarchart) {
4395
+ pointListBarchart.sort(function (aa, bb) {
4396
+ return aa.xval - bb.xval;
4397
+ });
4398
+ }
4399
+ // draw seriesList in reverse, so first series has highest z-index
4400
+ Array.from(seriesList).reverse().forEach(function ({
4401
+ elemGraphtracker,
4402
+ elemSeries,
4403
+ pointListSeries
4404
+ }) {
4405
+ elemSeriesList.appendChild(elemSeries);
4406
+ Array.from(pointListSeries).reverse().forEach(function ({
4407
+ elemPoint
4408
+ }) {
4409
+ elemSeries.appendChild(elemPoint);
4410
+ });
4411
+ if (!isBarchart) {
4412
+ elemMousetrackerList.appendChild(elemGraphtracker);
4413
+ }
4414
+ });
4415
+ // draw elemLegend
4416
+ elemLegend.innerHTML = uichart.seriesList.map(function (series, ii) {
4417
+ return (`
4418
+ <a
4419
+ class="uichartAction uichartLegendElem"
4420
+ data-action="uichartSeriesHideOrShow"
4421
+ data-dummy="${series.isDummy | 0}"
4422
+ data-hidden="${series.isHidden | 0}"
4423
+ data-series-ii="${ii}"
4424
+ title="${stringHtmlSafe(series.seriesName)}"
4425
+ >
4426
+ <svg class="uichartLegendElemSvg" xmlns="http://www.w3.org/2000/svg">
4427
+ <g>
4428
+ <path
4429
+ d="${svgShapeDraw(series.seriesShape, 0, 2, 10, 10)}"
4430
+ fill="${series.seriesColor}"
4431
+ stroke-width="0"
4432
+ >
4433
+ </path>
4434
+ </g>
4435
+ </svg>
4436
+ <span style="margin-left: 12px; position: absolute;">${series.seriesName}</span>
4437
+ </a>
4438
+ `);
4439
+ }).join("");
4440
+ // first-draw
4441
+ uichartRedraw(true);
4442
+ // after first-draw, init startup-animation
4443
+ svgAttrSet(elemClip.firstElementChild, {
4444
+ width: 0
4445
+ });
4446
+ svgAnimate(elemClip.firstElementChild, {
4447
+ width: plotWidth
4448
+ });
4449
+ return Object.assign(uichart, {
4450
+ uichartRedraw,
4451
+ uichartResize
4452
+ });
4453
+ //
4454
+ // uichartCreate - end
4455
+ //
4456
+ }
4457
+
4458
+ async function uitableAjax(baton, {
4459
+ rowList,
4460
+ type
4461
+ }) {
4462
+ let {
4463
+ colList,
4464
+ contentElem,
4465
+ db,
4466
+ dbtableName,
4467
+ elemInfo,
4468
+ elemLoading,
4469
+ elemScroller,
4470
+ elemTable,
4471
+ hashtag,
4472
+ isDbchart,
4473
+ rowCount,
4474
+ sortCol,
4475
+ sortDir
4476
+ } = baton;
4477
+ let html = "";
4478
+ let viewRowBeg;
4479
+ let viewRowEnd;
4480
+ if (baton.rowCount === 0) {
4481
+ // uitableLoading - hide
4482
+ uiFadeOut(elemLoading);
4483
+ return;
4484
+ }
4485
+ switch (type) {
4486
+ // uitableScroll
4487
+ case "scroll":
4488
+ case "uitableInit":
4489
+ if (type === "uitableInit" && isDbchart) {
4490
+ uiFadeIn(elemLoading);
4491
+ await uiTryCatch(async function () {
4492
+ // resize uichart
4493
+ if (baton.uichart) {
4494
+ baton.uichart.uichartResize();
4495
+ return;
4496
+ }
4497
+ // create uichart
4498
+ baton.uichart = await uichartCreate(baton);
4499
+ });
4500
+ await waitAsync(500);
4501
+ uiFadeOut(elemLoading);
4502
+ return;
4503
+ }
4504
+ viewRowBeg = Math.max(0, Math.round(
4505
+ rowCount
4506
+ * elemScroller.scrollTop
4507
+ / (elemScroller.scrollHeight - 1 * UI_ROW_HEIGHT)
4508
+ ));
4509
+ viewRowEnd = Math.min(rowCount, Math.round(viewRowBeg + UI_VIEW_SIZE));
4510
+ // update table-view info
4511
+ elemInfo.textContent = (
4512
+ "showing "
4513
+ + new Intl.NumberFormat().format(viewRowBeg + 1)
4514
+ + " to "
4515
+ + new Intl.NumberFormat().format(viewRowEnd)
4516
+ + " of "
4517
+ + new Intl.NumberFormat().format(rowCount)
4518
+ + " rows"
4519
+ );
4520
+ // skip expensive table-redraw, if scroll-point is within boundaries
4521
+ if (
4522
+ contentElem.dataset.init !== "0"
4523
+ && baton.rowOffset <= Math.max(0, viewRowBeg - 1 * UI_VIEW_SIZE)
4524
+ && (
4525
+ Math.min(rowCount, viewRowEnd + 1 * UI_VIEW_SIZE)
4526
+ <= baton.rowOffset + UI_PAGE_SIZE
4527
+ )
4528
+ ) {
4529
+ return;
4530
+ }
4531
+ // Do the uitable redraw based on the calculated start point
4532
+ baton.rowOffset = Math.max(0, Math.round(
4533
+ viewRowBeg + 0.5 * UI_VIEW_SIZE - 0.5 * UI_PAGE_SIZE
4534
+ ));
4535
+ break;
4536
+ }
4537
+ switch (type !== "uitableDraw" && baton.modeAjax) {
4538
+ // uitableAjax
4539
+ case 0:
4540
+ // uitableLoading - show
4541
+ uiFadeIn(elemLoading);
4542
+ baton.modeAjax = 1;
4543
+ // uitable - paginate
4544
+ rowList = await dbExecAsync({
4545
+ db,
4546
+ responseType: "list",
4547
+ sql: (`
4548
+ SELECT
4549
+ rowid,
4550
+ --
4551
+ ${dbtableName}.*
4552
+ FROM ${dbtableName}
4553
+ ORDER BY [${colList[sortCol]}] ${sortDir}
4554
+ LIMIT ${Number(UI_PAGE_SIZE)}
4555
+ OFFSET ${Number(baton.rowOffset)};
4556
+ `)
4557
+ });
4558
+ rowList = (
4559
+ rowList[0]
4560
+ ? rowList[0].slice(1)
4561
+ : []
4562
+ ).map(function (row) {
4563
+ return row.map(function (val) {
4564
+ return (
4565
+ // bugfix - truncate large text to avoid freezing browser
4566
+ (typeof val === "string" && val.length > 65536)
4567
+ ? val.slice(0, 65536)
4568
+ : val
4569
+ );
4570
+ });
4571
+ });
4572
+ // recurse - draw
4573
+ await uitableAjax(baton, {
4574
+ rowList,
4575
+ type: "uitableDraw"
4576
+ });
4577
+ return;
4578
+ // debounce
4579
+ case 1:
4580
+ baton.modeAjax = 2;
4581
+ return;
4582
+ // debounce
4583
+ case 2:
4584
+ return;
4585
+ }
4586
+ // uitableDraw
4587
+ // Position the table in the virtual scroller
4588
+ elemTable.style.top = Math.max(0, Math.round(
4589
+ elemScroller.scrollHeight * baton.rowOffset / (baton.rowCount + 1)
4590
+ )) + "px";
4591
+ // Insert the required TR nodes into the table for display
4592
+ jsonHtmlSafe(rowList).forEach(function (row) {
4593
+ html += (`
4594
+ <tr data-dbtype="row" data-hashtag="${hashtag}" data-rowid="${row[0]}">
4595
+ `);
4596
+ row.forEach(function (val) {
4597
+ html += "<td>" + (val ?? "") + "</td>";
4598
+ });
4599
+ html += "</tr>";
4600
+ });
4601
+ elemTable.children[1].innerHTML = html;
4602
+ // debounce - throttle
4603
+ await waitAsync(500);
4604
+ // debounce - next
4605
+ if (baton.modeAjax === 2) {
4606
+ baton.modeAjax = 0;
4607
+ // keep focus on current scroller when debouncing
4608
+ if (type === "scroll") {
4609
+ elemScroller.focus();
4610
+ }
4611
+ await uitableAjax(baton, {});
4612
+ return;
4613
+ }
4614
+ // cleanup
4615
+ baton.modeAjax = 0;
4616
+ // uitableLoading - hide
4617
+ uiFadeOut(elemLoading);
4618
+ }
4619
+
4620
+ function uitableCreate(baton) {
4621
+ // this function will create a dom-datatable-view of sqlite queries and tables
4622
+ let contentElem;
4623
+ // All uitables are wrapped in a div
4624
+ // Generate the node required for the processing node
4625
+ // The HTML structure that we want to generate in this function is:
4626
+ // div - scroller
4627
+ // div - scroll head
4628
+ // div - scroll head inner
4629
+ // table - scroll head table
4630
+ // thead - thead
4631
+ // div - scroll body
4632
+ // table - table (master table)
4633
+ // thead - thead clone for sizing
4634
+ // tbody - tbody
4635
+ contentElem = domDivCreate(
4636
+ (`
4637
+ <div class="contentElem" data-init="0" id="${baton.hashtag}">
4638
+ <div class="contentElemTitle title">${stringHtmlSafe(baton.title)}</div>
4639
+ <div class="uitableLoading">loading</div>
4640
+ <div class="uichart" style="display: none;"></div>
4641
+ <div class="uitable">
4642
+ <div class="uitableInfo">showing 0 to 0 of 0 entries</div>
4643
+ <div
4644
+ class="uitableScroller"
4645
+ style="height: ${(UI_VIEW_SIZE + 2) * UI_ROW_HEIGHT}px;"
4646
+ tabindex="-1"
4647
+ >
4648
+ <div
4649
+ class="uitableScrollerDummy"
4650
+ style="height: ${baton.rowCount * UI_ROW_HEIGHT}px;"
4651
+ ></div>
4652
+ <table class="uitableTable">
4653
+ <thead>
4654
+ <tr>
4655
+ `)
4656
+ + jsonHtmlSafe(baton.colList).map(function (col, ii) {
4657
+ return (
4658
+ ii === 0
4659
+ ? `<th title="${col}" data-sort="asc">${col}</th>`
4660
+ : `<th title="${col}">${col}</th>`
4661
+ );
4662
+ }).join("")
4663
+ + (`
4664
+ </tr>
4665
+ </thead>
4666
+ <tbody>
4667
+ <tr data-dbtype="row" data-hashtag="${baton.hashtag}">
4668
+ <td colspan="${baton.colList.length}">
4669
+ No data available in table
4670
+ </td>
4671
+ </tr>
4672
+ </tbody>
4673
+ </table>
4674
+ </div>
4675
+ </div>
4676
+ </div>
4677
+ `)
4678
+ ).firstElementChild;
4679
+ // init event-handling - crud
4680
+ contentElem.querySelector(
4681
+ "tbody"
4682
+ ).oncontextmenu = onContextmenu;
4683
+ // init event-handling - sorting
4684
+ contentElem.querySelector(
4685
+ "thead tr"
4686
+ ).onclick = uitableSort.bind(undefined, baton);
4687
+ // init event-handling - scrolling
4688
+ contentElem.querySelector(
4689
+ ".uitableScroller"
4690
+ ).onscroll = uitableAjax.bind(undefined, baton);
4691
+ contentElem.addEventListener("uitableInit", function (evt) {
4692
+ uitableAjax(baton, evt);
4693
+ });
4694
+ Object.assign(baton, {
4695
+ contentElem,
4696
+ elemInfo: contentElem.querySelector(".uitableInfo"),
4697
+ elemLoading: contentElem.querySelector(".uitableLoading"),
4698
+ elemScroller: contentElem.querySelector(".uitableScroller"),
4699
+ elemTable: contentElem.querySelector(".uitableTable"),
4700
+ modeAjax: 0,
4701
+ rowOffset: 0
4702
+ });
4703
+ return contentElem;
4704
+ }
4705
+
4706
+ function uitableInitWithinView({
4707
+ modeDebounce
4708
+ }) {
4709
+ // this function will defer-init uitables when visible in viewport
4710
+ if (!modeDebounce) {
4711
+ debounce("uitableInitWithinView", uitableInitWithinView, {
4712
+ modeDebounce: true
4713
+ });
4714
+ return;
4715
+ }
4716
+ document.querySelectorAll(
4717
+ `#contentPanel1 .contentElem[data-init="0"]`
4718
+ ).forEach(function (elem) {
4719
+ let rect = elem.getBoundingClientRect();
4720
+ if (0 <= rect.bottom && rect.top < window.innerHeight) {
4721
+ elem.dispatchEvent(new window.CustomEvent("uitableInit"));
4722
+ elem.dataset.init = "1";
4723
+ }
4724
+ });
4725
+ }
4726
+
4727
+ function uitableSort(baton, {
4728
+ currentTarget,
4729
+ target
4730
+ }) {
4731
+ // Function to run on user sort request
4732
+ let colIi;
4733
+ let direction;
4734
+ let elem = target.closest("th");
4735
+ if (!elem) {
4736
+ return;
4737
+ }
4738
+ direction = elem.dataset.sort;
4739
+ direction = (
4740
+ direction === "asc"
4741
+ ? "desc"
4742
+ : "asc"
4743
+ );
4744
+ Array.from(currentTarget.children).forEach(function (elemTh, ii) {
4745
+ if (elemTh !== elem) {
4746
+ elemTh.dataset.sort = "";
4747
+ return;
4748
+ }
4749
+ colIi = ii;
4750
+ });
4751
+ elem.dataset.sort = direction;
4752
+ baton.sortCol = colIi;
4753
+ baton.sortDir = direction;
4754
+ // Reset scroll to top in redraw.
4755
+ baton.elemScroller.scrollTop = 0;
4756
+ baton.rowOffset = 0;
4757
+ uitableAjax(baton, {});
4758
+ }
4759
+
4760
+ function waitAsync(timeout) {
4761
+ // this function will wait <timeout> milliseconds
4762
+ return new Promise(function (resolve) {
4763
+ setTimeout(resolve, timeout);
4764
+ });
4765
+ }
4766
+
4767
+ // init
4768
+ window.addEventListener("load", init);