sqlmath 2025.8.30 → 2025.12.28

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,4769 @@
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
+ -- IF OBJECT_ID('tempdb..#tmp1') IS NOT NULL DROP TABLE #tmp1;
2050
+ -- SELECT * FROM __sqlite_table_01;
2051
+ -- ALTER TABLE __sqlite_table_01 RENAME TO __sqlite_table_02;
2052
+ -- EXEC sp_rename '__sqlite_table_01', '__sqlite_table_02';
2053
+ `).trim()
2054
+ + `\nCREATE TABLE __sqlite_table_01 (\n`
2055
+ + baton.colList.map(function (col, ii) {
2056
+ col = col.replace((/\W/g), "_");
2057
+ col = col.replace((/^\d/), "_$&");
2058
+ // #define SQLITE_INTEGER 1
2059
+ // #define SQLITE_COLUMNTYPE_INTEGER_BIG 11
2060
+ // #define SQLITE_FLOAT 2
2061
+ // #define SQLITE_TEXT 3
2062
+ // #define SQLITE_COLUMNTYPE_TEXT_BIG 13
2063
+ // #define SQLITE_BLOB 4
2064
+ // #define SQLITE_NULL 5
2065
+ switch (columntypeList[ii]) {
2066
+ case 2: // SQLITE_FLOAT
2067
+ return ` ${col} FLOAT(53)`;
2068
+ case 3: // SQLITE_TEXT
2069
+ return ` ${col} VARCHAR(255)`;
2070
+ case 11: // SQLITE_COLUMNTYPE_INTEGER_BIG
2071
+ return ` ${col} BIGINT`;
2072
+ case 13: // SQLITE_COLUMNTYPE_TEXT_BIG
2073
+ return ` ${col} TEXT`;
2074
+ default:
2075
+ return ` ${col} INTEGER`;
2076
+ }
2077
+ }).join(",\n")
2078
+ + `\n);\n`
2079
+ + data.map(function (rowList) {
2080
+ return (
2081
+ `INSERT INTO __sqlite_table_01 VALUES (`
2082
+ + rowList.map(function (val, ii) {
2083
+ switch (val !== null && columntypeList[ii]) {
2084
+ case 1: // SQLITE_INTEGER
2085
+ case 2: // SQLITE_FLOAT
2086
+ case 11: // SQLITE_COLUMNTYPE_INTEGER_BIG
2087
+ return val;
2088
+ case 3: // SQLITE_TEXT
2089
+ case 13: // SQLITE_COLUMNTYPE_TEXT_BIG
2090
+ return "'" + val.replace((/'/g), "''") + "'";
2091
+ // case 4: // SQLITE_BLOB
2092
+ // case 5: // SQLITE_NULL
2093
+ default:
2094
+ return "NULL";
2095
+ }
2096
+ }).join(",")
2097
+ + `);\n`
2098
+ );
2099
+ }).join("")
2100
+ );
2101
+ fileSave({
2102
+ buf: data,
2103
+ filename: `sqlite_table_${baton.dbtableName}.sql`
2104
+ });
2105
+ return;
2106
+ }
2107
+ throw new Error(`onDbAction - invalid action ${action}`);
2108
+ }
2109
+
2110
+ async function onDbExec({
2111
+ modeTryCatch
2112
+ }) {
2113
+ // this function will
2114
+ // 1. exec sql-command in webworker
2115
+ // 2. save query-result
2116
+ // 3. ui-render sql-queries to html
2117
+ let dbqueryList;
2118
+ if (!modeTryCatch) {
2119
+ await uiTryCatch(onDbExec, {
2120
+ modeTryCatch: true
2121
+ });
2122
+ return;
2123
+ }
2124
+ // close error modal
2125
+ uiFadeOut(document.querySelector("#errorPanel1"));
2126
+ // DBTABLE_DICT - cleanup old uitable
2127
+ DBTABLE_DICT.forEach(function ({
2128
+ isDbchart,
2129
+ isDbquery
2130
+ }, key) {
2131
+ if (isDbchart || isDbquery) {
2132
+ DBTABLE_DICT.delete(key);
2133
+ }
2134
+ });
2135
+ await Promise.all([
2136
+ DB_CHART, DB_QUERY
2137
+ ].map(async function (db) {
2138
+ let sqlCleanup = noop(
2139
+ await dbExecAsync({
2140
+ db,
2141
+ sql: (`
2142
+ BEGIN TRANSACTION;
2143
+ SELECT
2144
+ group_concat('DROP TABLE [' || name || '];', '') AS sql
2145
+ FROM sqlite_master
2146
+ WHERE
2147
+ type = 'table';
2148
+ END TRANSACTION;
2149
+ `)
2150
+ })
2151
+ )[0][0].sql || "";
2152
+ await dbExecAsync({
2153
+ db,
2154
+ sql: sqlCleanup
2155
+ });
2156
+ }));
2157
+ // 1. exec sql-command in webworker
2158
+ dbqueryList = await dbExecAsync({
2159
+ db: DB_MAIN,
2160
+ responseType: "list",
2161
+ sql: UI_EDITOR.getValue()
2162
+ });
2163
+ // 2. save query-result
2164
+ await Promise.all(dbqueryList.map(async function (rowList, ii) {
2165
+ let colList = rowList.shift().map(function (col, ii) {
2166
+ // bugfix - escape special-character in col
2167
+ return `value->>${ii} AS "${col.replace((/"/g), "\"\"")}"`;
2168
+ }).join(",");
2169
+ await dbExecAsync({
2170
+ bindList: {
2171
+ tmp1: JSON.stringify(rowList)
2172
+ },
2173
+ db: DB_QUERY,
2174
+ sql: (`
2175
+ BEGIN TRANSACTION;
2176
+ CREATE TABLE result_${String(ii).padStart(2, "0")} AS
2177
+ SELECT
2178
+ ${colList}
2179
+ FROM JSON_EACH($tmp1);
2180
+ END TRANSACTION;
2181
+ `)
2182
+ });
2183
+ }));
2184
+ // 3. ui-render sql-queries to html
2185
+ await uiRenderDb();
2186
+ }
2187
+
2188
+ async function onDbcrudExec({
2189
+ modeTryCatch
2190
+ }) {
2191
+ // this function will exec crud operation
2192
+ let sql;
2193
+ if (!modeTryCatch) {
2194
+ await uiTryCatch(onDbcrudExec, {
2195
+ modeTryCatch: true
2196
+ });
2197
+ return;
2198
+ }
2199
+ sql = document.querySelector("#crudPanel1 textarea").value.replace((
2200
+ /\{\{\w+?\}\}/g
2201
+ ), function (match0) {
2202
+ let val = document.querySelector(
2203
+ `#crudPanel1 .crudInput-${match0.slice(2, -2)} .crudInput`
2204
+ );
2205
+ if (!val) {
2206
+ return match0;
2207
+ }
2208
+ if (val.tagName === "SELECT") {
2209
+ return val.selectedOptions[0].textContent;
2210
+ }
2211
+ return val.value.trim();
2212
+ });
2213
+ await dbExecAsync({
2214
+ db: DB_MAIN,
2215
+ sql
2216
+ });
2217
+ await uiRenderDb();
2218
+ uiFadeOut(UI_CRUD);
2219
+ UI_CRUD.style.zIndex = -1;
2220
+ }
2221
+
2222
+ function onKeyDown(evt) {
2223
+ // this function will handle event keyup
2224
+ switch (evt.key) {
2225
+ case "Escape":
2226
+ // close error-modal
2227
+ uiFadeOut(document.querySelector("#errorPanel1"));
2228
+ // close contextmenu
2229
+ uiFadeOut(UI_CONTEXTMENU);
2230
+ return;
2231
+ }
2232
+ switch ((evt.ctrlKey || evt.metaKey) && evt.key) {
2233
+ // database - execute
2234
+ case "Enter":
2235
+ onDbExec({});
2236
+ return;
2237
+ // database - open
2238
+ case "o":
2239
+ evt.preventDefault();
2240
+ evt.stopPropagation();
2241
+ document.querySelector("button[data-action='dbOpen']").click();
2242
+ return;
2243
+ // database - save
2244
+ case "s":
2245
+ evt.preventDefault();
2246
+ evt.stopPropagation();
2247
+ document.querySelector("button[data-action='dbExportMain']").click();
2248
+ return;
2249
+ }
2250
+ }
2251
+
2252
+ function onModalClose({
2253
+ currentTarget
2254
+ }) {
2255
+ // this function will close current modal
2256
+ uiFadeOut(currentTarget.closest(".modalPanel"));
2257
+ }
2258
+
2259
+ function onResize() {
2260
+ // this function will handle resize-event
2261
+ document.querySelectorAll(
2262
+ "#dbchartList1 .contentElem"
2263
+ ).forEach(function (elem) {
2264
+ elem.dataset.init = "0";
2265
+ });
2266
+ uitableInitWithinView({});
2267
+ }
2268
+
2269
+ function rowListToCsv({
2270
+ colList,
2271
+ rowList
2272
+ }) {
2273
+ // this function will convert json <rowList> to csv with given <colList>
2274
+ let data = JSON.stringify([[colList], rowList].flat(), undefined, 1);
2275
+ // convert data to csv
2276
+ data = data.replace((
2277
+ /\n /g
2278
+ ), "");
2279
+ data = data.replace((
2280
+ /\n \[/g
2281
+ ), "");
2282
+ data = data.replace((
2283
+ /\n \],?/g
2284
+ ), "\r\n");
2285
+ data = data.slice(1, -2);
2286
+ // sqlite-strings are c-strings which should never contain null-char
2287
+ data = data.replace((
2288
+ /\u0000/g
2289
+ ), "");
2290
+ // hide double-backslash `\\\\` as null-char
2291
+ data = data.replace((
2292
+ /\\\\/g
2293
+ ), "\u0000");
2294
+ // escape double-quote `\\"` to `""`
2295
+ data = data.replace((
2296
+ /\\"/g
2297
+ ), "\"\"");
2298
+ // replace newline with space
2299
+ data = data.replace((
2300
+ /\\r\\n|\\r|\\n/g
2301
+ ), " ");
2302
+ // restore double-backslash `\\\\` from null-char
2303
+ data = data.replace((
2304
+ /\u0000/g
2305
+ ), "\\\\");
2306
+ return data;
2307
+ }
2308
+
2309
+ function stringHtmlSafe(str) {
2310
+ // this function will make <str> html-safe
2311
+ // https://stackoverflow.com/questions/7381974
2312
+ if (typeof str !== "string") {
2313
+ str = String(str);
2314
+ }
2315
+ return str.replace((
2316
+ /&/gu
2317
+ ), "&amp;").replace((
2318
+ /</gu
2319
+ ), "&lt;").replace((
2320
+ />/gu
2321
+ ), "&gt;").replace((
2322
+ /"/gu
2323
+ ), "&quot;");
2324
+ }
2325
+
2326
+ function svgAnimate(elem, attrDict, mode) {
2327
+ // this function will animate <elem> with given <fxattr> in <attrDict>
2328
+ let {
2329
+ childNodes,
2330
+ fx_rotate,
2331
+ fx_seriesShape,
2332
+ nodeName
2333
+ } = elem;
2334
+ let datebeg = UI_ANIMATE_DATENOW;
2335
+ let fxstateDict = {};
2336
+ let {
2337
+ translateY
2338
+ } = attrDict;
2339
+ // optimization - skip animation if hidden
2340
+ if (attrDict.visibility === "hidden") {
2341
+ svgAttrSet(elem, attrDict);
2342
+ return;
2343
+ }
2344
+ // init fxstateDict
2345
+ Object.entries(attrDict).forEach(function ([
2346
+ fxattr, fxend
2347
+ ]) {
2348
+ let dpathList;
2349
+ let fxattr2 = "fx_" + fxattr;
2350
+ let fxbeg;
2351
+ let fxstate;
2352
+ // crispify height, width, x, y
2353
+ switch (fxattr) {
2354
+ case "height":
2355
+ case "width":
2356
+ case "x":
2357
+ case "y":
2358
+ fxend = Math.round(fxend);
2359
+ switch (fxattr) {
2360
+ case "x":
2361
+ fxend += CRISPX;
2362
+ break;
2363
+ case "y":
2364
+ fxend += CRISPY;
2365
+ break;
2366
+ }
2367
+ break;
2368
+ }
2369
+ fxstate = {
2370
+ fxend
2371
+ };
2372
+ switch (fxattr) {
2373
+ case "d":
2374
+ dpathList = fxend.split(" ");
2375
+ fxbeg = noop(elem.getAttribute(fxattr) ?? "").split(" ");
2376
+ if (!(fxbeg.length > 0 && fxbeg.length === dpathList.length)) {
2377
+ elem[fxattr2] = fxend;
2378
+ elem.setAttribute(fxattr, fxend);
2379
+ return;
2380
+ }
2381
+ fxstate.dpathList = dpathList;
2382
+ fxstate.fxbeg = fxbeg;
2383
+ fxstateDict[fxattr] = fxstate;
2384
+ break;
2385
+ case "height":
2386
+ case "translateX":
2387
+ case "translateY":
2388
+ case "width":
2389
+ case "x":
2390
+ case "y":
2391
+ fxstate.fxbeg = Number(
2392
+ elem[fxattr2] ?? elem.getAttribute(fxattr) ?? 0
2393
+ );
2394
+ if (fx_seriesShape) {
2395
+ fxstate.fxbeg = fxstate.fxbeg || fxend;
2396
+ }
2397
+ fxstateDict[fxattr] = fxstate;
2398
+ break;
2399
+ case "visibility":
2400
+ elem.setAttribute(fxattr, fxend);
2401
+ return;
2402
+ default:
2403
+ throw new Error(`svgAnimate - invalid attribute - ${fxattr}`);
2404
+ }
2405
+ });
2406
+ function svgAnimateStep() {
2407
+ let fxprg = 1;
2408
+ let isDone = datebeg + UI_ANIMATE_DURATION <= UI_ANIMATE_DATENOW;
2409
+ // animate - linear fxnow, fxprg
2410
+ if (!isDone) {
2411
+ fxprg = (
2412
+ UI_ANIMATE_DURATION_INV
2413
+ * (UI_ANIMATE_DATENOW - datebeg)
2414
+ );
2415
+ if (mode === "easeout") {
2416
+ fxprg = Math.sqrt(fxprg);
2417
+ }
2418
+ }
2419
+ Object.entries(fxstateDict).forEach(function ([
2420
+ fxattr, fxstate
2421
+ ]) {
2422
+ let {
2423
+ dpathList,
2424
+ fxbeg,
2425
+ fxend
2426
+ } = fxstate;
2427
+ let fxattr2 = "fx_" + fxattr;
2428
+ let fxnow = fxend;
2429
+ switch (fxattr) {
2430
+ // Perform the next step of the animation on "d"
2431
+ case "d":
2432
+ if (!dpathList) {
2433
+ return;
2434
+ }
2435
+ // interpolate fxnow from dpathList, fxbeg
2436
+ if (!isDone) {
2437
+ fxnow = fxbeg.map(function (char, ii) {
2438
+ let num;
2439
+ if ("CLMZ".indexOf(char) !== -1) {
2440
+ return char;
2441
+ }
2442
+ num = Number(char);
2443
+ return num + fxprg * (dpathList[ii] - num);
2444
+ }).join(" ");
2445
+ }
2446
+ // cache attribute
2447
+ elem[fxattr2] = fxnow;
2448
+ elem.setAttribute("d", fxnow);
2449
+ return;
2450
+ case "height":
2451
+ case "translateX":
2452
+ case "translateY":
2453
+ case "width":
2454
+ case "x":
2455
+ case "y":
2456
+ if (!isDone) {
2457
+ fxnow = fxbeg + fxprg * (fxend - fxbeg);
2458
+ }
2459
+ // cache attribute
2460
+ elem[fxattr2] = fxnow;
2461
+ if (fx_seriesShape || translateY) {
2462
+ return;
2463
+ }
2464
+ // update child tspans x values
2465
+ if (fxattr === "x" && nodeName === "text") {
2466
+ childNodes.forEach(function (child) {
2467
+ // if the x values are equal, the tspan represents a
2468
+ // linebreak
2469
+ child.setAttribute("x", fxnow);
2470
+ });
2471
+ if (fx_rotate) {
2472
+ elem.setAttribute(
2473
+ "transform",
2474
+ `rotate(-15 ${fxnow} ${elem.fx_y || 0})`
2475
+ );
2476
+ }
2477
+ }
2478
+ elem.setAttribute(fxattr, fxnow);
2479
+ return;
2480
+ }
2481
+ });
2482
+ if (fx_seriesShape) {
2483
+ elem.setAttribute("d", svgShapeDraw(
2484
+ fx_seriesShape,
2485
+ elem.fx_x,
2486
+ elem.fx_y,
2487
+ elem.fx_width,
2488
+ elem.fx_height
2489
+ ));
2490
+ }
2491
+ if (translateY) {
2492
+ elem.setAttribute(
2493
+ "transform",
2494
+ `translate(${elem.fx_translateX},${elem.fx_translateY})`
2495
+ );
2496
+ }
2497
+ return isDone;
2498
+ }
2499
+ // animate - stop existing animation for given elem
2500
+ svgAnimateStep.elem2 = elem;
2501
+ UI_ANIMATE_LIST = UI_ANIMATE_LIST.filter(function ({
2502
+ elem2
2503
+ }) {
2504
+ return elem2 !== elem;
2505
+ });
2506
+ // animate - svgAnimateStep()
2507
+ svgAnimateStep();
2508
+ // animate - setInterval()
2509
+ UI_ANIMATE_LIST.push(svgAnimateStep);
2510
+ }
2511
+
2512
+ function svgAttrSet(elem, attrDict = {}) {
2513
+ // this function will set-attribute items in <attrDict> to <elem>
2514
+ if (typeof elem === "string") {
2515
+ elem = document.createElementNS("http://www.w3.org/2000/svg", elem);
2516
+ }
2517
+ Object.entries(attrDict).forEach(function ([
2518
+ key, val
2519
+ ]) {
2520
+ if (val !== null && val !== undefined) {
2521
+ elem.setAttribute(key, val);
2522
+ switch (key) {
2523
+ case "clip-path":
2524
+ case "d":
2525
+ case "fill":
2526
+ case "stroke":
2527
+ case "stroke-linecap":
2528
+ case "stroke-linejoin":
2529
+ case "stroke-width":
2530
+ case "text-anchor":
2531
+ case "transform":
2532
+ case "visibility":
2533
+ elem.setAttribute(key, val);
2534
+ return;
2535
+ // crispify height, width, x, y
2536
+ case "height":
2537
+ case "width":
2538
+ case "x":
2539
+ case "y":
2540
+ val = Math.round(val);
2541
+ switch (key) {
2542
+ case "x":
2543
+ val += CRISPX;
2544
+ break;
2545
+ case "y":
2546
+ val += CRISPY;
2547
+ break;
2548
+ }
2549
+ elem.setAttribute(key, val);
2550
+ // cache attribute
2551
+ elem["fx_" + key] = val;
2552
+ return;
2553
+ default:
2554
+ throw new Error(`svgAttrSet - invalid attribute - ${key}`);
2555
+ }
2556
+ }
2557
+ });
2558
+ return elem;
2559
+ }
2560
+
2561
+ function svgShapeDraw(seriesShape, x, y, w, h) {
2562
+ // this function will create svg-dpath for given <seriesShape>
2563
+ let tmp;
2564
+ switch (seriesShape) {
2565
+ case "circle":
2566
+ tmp = 0.166 * w;
2567
+ return [
2568
+ "M", x + w / 2, y,
2569
+ "C", x + w + tmp, y, x + w + tmp, y + h, x + w / 2, y + h,
2570
+ "C", x - tmp, y + h, x - tmp, y, x + w / 2, y,
2571
+ "Z"
2572
+ ].join(" ");
2573
+ case "diamond":
2574
+ return [
2575
+ "M", x + w / 2, y,
2576
+ "L", x + w, y + h / 2,
2577
+ x + w / 2, y + h,
2578
+ x, y + h / 2,
2579
+ "Z"
2580
+ ].join(" ");
2581
+ case "square":
2582
+ x = Math.round(x + 0.0625 * w);
2583
+ y = Math.round(y + 0.0625 * h);
2584
+ w = Math.round(0.875 * w);
2585
+ h = Math.round(0.875 * h);
2586
+ return [
2587
+ "M", x, y,
2588
+ "L", x + w, y,
2589
+ x + w, y + h,
2590
+ x, y + h,
2591
+ "Z"
2592
+ ].join(" ");
2593
+ case "triangle":
2594
+ return [
2595
+ "M", x + w / 2, y,
2596
+ "L", x + w, y + h,
2597
+ x, y + h,
2598
+ "Z"
2599
+ ].join(" ");
2600
+ case "triangle-down":
2601
+ return [
2602
+ "M", x, y,
2603
+ "L", x + w, y,
2604
+ x + w / 2, y + h,
2605
+ "Z"
2606
+ ].join(" ");
2607
+ }
2608
+ }
2609
+
2610
+ function uiFadeIn(elem) {
2611
+ // this function will fade-in <elem>
2612
+ elem.style.opacity = (
2613
+ elem === UI_CRUD
2614
+ ? "1"
2615
+ : "0.875"
2616
+ );
2617
+ elem.style.visibility = "visible";
2618
+ }
2619
+
2620
+ function uiFadeOut(elem) {
2621
+ // this function will fade-out <elem>
2622
+ elem.style.opacity = "0";
2623
+ elem.style.visibility = "hidden";
2624
+ }
2625
+
2626
+ async function uiRenderDb() {
2627
+ // this function will render #dbtableList1
2628
+ let dbList;
2629
+ let dbtableIi = 0;
2630
+ let html = "";
2631
+ let windowScrollY;
2632
+ // reset location.hash
2633
+ location.hash = "0";
2634
+ // save window.scrollY
2635
+ windowScrollY = window.scrollY;
2636
+ // DB_DICT - sync
2637
+ dbList = await dbExecAsync({
2638
+ db: DB_MAIN,
2639
+ sql: "PRAGMA database_list;"
2640
+ });
2641
+ dbList = new Set(dbList[0].map(function ({
2642
+ name
2643
+ }) {
2644
+ return `${name}`;
2645
+ }));
2646
+ DB_DICT.forEach(function ({
2647
+ dbName,
2648
+ isDbchart,
2649
+ isDbquery
2650
+ }) {
2651
+ if (!isDbchart && !isDbquery && !dbList.has(dbName)) {
2652
+ DB_DICT.delete(dbName);
2653
+ }
2654
+ });
2655
+ // DBTABLE_DICT - cleanup old uitable
2656
+ DBTABLE_DICT.forEach(function ({
2657
+ isDbchart,
2658
+ isDbquery
2659
+ }, key) {
2660
+ if (!isDbchart && !isDbquery) {
2661
+ DBTABLE_DICT.delete(key);
2662
+ }
2663
+ });
2664
+ // DBTABLE_DICT - sync
2665
+ await Promise.all(Array.from(DB_DICT.values()).map(async function (db) {
2666
+ let baton;
2667
+ let {
2668
+ dbName,
2669
+ isDbchart,
2670
+ isDbmain,
2671
+ isDbquery
2672
+ } = db;
2673
+ let dbtableList;
2674
+ let tmp;
2675
+ db.dbtableList = new Map();
2676
+ dbtableList = noop(
2677
+ await dbExecAsync({
2678
+ db,
2679
+ sql: (`
2680
+ SELECT * FROM sqlite_schema WHERE type = 'table' ORDER BY tbl_name;
2681
+ `)
2682
+ })
2683
+ )[0];
2684
+ if (!dbtableList) {
2685
+ return;
2686
+ }
2687
+ dbtableList = new Map(dbtableList.map(function ({
2688
+ colList = [],
2689
+ dbtableFullname,
2690
+ rowCount = 0,
2691
+ rowList0,
2692
+ sql,
2693
+ tbl_name
2694
+ }, ii) {
2695
+ dbtableFullname = dbtableFullname || `${dbName}.[${tbl_name}]`;
2696
+ dbtableIi += 1;
2697
+ baton = {
2698
+ colList,
2699
+ db,
2700
+ dbName,
2701
+ dbtableFullname,
2702
+ dbtableIi,
2703
+ dbtableName: `[${tbl_name}]`,
2704
+ hashtag: `dbtable_${String(dbtableIi).padStart(2, "0")}`,
2705
+ ii,
2706
+ isDbchart,
2707
+ isDbmain,
2708
+ isDbquery,
2709
+ rowCount,
2710
+ rowList0,
2711
+ sortCol: 0,
2712
+ sortDir: "asc",
2713
+ sql,
2714
+ title: `table ${dbtableFullname}`
2715
+ };
2716
+ DBTABLE_DICT.set(baton.hashtag, baton);
2717
+ return [
2718
+ baton.dbtableName, baton
2719
+ ];
2720
+ }));
2721
+ tmp = "";
2722
+ dbtableList.forEach(function ({
2723
+ dbtableName,
2724
+ hashtag
2725
+ }) {
2726
+ tmp += (`
2727
+ SELECT '${hashtag}' AS hashtag;
2728
+ PRAGMA table_info(${dbtableName});
2729
+ SELECT COUNT(*) AS rowcount FROM ${dbtableName};
2730
+ `);
2731
+ });
2732
+ tmp = await dbExecAsync({
2733
+ db,
2734
+ sql: tmp
2735
+ });
2736
+ tmp.forEach(function (rowList) {
2737
+ let row0 = rowList[0];
2738
+ if (!row0) {
2739
+ return;
2740
+ }
2741
+ [
2742
+ "cid", "hashtag", "rowcount"
2743
+ ].forEach(function (key) {
2744
+ switch (row0.hasOwnProperty(key) && key) {
2745
+ case "cid":
2746
+ baton.colList = [
2747
+ "rowid",
2748
+ rowList.map(function ({
2749
+ name
2750
+ }) {
2751
+ return name;
2752
+ })
2753
+ ].flat();
2754
+ break;
2755
+ case "hashtag":
2756
+ baton = DBTABLE_DICT.get(row0.hashtag);
2757
+ break;
2758
+ case "rowcount":
2759
+ baton.rowCount = row0.rowcount;
2760
+ break;
2761
+ }
2762
+ });
2763
+ });
2764
+ db.dbtableList = dbtableList;
2765
+ }));
2766
+ // ui-render databases and tables to html
2767
+ document.querySelector("#dbchartList1").innerHTML = "";
2768
+ document.querySelector("#dbqueryList1").innerHTML = "";
2769
+ document.querySelector("#dbtableList1").innerHTML = "";
2770
+ DB_DICT.forEach(function ({
2771
+ dbtableList,
2772
+ isDbchart,
2773
+ isDbquery
2774
+ }) {
2775
+ dbtableList.forEach(function (baton) {
2776
+ // create uitable
2777
+ document.querySelector(
2778
+ isDbchart
2779
+ ? "#dbchartList1"
2780
+ : isDbquery
2781
+ ? "#dbqueryList1"
2782
+ : "#dbtableList1"
2783
+ ).appendChild(uitableCreate(baton));
2784
+ });
2785
+ });
2786
+ // ui-render #tocPanel1
2787
+ html = "";
2788
+ DB_DICT.forEach(function ({
2789
+ dbName,
2790
+ dbtableList,
2791
+ isDbchart,
2792
+ isDbquery
2793
+ }) {
2794
+ html += `<div class="tocTitle">` + (
2795
+ isDbchart
2796
+ ? "chart"
2797
+ : isDbquery
2798
+ ? `query result`
2799
+ : `database ${dbName}`
2800
+ ) + `</div>`;
2801
+ dbtableList.forEach(function ({
2802
+ colList,
2803
+ dbtableFullname,
2804
+ dbtableName,
2805
+ hashtag,
2806
+ ii,
2807
+ rowCount
2808
+ }) {
2809
+ html += `<a class="tocElemA"`;
2810
+ html += ` data-hashtag="${hashtag}"`;
2811
+ html += ` href="#${hashtag}"`;
2812
+ html += (
2813
+ isDbchart
2814
+ ? ` data-dbtype="chart"`
2815
+ : isDbquery
2816
+ ? ` data-dbtype="query"`
2817
+ : ` data-dbtype="table"`
2818
+ );
2819
+ html += (
2820
+ ` title="`
2821
+ + stringHtmlSafe((
2822
+ `right-click for crud operation\n\n`
2823
+ ) + JSON.stringify({
2824
+ dbtableFullname,
2825
+ rowCount,
2826
+ colList //jslint-ignore-line
2827
+ }, undefined, 4))
2828
+ + `"`
2829
+ );
2830
+ html += `>${ii + 1}. `;
2831
+ html += (
2832
+ isDbchart
2833
+ ? "chart"
2834
+ : isDbquery
2835
+ ? "query"
2836
+ : "table"
2837
+ );
2838
+ html += ` ${stringHtmlSafe(dbtableName.slice(1, -1))}</a>\n`;
2839
+ });
2840
+ });
2841
+ document.querySelector("#tocDbList1").innerHTML = html;
2842
+ // restore window.scrollY
2843
+ window.scroll({
2844
+ behavior: "smooth",
2845
+ top: windowScrollY
2846
+ });
2847
+ uitableInitWithinView({});
2848
+ }
2849
+
2850
+ async function uiTryCatch(func, ...argList) {
2851
+ // this function will call <func> in a try-catch-block
2852
+ // that will display any error thrown to user
2853
+ try {
2854
+ UI_LOADING_COUNTER += 1;
2855
+ uiFadeIn(UI_LOADING);
2856
+ await func(...argList);
2857
+ } catch (err) {
2858
+ console.error(err);
2859
+ document.querySelector(
2860
+ "#errorPanel1 .modalContent"
2861
+ ).textContent = err;
2862
+ uiFadeIn(document.querySelector(
2863
+ "#errorPanel1"
2864
+ ));
2865
+ } finally {
2866
+ await waitAsync(500);
2867
+ UI_LOADING_COUNTER -= 1;
2868
+ if (
2869
+ UI_LOADING_COUNTER === 0
2870
+ && UI_LOADING.style.visibility === "visible"
2871
+ ) {
2872
+ uiFadeOut(UI_LOADING);
2873
+ }
2874
+ }
2875
+ }
2876
+
2877
+ async function uichartCreate(baton) {
2878
+ // this function will create xy-line-chart from given sqlite table <baton>
2879
+ // init pre-var
2880
+ let {
2881
+ contentElem,
2882
+ db,
2883
+ dbtableName
2884
+ } = baton;
2885
+ let elemCanvasFlex;
2886
+ let elemLegend;
2887
+ let elemLegendWidth = 256;
2888
+ let elemUichart = contentElem.querySelector(".uichart");
2889
+ let elemUichartHeight = 384;
2890
+ let uichart = await dbExecAsync({
2891
+ db,
2892
+ sql: (`
2893
+ -- table - __chart_options1 - insert
2894
+ DROP TABLE IF EXISTS __chart_options1;
2895
+ CREATE TEMP TABLE __chart_options1 AS
2896
+ SELECT
2897
+ JSON(options) AS options,
2898
+ CASTREALORZERO(options->>'$.xstep') AS xstep,
2899
+ CASTREALORZERO(1.0 / options->>'$.xstep') AS xstep_inv,
2900
+ CASTREALORZERO(options->>'$.ystep') AS ystep,
2901
+ CASTREALORZERO(1.0 / options->>'$.ystep') AS ystep_inv
2902
+ FROM (
2903
+ SELECT options FROM ${dbtableName} WHERE datatype = 'options' LIMIT 1
2904
+ );
2905
+
2906
+ -- table - __chart_series_xy1 - insert
2907
+ DROP TABLE IF EXISTS __chart_series_xy1;
2908
+ CREATE TEMP TABLE __chart_series_xy1 AS
2909
+ SELECT
2910
+ series_index,
2911
+ xx,
2912
+ LAG(xx, 1, NULL) OVER (
2913
+ PARTITION BY series_index ORDER BY xx
2914
+ ) AS xx_lag,
2915
+ LEAD(xx, 1, NULL) OVER (
2916
+ PARTITION BY series_index ORDER BY xx
2917
+ ) AS xx_lead,
2918
+ yy
2919
+ FROM (
2920
+ SELECT
2921
+ series_index,
2922
+ xx,
2923
+ yy
2924
+ FROM (
2925
+ SELECT
2926
+ ROW_NUMBER() OVER (
2927
+ PARTITION BY series_index, xx ORDER BY rowid DESC
2928
+ ) AS rownum,
2929
+ series_index,
2930
+ xx,
2931
+ yy
2932
+ FROM (
2933
+ SELECT
2934
+ ${dbtableName}.rowid,
2935
+ --
2936
+ series_index,
2937
+ IIF(xstep_inv, ROUND(xstep_inv * xx), xx) AS xx,
2938
+ IIF(ystep_inv, ROUND(ystep_inv * yy), yy) AS yy
2939
+ FROM ${dbtableName}
2940
+ JOIN __chart_options1
2941
+ WHERE
2942
+ datatype = 'yy_value'
2943
+ AND xx IS NOT NULL
2944
+ )
2945
+ ORDER BY
2946
+ series_index,
2947
+ xx
2948
+ )
2949
+ WHERE
2950
+ rownum = 1
2951
+ );
2952
+
2953
+ -- table - __chart_series_maxmin1 - insert
2954
+ DROP TABLE IF EXISTS __chart_series_maxmin1;
2955
+ CREATE TEMP TABLE __chart_series_maxmin1 AS
2956
+ SELECT
2957
+ series_index,
2958
+ MAX(xx) AS xdataMax,
2959
+ MIN(xx) AS xdataMin,
2960
+ MAX(yy) AS ydataMax,
2961
+ MIN(yy) AS ydataMin
2962
+ FROM __chart_series_xy1
2963
+ WHERE
2964
+ yy IS NOT NULL
2965
+ GROUP BY series_index;
2966
+
2967
+ -- table - __chart_options1 - select - options
2968
+ SELECT
2969
+ JSON_SET(
2970
+ options,
2971
+ '$.seriesList', JSON(COALESCE(seriesList, '[]')),
2972
+ '$.xdataDxx', COALESCE(xdataDxx, 1),
2973
+ '$.xdataMax', xdataMax,
2974
+ '$.xdataMin', xdataMin,
2975
+ '$.xlabelList', JSON(COALESCE(xlabelList, '[]')),
2976
+ '$.ydataMax', ydataMax,
2977
+ '$.ydataMin', ydataMin
2978
+ ) AS options
2979
+ FROM __chart_options1
2980
+ JOIN (
2981
+ SELECT
2982
+ JSON_GROUP_ARRAY(xx_label) AS xlabelList
2983
+ FROM ${dbtableName}
2984
+ WHERE
2985
+ datatype = 'xx_label'
2986
+ )
2987
+ JOIN (
2988
+ SELECT
2989
+ JSON_GROUP_ARRAY(JSON(JSON_SET(
2990
+ COALESCE(options, '{}'),
2991
+ '$.seriesName', series_label,
2992
+ '$.xdata', JSON(COALESCE(xdata, '[]')),
2993
+ '$.ydata', JSON(COALESCE(ydata, '[]'))
2994
+ ))) AS seriesList
2995
+ FROM (
2996
+ SELECT
2997
+ options,
2998
+ series_index,
2999
+ (
3000
+ ROW_NUMBER() OVER (ORDER BY series_index)
3001
+ || '. '
3002
+ || series_label
3003
+ ) AS series_label
3004
+ FROM ${dbtableName}
3005
+ WHERE
3006
+ datatype = 'series_label'
3007
+ ORDER BY
3008
+ series_index
3009
+ )
3010
+ -- calculate series.xdata, series.ydata
3011
+ LEFT JOIN (
3012
+ SELECT
3013
+ series_index,
3014
+ JSON_GROUP_ARRAY(xx) AS xdata,
3015
+ JSON_GROUP_ARRAY(yy) AS ydata
3016
+ FROM __chart_series_xy1
3017
+ GROUP BY series_index
3018
+ ) USING (series_index)
3019
+ )
3020
+ JOIN (
3021
+ SELECT
3022
+ MAX(xdataMax) AS xdataMax,
3023
+ MIN(xdataMin) AS xdataMin,
3024
+ MAX(ydataMax) AS ydataMax,
3025
+ MIN(ydataMin) AS ydataMin
3026
+ FROM __chart_series_maxmin1
3027
+ )
3028
+ -- calculate uichart.xdataDxx
3029
+ JOIN (
3030
+ SELECT
3031
+ MIN(ABS(xx - xx_prv)) AS xdataDxx
3032
+ FROM (
3033
+ SELECT
3034
+ xx,
3035
+ LAG(xx, 1, NULL) OVER (ORDER BY xx) AS xx_prv
3036
+ FROM (
3037
+ SELECT DISTINCT
3038
+ xx
3039
+ FROM __chart_series_xy1
3040
+ )
3041
+ )
3042
+ );
3043
+ `)
3044
+ });
3045
+ uichart = JSON.parse(uichart[0][0].options);
3046
+ contentElem.querySelector(".uitable").style.display = "none";
3047
+ elemUichart.style.display = "flex";
3048
+ elemUichart.style.height = `${elemUichartHeight}px`;
3049
+ elemUichart.innerHTML = (`
3050
+ <div
3051
+ class="uichartNav"
3052
+ style="height: ${elemUichartHeight}px; width: ${elemLegendWidth}px;"
3053
+ >
3054
+ <button
3055
+ class="uichartAction"
3056
+ data-action="uichartZoomReset"
3057
+ >reset zoom</button>
3058
+ <button
3059
+ class="uichartAction"
3060
+ data-action="uichartSeriesHideAll"
3061
+ >hide all</button>
3062
+ <button
3063
+ class="uichartAction"
3064
+ data-action="uichartSeriesShowAll"
3065
+ >show all</button>
3066
+ <div
3067
+ class="uichartLegend"
3068
+ style="height: ${elemUichartHeight - 64}px;"
3069
+ ></div>
3070
+ </div>
3071
+ <div style="position: relative; margin-left: 16px; width: 16px;">
3072
+ <div class="uichartAxislabel1">${stringHtmlSafe(uichart.yaxisTitle)}</div>
3073
+ </div>
3074
+ <div style="display: flex; flex: 1; flex-direction: column; padding: 5px 0;">
3075
+ <div class="uichartTitle">
3076
+ ${
3077
+ stringHtmlSafe(uichart.title).replace((
3078
+ /\n/g
3079
+ ), "<br>")
3080
+ }
3081
+ </div>
3082
+ <div class="uichartCanvasFlex" style="flex: 1;">
3083
+ <svg
3084
+ class="uichartCanvasFixed"
3085
+ version="1.1"
3086
+ xmlns="http://www.w3.org/2000/svg"
3087
+ >
3088
+ <clipPath class="uichartClip" id="uichartClip${baton.dbtableIi}">
3089
+ <rect fill="none" height="0" width="0" x="0" y="0"></rect>
3090
+ </clipPath>
3091
+ <g class="uichartGridlineList"></g>
3092
+ <g class="uichartAxistickList"></g>
3093
+ <g class="uichartSeriesList"></g>
3094
+ <g class="uichartCrosshairList">
3095
+ <path stroke-width="1" stroke="#333" visibility="hidden"></path>
3096
+ <path stroke-width="1" stroke="#333" visibility="hidden"></path>
3097
+ </g>
3098
+ <g class="uichartTooltip" visibility="hidden">
3099
+ <rect
3100
+ class="uichartTooltipBorder"
3101
+ fill-opacity="0.8000"
3102
+ fill="#fff"
3103
+ rx="5"
3104
+ ry="5"
3105
+ stroke-width="3"
3106
+ x="0"
3107
+ y="0"
3108
+ >
3109
+ </rect>
3110
+ <text class="uichartTooltipText"></text>
3111
+ </g>
3112
+ <g class="uichartMousetrackerList">/g>
3113
+ </svg>
3114
+ </div>
3115
+ <div class="uichartAxislabel0">${stringHtmlSafe(uichart.xaxisTitle)}</div>
3116
+ </div>
3117
+ `);
3118
+ elemCanvasFlex = elemUichart.querySelector(".uichartCanvasFlex");
3119
+ elemLegend = elemUichart.querySelector(".uichartLegend");
3120
+ // init var
3121
+ let ELEM_GRAPH_LINE_WIDTH = 1; //jslint-ignore-line
3122
+ let ELEM_POINT_BORDER_WIDTH = 0;
3123
+ let ELEM_POINT_RADIUS = 4;
3124
+ let canvasHeight = elemCanvasFlex.clientHeight;
3125
+ let canvasWidth = elemCanvasFlex.clientWidth;
3126
+ let counterColor = 1;
3127
+ let counterShape = 1;
3128
+ let elemCanvasFixed = elemCanvasFlex.firstElementChild;
3129
+ let [
3130
+ elemClip,
3131
+ elemGridlineList,
3132
+ elemAxistickList,
3133
+ elemSeriesList,
3134
+ elemCrosshairList,
3135
+ elemTooltip,
3136
+ elemMousetrackerList
3137
+ ] = elemCanvasFixed.children;
3138
+ let [
3139
+ elemTooltipBorder,
3140
+ elemTooltipText
3141
+ ] = elemTooltip.children;
3142
+ let {
3143
+ isBarchart,
3144
+ seriesList,
3145
+ xdataDxx,
3146
+ xdataMax,
3147
+ xdataMin,
3148
+ xlabelList
3149
+ } = uichart;
3150
+ let isResizing = 0;
3151
+ let plotBottom;
3152
+ let plotHeight;
3153
+ let plotLeft;
3154
+ let plotRight;
3155
+ let plotTop;
3156
+ let plotWidth;
3157
+ let plotbarMargin;
3158
+ let plotbarYaxis;
3159
+ let pointHovered;
3160
+ let pointListBarchart = [];
3161
+ let redrawTimer;
3162
+ let seriesColorList = [
3163
+ /*
3164
+ // highcharts-v4
3165
+ "#7cb5ec", // light-blue - highcharts-v4
3166
+ "#434348", // black - highcharts-v4
3167
+ "#90ed7d", // light-green - highcharts-v4
3168
+ "#f7a35c", // orange - highcharts-v4
3169
+ "#8085e9", // lavender-blue - highcharts-v4
3170
+ "#f15c80", // pink - highcharts-v4
3171
+ "#e4d354", // yellow - highcharts-v4
3172
+ "#2b908f", // teal - highcharts-v4
3173
+ "#f45b5b", // orange-red - highcharts-v4
3174
+ "#91e8e1" // aqua - highcharts-v4
3175
+ // highcharts-v3
3176
+ "#2f7ed8", // blue - highcharts-v3
3177
+ "#0d233a", // dark-teal - highcharts-v3
3178
+ "#8bbc21", // olive - highcharts-v3
3179
+ "#910000", // maroon - highcharts-v3
3180
+ "#1aadce", // aqua - highcharts-v3
3181
+ "#492970", // purple - highcharts-v3
3182
+ "#f28f43", // orange - highcharts-v3
3183
+ "#77a1e5", // light-blue - highcharts-v3
3184
+ "#c42525", // red - highcharts-v3
3185
+ "#a6c96a" // light-olive - highcharts-v3
3186
+ // highcharts-v2
3187
+ "#4572a7", // blue - highcharts-v2
3188
+ "#aa4643", // red - highcharts-v2
3189
+ "#89a54e", // olive - highcharts-v2
3190
+ "#80699b", // purple - highcharts-v2
3191
+ "#3d96ae", // aqua - highcharts-v2
3192
+ "#db843d", // orange - highcharts-v2
3193
+ "#92a8cd", // light-navy - highcharts-v2
3194
+ "#a47d7c", // brown - highcharts-v2
3195
+ "#b5ca92" // light-olive - highcharts-v2
3196
+ */
3197
+ //
3198
+ // "#aa4643", // red - highcharts-v2
3199
+ "#a44", // red
3200
+ // "#4572a7", // blue - highcharts-v2
3201
+ "#57b", // blue
3202
+ "#7ecf6d", // light-green - highcharts-v4
3203
+ // "#80699b", // purple - highcharts-v2
3204
+ "#86a", // purple
3205
+ //
3206
+ "#3d96ae", // aqua - highcharts-v2
3207
+ "#f15c80", // pink - highcharts-v4
3208
+ "#8085e9", // lavender-blue - highcharts-v4
3209
+ // "#a47d7c", // brown - highcharts-v2
3210
+ // "#b88", // brown
3211
+ "#89a54e", // olive - highcharts-v2
3212
+ // "#0d233a", // dark-teal - highcharts-v3
3213
+ "#357", // dark-teal
3214
+ "#492970" // purple - highcharts-v3
3215
+ ];
3216
+ let seriesHovered;
3217
+ let seriesShapeList = [
3218
+ // "circle", "diamond", "square", "triangle", "triangle-down"
3219
+ // "circle",
3220
+ "square",
3221
+ "triangle",
3222
+ "diamond",
3223
+ "triangle-down"
3224
+ ];
3225
+ let xaxisMax;
3226
+ let xaxisMin;
3227
+ let xaxistickDict = {};
3228
+ let xpixelToPointDictHovered = [];
3229
+ let xtransOffset;
3230
+ let xtransWidth;
3231
+ let xzoomMax;
3232
+ let xzoomMin;
3233
+ let yaxisMax;
3234
+ let yaxisMin;
3235
+ let yaxistickDict = {};
3236
+ let ytransWidth;
3237
+ function axistickCreate(isXaxis, tickx) {
3238
+ // this function will create <elemGridline>, <elemTick>, <elemTicklabel>
3239
+ // at given <tickx> in <axis>
3240
+ let elemTick;
3241
+ let elemTicklabel;
3242
+ let elemTspan;
3243
+ // create elemTick
3244
+ elemTick = svgAttrSet("path", {
3245
+ fill: "none",
3246
+ stroke: "#33b",
3247
+ "stroke-width": 3
3248
+ });
3249
+ elemTick.isFirstDraw = true;
3250
+ elemTick.isXaxis = isXaxis;
3251
+ // if isXaxis, then draw tick
3252
+ if (isXaxis) {
3253
+ elemAxistickList.appendChild(elemTick);
3254
+ // create elemGridline
3255
+ } else {
3256
+ elemTick.elemGridline = svgAttrSet("path", {
3257
+ fill: "none",
3258
+ stroke: "#999",
3259
+ "stroke-width": 1
3260
+ });
3261
+ elemGridlineList.appendChild(elemTick.elemGridline);
3262
+ }
3263
+ // skip ticklabel, if elemTick is not contained in xlabelList
3264
+ if (
3265
+ isXaxis
3266
+ && xlabelList.length
3267
+ && !xlabelList.hasOwnProperty(tickx - 1)
3268
+ ) {
3269
+ return elemTick;
3270
+ }
3271
+ // create ticklabel
3272
+ elemTspan = svgAttrSet("tspan");
3273
+ elemTspan.textContent = (function () {
3274
+ let num;
3275
+ let numDigitList;
3276
+ let xOrY = (
3277
+ isXaxis
3278
+ ? "x"
3279
+ : "y"
3280
+ );
3281
+ num = numberFormat({
3282
+ convert: uichart[xOrY + "valueConvert"],
3283
+ modeTick: true,
3284
+ num: (
3285
+ isXaxis
3286
+ ? xlabelList[tickx - 1] ?? tickx
3287
+ : tickx
3288
+ ),
3289
+ prefix: uichart[xOrY + "valuePrefix"],
3290
+ step: uichart[xOrY + "step"],
3291
+ suffix: uichart[xOrY + "valueSuffix"]
3292
+ }).slice(0, 16);
3293
+ // number already formatted
3294
+ if (typeof num !== "number") {
3295
+ return num;
3296
+ }
3297
+ // small number
3298
+ if (Math.abs(num) < 1000) {
3299
+ return num.toLocaleString();
3300
+ }
3301
+ // large number
3302
+ num = Math.round(num).toLocaleString();
3303
+ numDigitList = num.split(
3304
+ /[^+\-0-9]/
3305
+ );
3306
+ switch (numDigitList.length) {
3307
+ // kilo
3308
+ case 2:
3309
+ return numDigitList[0] + "k";
3310
+ // mega
3311
+ case 3:
3312
+ return numDigitList[0] + "M";
3313
+ // giga
3314
+ case 4:
3315
+ return numDigitList[0] + "G";
3316
+ // tera
3317
+ case 5:
3318
+ return numDigitList[0] + "T";
3319
+ // peta
3320
+ case 6:
3321
+ return numDigitList[0] + "P";
3322
+ // exa
3323
+ case 7:
3324
+ return numDigitList[0] + "E";
3325
+ default:
3326
+ return num;
3327
+ }
3328
+ }());
3329
+ elemTicklabel = svgAttrSet("text", {
3330
+ "text-anchor": (
3331
+ isXaxis
3332
+ ? "middle"
3333
+ : "end"
3334
+ )
3335
+ });
3336
+ elemTick.elemTicklabel = elemTicklabel;
3337
+ elemTicklabel.fx_rotate = isXaxis;
3338
+ elemTicklabel.appendChild(elemTspan);
3339
+ elemAxistickList.appendChild(elemTicklabel);
3340
+ return elemTick;
3341
+ }
3342
+ function floatCorrect(num) {
3343
+ // this function will correct float-error in <num>
3344
+ return parseFloat(
3345
+ num.toPrecision(12)
3346
+ );
3347
+ }
3348
+ function numberFormat({
3349
+ convert,
3350
+ modeTick,
3351
+ num,
3352
+ prefix,
3353
+ step,
3354
+ suffix
3355
+ }) {
3356
+ // this function will format <num>
3357
+ if (step && typeof num === "number") {
3358
+ num *= step;
3359
+ }
3360
+ switch (convert) {
3361
+ case "juliandayToDate":
3362
+ num = new Date((num - 2440587.5) * 86400 * 1000);
3363
+ // num = num.toUTCString().slice(0, 16);
3364
+ num = (
3365
+ modeTick
3366
+ ? num.toUTCString().slice(5, 16)
3367
+ : num.toUTCString().slice(0, 16)
3368
+ );
3369
+ break;
3370
+ case "unixepochToTimelocal":
3371
+ num = new Date(num * 1000);
3372
+ num = (
3373
+ modeTick
3374
+ ? num.toLocaleTimeString()
3375
+ : num.toLocaleString()
3376
+ );
3377
+ break;
3378
+ case "unixepochToTimeutc":
3379
+ // num = new Date(num * 1000).toISOString().slice(11, 19) + "Z";
3380
+ num = new Date(num * 1000);
3381
+ num = (
3382
+ modeTick
3383
+ ? num.toUTCString().slice(17)
3384
+ : num.toUTCString()
3385
+ );
3386
+ break;
3387
+ }
3388
+ if (prefix) {
3389
+ num = prefix + num;
3390
+ }
3391
+ if (suffix) {
3392
+ num += suffix;
3393
+ }
3394
+ return String(num);
3395
+ }
3396
+ function onCanvasZoom(evt) {
3397
+ // this function will zoom/un-zoom at current mouse-location on canvas-area
3398
+ let xmid = 0.5;
3399
+ let xscale = 1.2500; // zoom-out
3400
+ evt.preventDefault();
3401
+ evt.stopPropagation();
3402
+ if (!evt.modeDebounce) {
3403
+ debounce("onCanvasZoom", onCanvasZoom, Object.assign(evt, {
3404
+ modeDebounce: true
3405
+ }));
3406
+ return;
3407
+ }
3408
+ if (evt.deltaY < 0) {
3409
+ xmid = (
3410
+ evt.pageX
3411
+ - elemCanvasFixed.getBoundingClientRect().left
3412
+ - window.scrollX
3413
+ + document.documentElement.clientLeft
3414
+ - plotLeft
3415
+ ) / plotWidth;
3416
+ xscale = 0.8000; // zoom-in
3417
+ }
3418
+ xmid += 0.5 * (xmid - 0.4000);
3419
+ xmid = xaxisMin + xmid * (xaxisMax - xaxisMin);
3420
+ xzoomMax = xmid + xscale * (xaxisMax - xmid);
3421
+ xzoomMin = xmid + xscale * (xaxisMin - xmid);
3422
+ // uichartRedraw - uichartZoom
3423
+ uichartRedraw();
3424
+ }
3425
+ function onPointHover(evt) {
3426
+ // this function will handle <evt> when mouse hover over point
3427
+ let mouseX;
3428
+ let pointObj;
3429
+ let rect;
3430
+ let tooltipBbox;
3431
+ let tooltipHeight;
3432
+ let tooltipMargin = 32;
3433
+ let tooltipWidth;
3434
+ let tooltipX;
3435
+ let tooltipY;
3436
+ // get mouse-position
3437
+ rect = elemCanvasFixed.getBoundingClientRect();
3438
+ mouseX = Math.round(evt.pageX - rect.left - window.scrollX - plotLeft);
3439
+ // if mouse is outside canvas-area, then return
3440
+ if (!(0 <= mouseX && mouseX <= plotWidth)) {
3441
+ return;
3442
+ }
3443
+ // get closest-series-point to mouse as pointHovered
3444
+ pointObj = xpixelToPointDictHovered[mouseX];
3445
+ if (
3446
+ !pointObj
3447
+ || pointObj === pointHovered
3448
+ || pointObj.series.isHidden
3449
+ ) {
3450
+ return;
3451
+ }
3452
+ pointHovered = pointObj;
3453
+ // redraw seriesHovered
3454
+ onSeriesHover(pointObj.series, true);
3455
+ // redraw tooltip around pointHovered
3456
+ let { //jslint-ignore-line
3457
+ pointX,
3458
+ pointY,
3459
+ xval,
3460
+ yval
3461
+ } = pointHovered;
3462
+ let xlabel;
3463
+ let ylabel;
3464
+ if (redrawTimer || pointY === undefined) {
3465
+ return;
3466
+ }
3467
+ xlabel = numberFormat({
3468
+ convert: uichart.xvalueConvert,
3469
+ num: xlabelList[xval - 1] ?? xval,
3470
+ prefix: uichart.xvaluePrefix,
3471
+ step: uichart.xstep,
3472
+ suffix: uichart.xvalueSuffix
3473
+ });
3474
+ ylabel = numberFormat({
3475
+ convert: uichart.yvalueConvert,
3476
+ num: yval,
3477
+ prefix: uichart.yvaluePrefix,
3478
+ step: uichart.ystep,
3479
+ suffix: uichart.yvalueSuffix
3480
+ });
3481
+ // update elemTooltipText
3482
+ elemTooltip.setAttribute("visibility", "visible");
3483
+ elemTooltipText.innerHTML = (`
3484
+ <tspan dy="17" x="6">${stringHtmlSafe(seriesHovered.seriesName)}</tspan>
3485
+ <tspan dy="17" x="6">x: ${stringHtmlSafe(xlabel)}</tspan>
3486
+ <tspan
3487
+ dy="19"
3488
+ style="font-size: 14px; font-weight: bold;"
3489
+ x="6"
3490
+ >y: ${stringHtmlSafe(ylabel)}</tspan>
3491
+ `);
3492
+ // update elemTooltipBorder after text-update
3493
+ tooltipBbox = elemTooltipText.getBBox();
3494
+ tooltipWidth = tooltipBbox.width + 10;
3495
+ tooltipHeight = tooltipBbox.height + 10;
3496
+ svgAttrSet(elemTooltipBorder, {
3497
+ height: tooltipHeight,
3498
+ stroke: seriesHovered.seriesColor,
3499
+ width: tooltipWidth
3500
+ });
3501
+ // calculate tooltipX
3502
+ tooltipX = pointX + plotLeft - 0.5 * tooltipWidth;
3503
+ tooltipX = Math.max(tooltipX, plotLeft + tooltipMargin);
3504
+ tooltipX = Math.min(
3505
+ tooltipX,
3506
+ canvasWidth - tooltipWidth - tooltipMargin
3507
+ );
3508
+ // calculate tooltipY
3509
+ tooltipY = pointY + plotTop - tooltipHeight - tooltipMargin;
3510
+ if (tooltipY < plotTop) {
3511
+ tooltipY = pointY + plotTop + tooltipMargin;
3512
+ }
3513
+ // animate-move tooltip to tooltipX, tooltipY
3514
+ svgAnimate(elemTooltip, {
3515
+ translateX: tooltipX,
3516
+ translateY: tooltipY
3517
+ }, "easeout");
3518
+ // animate-move crosshair
3519
+ [
3520
+ true, false
3521
+ ].forEach(function (isXaxis, ii) {
3522
+ let d;
3523
+ if (isXaxis) {
3524
+ d = xaxisTranslate(xval) + CRISPX;
3525
+ d = `M ${d} 0 L ${d} ${canvasHeight}`;
3526
+ } else {
3527
+ d = yaxisTranslate(yval) + CRISPY;
3528
+ d = `M 0 ${d} L ${canvasWidth} ${d}`;
3529
+ }
3530
+ svgAnimate(elemCrosshairList.children[ii], {
3531
+ d,
3532
+ visibility: "visible"
3533
+ }, "easeout");
3534
+ });
3535
+ }
3536
+ function onPointUnhover() {
3537
+ // this function will handle <evt> when mouse un-hover from point
3538
+ onSeriesUnhover();
3539
+ pointHovered = undefined;
3540
+ // hide elemcrosshairList, elemTooltip
3541
+ elemCrosshairList.children[0].setAttribute("visibility", "hidden");
3542
+ elemCrosshairList.children[1].setAttribute("visibility", "hidden");
3543
+ elemTooltip.setAttribute("visibility", "hidden");
3544
+ }
3545
+ function onSeriesHover(evt, scrollTo) {
3546
+ // this function will handle <evt> when mouse hover over series
3547
+ let series = (
3548
+ evt.target
3549
+ ? seriesList[
3550
+ evt.target.closest("[data-series-ii]")?.dataset.seriesIi
3551
+ ]
3552
+ : evt
3553
+ );
3554
+ let seriesColor;
3555
+ if (!series || series.isHidden) {
3556
+ onSeriesUnhover();
3557
+ return;
3558
+ }
3559
+ // un-hover previous seriesHovered
3560
+ if (series !== seriesHovered) {
3561
+ onSeriesUnhover();
3562
+ }
3563
+ seriesHovered = series;
3564
+ // darken series-color
3565
+ if (isBarchart) {
3566
+ seriesColor = series.seriesColor.replace((
3567
+ /^#(..)(..)(..)$/
3568
+ ), function (ignore, rr, gg, bb) {
3569
+ return (
3570
+ "rgb("
3571
+ + Math.round(0.5 * parseInt(rr, 16)) + ","
3572
+ + Math.round(0.5 * parseInt(gg, 16)) + ","
3573
+ + Math.round(0.5 * parseInt(bb, 16))
3574
+ + ")"
3575
+ );
3576
+ });
3577
+ seriesHovered.pointListSeries.forEach(function (pointObj) {
3578
+ pointObj.elemPoint.setAttribute("fill", seriesColor);
3579
+ });
3580
+ }
3581
+ // thicken series-line-width
3582
+ if (!isBarchart) {
3583
+ xpixelToPointDictHovered = series.xpixelToPointDict;
3584
+ svgAttrSet(seriesHovered.elemGraph, {
3585
+ "stroke-width": ELEM_GRAPH_LINE_WIDTH + 2
3586
+ });
3587
+ }
3588
+ Array.from(elemLegend.children).forEach(function (elem, seriesIi) {
3589
+ if (seriesIi !== seriesHovered.seriesIi) {
3590
+ elem.style.background = "none";
3591
+ return;
3592
+ }
3593
+ // hover series in legend
3594
+ elem.style.background = "#ccc";
3595
+ // scroll to series in legend
3596
+ if (scrollTo) {
3597
+ debounce("onSeriesScroll", function () {
3598
+ elemLegend.scroll({
3599
+ behavior: "smooth",
3600
+ top: elem.offsetTop - elemLegend.offsetHeight
3601
+ });
3602
+ });
3603
+ }
3604
+ });
3605
+ }
3606
+ function onSeriesHoveredHide() {
3607
+ // this function will handle <evt> to hide hovered-series
3608
+ if (seriesHovered) {
3609
+ uichartSeriesHideOrShow(seriesHovered, true);
3610
+ uichartRedraw();
3611
+ return;
3612
+ }
3613
+ }
3614
+ function onSeriesUnhover() {
3615
+ // this function will handle <evt> when mouse un-hover from series
3616
+ let seriesColor;
3617
+ if (!seriesHovered) {
3618
+ return;
3619
+ }
3620
+ // un-darken series-color
3621
+ if (isBarchart) {
3622
+ seriesColor = seriesHovered.seriesColor;
3623
+ seriesHovered.pointListSeries.forEach(function (pointObj) {
3624
+ pointObj.elemPoint.setAttribute("fill", seriesColor);
3625
+ });
3626
+ }
3627
+ // un-thicken series-line-width
3628
+ if (!isBarchart) {
3629
+ svgAttrSet(seriesHovered.elemGraph, {
3630
+ "stroke-width": ELEM_GRAPH_LINE_WIDTH
3631
+ });
3632
+ }
3633
+ // un-hover series in legend
3634
+ elemLegend.children[seriesHovered.seriesIi].style.background = "none";
3635
+ seriesHovered = undefined;
3636
+ }
3637
+ async function onUichartAction(evt) {
3638
+ // this function will handle uichart event <evt>
3639
+ let action;
3640
+ let modeHide;
3641
+ let target;
3642
+ evt.preventDefault();
3643
+ evt.stopPropagation();
3644
+ if (!evt.modeDebounce) {
3645
+ debounce("onUichartAction", onUichartAction, Object.assign(evt, {
3646
+ modeDebounce: true
3647
+ }));
3648
+ return;
3649
+ }
3650
+ target = evt.target.closest("[data-action]");
3651
+ if (!target) {
3652
+ return;
3653
+ }
3654
+ action = target.dataset.action;
3655
+ switch (action) {
3656
+ case "uichartSeriesHideAll":
3657
+ case "uichartSeriesShowAll":
3658
+ uiFadeIn(baton.elemLoading);
3659
+ await waitAsync(50);
3660
+ modeHide = action === "uichartSeriesHideAll";
3661
+ // hide or show series
3662
+ seriesList.forEach(function (series) {
3663
+ uichartSeriesHideOrShow(series, modeHide);
3664
+ });
3665
+ uichartRedraw();
3666
+ await waitAsync(200);
3667
+ uiFadeOut(baton.elemLoading);
3668
+ return;
3669
+ case "uichartSeriesHideOrShow":
3670
+ modeHide = target.dataset.hidden !== "1";
3671
+ // hide or show series
3672
+ uichartSeriesHideOrShow(
3673
+ seriesList[target.dataset.seriesIi],
3674
+ modeHide
3675
+ );
3676
+ uichartRedraw();
3677
+ return;
3678
+ case "uichartZoomReset":
3679
+ uiFadeIn(baton.elemLoading);
3680
+ await waitAsync(50);
3681
+ xzoomMax = undefined;
3682
+ xzoomMin = undefined;
3683
+ // uichartRedraw - uichartZoomReset
3684
+ uichartRedraw();
3685
+ await waitAsync(200);
3686
+ uiFadeOut(baton.elemLoading);
3687
+ return;
3688
+ }
3689
+ }
3690
+ // uichartRedraw - start
3691
+ function uichartRedraw(modeDebounce) {
3692
+ // this function will redraw <uichart>
3693
+ if (!modeDebounce) {
3694
+ clearTimeout(redrawTimer);
3695
+ redrawTimer = setTimeout(uichartRedraw, 0, true);
3696
+ return;
3697
+ }
3698
+ redrawTimer = undefined;
3699
+ //
3700
+ // calculate plotBottom, plotHeight, plotTop
3701
+ //
3702
+ plotTop = 16;
3703
+ plotHeight = canvasHeight - plotTop - 32;
3704
+ plotBottom = canvasHeight - plotHeight - plotTop;
3705
+ //
3706
+ // pre-calculate plotLeft, plotRight, plotWidth
3707
+ //
3708
+ plotLeft = 32;
3709
+ plotWidth = canvasWidth - plotLeft - 32;
3710
+ plotRight = canvasWidth - plotWidth - plotLeft;
3711
+ //
3712
+ // calculate axisMax, axisMin - start
3713
+ //
3714
+ [
3715
+ true, false
3716
+ ].forEach(function (isXaxis) {
3717
+ let axisMax;
3718
+ let axisMin;
3719
+ let tickExponent;
3720
+ let tickInterval;
3721
+ let tickMultiple;
3722
+ let tickx;
3723
+ let tickxDict;
3724
+ let tickxList;
3725
+ let tickxMax;
3726
+ let tickxMin;
3727
+ let tickxPrv;
3728
+ if (isResizing) {
3729
+ return;
3730
+ }
3731
+ //
3732
+ // if isXaxis, then calculate xaxismax, xaxismin
3733
+ //
3734
+ if (isXaxis) {
3735
+ axisMax = Math.min(xdataMax, xzoomMax ?? xdataMax);
3736
+ axisMin = Math.max(xdataMin, xzoomMin ?? xdataMin);
3737
+ // ensure xaxisMax - xaxisMin >= xdataDxx
3738
+ if (axisMax - axisMin < 4 * xdataDxx) {
3739
+ axisMax = Math.min(xdataMax, axisMin + 4 * xdataDxx);
3740
+ axisMin = Math.max(xdataMin, axisMax - 4 * xdataDxx);
3741
+ }
3742
+ // pad axisMax, axisMin
3743
+ axisMax += 0.02 * (axisMax - axisMin);
3744
+ axisMin -= 0.02 * (axisMax - axisMin);
3745
+ }
3746
+ //
3747
+ // if not isXaxis, then calculate yaxisMax, yaxisMin
3748
+ //
3749
+ // if isBarchart, then make sure yaxis is visible
3750
+ if (!isXaxis && isBarchart) {
3751
+ axisMax = 0;
3752
+ axisMin = 0;
3753
+ }
3754
+ seriesList.forEach(function (series) {
3755
+ let ii;
3756
+ let nn;
3757
+ let xcropEnd;
3758
+ let xcropStart;
3759
+ let {
3760
+ xdata,
3761
+ ydata
3762
+ } = series;
3763
+ let yval;
3764
+ if (isXaxis || series.isHidden) {
3765
+ return;
3766
+ }
3767
+ nn = xdata.length;
3768
+ // calculate xcropEnd, xcropStart
3769
+ // xdata inside xcrop-range
3770
+ if (xaxisMin <= xdata[0] && xdata[nn - 1] <= xaxisMax) {
3771
+ xcropEnd = nn;
3772
+ xcropStart = 0;
3773
+ // xdata outside xcrop-range
3774
+ } else if (xdata[nn - 1] < xaxisMin || xaxisMax < xdata[0]) {
3775
+ xcropEnd = 0;
3776
+ xcropStart = 0;
3777
+ // init xcropStart
3778
+ } else {
3779
+ ii = 0;
3780
+ while (ii < nn && xdata[ii] < xaxisMin) {
3781
+ ii += 1;
3782
+ }
3783
+ xcropStart = Math.max(0, ii - 1);
3784
+ // init xcropEnd
3785
+ while (ii < nn && xdata[ii] < xaxisMax) {
3786
+ ii += 1;
3787
+ }
3788
+ xcropEnd = ii;
3789
+ }
3790
+ // calculate yaxisMax, yaxisMin
3791
+ ii = xcropStart;
3792
+ while (ii < xcropEnd) {
3793
+ yval = ydata[ii];
3794
+ if (!Number.isNaN(yval)) {
3795
+ axisMax = Math.max(axisMax ?? yval, yval);
3796
+ axisMin = Math.min(axisMin ?? yval, yval);
3797
+ }
3798
+ ii += 1;
3799
+ }
3800
+ });
3801
+ axisMax = axisMax ?? 0;
3802
+ axisMin = axisMin ?? 0;
3803
+ axisMax += 0.01 * (axisMax - axisMin);
3804
+ axisMin -= 0.01 * (axisMax - axisMin);
3805
+ if (axisMax === axisMin) {
3806
+ axisMax = Math.max(0.99 * axisMax, 1.01 * axisMax);
3807
+ axisMin = Math.min(0.99 * axisMin, 1.01 * axisMin);
3808
+ }
3809
+ if (axisMax === axisMin) {
3810
+ axisMax += 1;
3811
+ axisMin -= 1;
3812
+ }
3813
+ //
3814
+ // add/remove elemGridline, elemTick, elemTicklabel
3815
+ //
3816
+ // calculate tickInterval
3817
+ tickInterval = (
3818
+ isXaxis
3819
+ ? (axisMax - axisMin) * 100 / plotWidth
3820
+ : (axisMax - axisMin) * 72 / plotHeight
3821
+ ) || 1;
3822
+ // normalize tickInterval to within 0...10
3823
+ tickExponent = Math.pow(
3824
+ 10,
3825
+ Math.floor(Math.log(tickInterval) / Math.LN10)
3826
+ );
3827
+ tickInterval = tickInterval / tickExponent;
3828
+ // round tickInterval to 1, 2, 5, or 10
3829
+ Array.from([
3830
+ 1, 2, 5, 10
3831
+ ]).some(function (multiple, ii, list) {
3832
+ tickMultiple = multiple;
3833
+ return (
3834
+ 2 * tickInterval
3835
+ <=
3836
+ tickMultiple + (list[ii + 1] || tickMultiple)
3837
+ );
3838
+ });
3839
+ // after rounding, un-normalize tickInterval from within 0...10
3840
+ tickInterval = tickMultiple * tickExponent;
3841
+ // calculate tickxList from tickInterval
3842
+ tickx = floatCorrect(
3843
+ Math.floor(axisMin / tickInterval) * tickInterval
3844
+ );
3845
+ tickxList = [];
3846
+ tickxMax = floatCorrect(
3847
+ Math.ceil(axisMax / tickInterval) * tickInterval
3848
+ );
3849
+ while (true) {
3850
+ tickxList.push(tickx);
3851
+ tickx = floatCorrect(tickx + tickInterval);
3852
+ if (tickx > tickxMax || tickx === tickxPrv) {
3853
+ break;
3854
+ }
3855
+ tickxPrv = tickx;
3856
+ }
3857
+ // sync tickxMax, tickxMin with axisMax, axisMin
3858
+ tickxMax = tickxList[tickxList.length - 1];
3859
+ tickxMin = tickxList[0];
3860
+ if (isXaxis) {
3861
+ if (tickxMax > axisMax + 0.5 * xdataDxx) {
3862
+ tickxList.pop();
3863
+ }
3864
+ if (tickxMin < axisMin - 0.5 * xdataDxx) {
3865
+ tickxList.shift();
3866
+ }
3867
+ } else {
3868
+ axisMax = tickxMax;
3869
+ axisMin = tickxMin;
3870
+ }
3871
+ // add elemGridline, elemTick, elemTicklabel
3872
+ tickxDict = (
3873
+ isXaxis
3874
+ ? xaxistickDict
3875
+ : yaxistickDict
3876
+ );
3877
+ tickxList.forEach(function (tickx) {
3878
+ tickxDict[tickx] = tickxDict[tickx] || axistickCreate(
3879
+ isXaxis,
3880
+ tickx
3881
+ );
3882
+ });
3883
+ // remove elemGridline, elemTick, elemTicklabel
3884
+ Object.entries(tickxDict).forEach(function ([
3885
+ tickx, elemTick
3886
+ ]) {
3887
+ if (
3888
+ elemTick.isXaxis !== isXaxis
3889
+ || tickxList.indexOf(Number(tickx)) !== -1
3890
+ ) {
3891
+ return;
3892
+ }
3893
+ elemTick.remove();
3894
+ if (elemTick.elemGridline) {
3895
+ elemTick.elemGridline.remove();
3896
+ delete elemTick.elemGridline;
3897
+ }
3898
+ if (elemTick.elemTicklabel) {
3899
+ elemTick.elemTicklabel.remove();
3900
+ delete elemTick.elemTicklabel;
3901
+ }
3902
+ delete tickxDict[tickx];
3903
+ });
3904
+ //
3905
+ // re-calculate plotLeft, plotRight, plotWidth
3906
+ //
3907
+ if (!isXaxis) {
3908
+ plotLeft = 0;
3909
+ tickxList.forEach(function (tickx) {
3910
+ plotLeft = Math.max(
3911
+ plotLeft,
3912
+ tickxDict[tickx].elemTicklabel.getBBox().width
3913
+ );
3914
+ });
3915
+ plotLeft = Math.round(16 + plotLeft);
3916
+ plotWidth = Math.round(canvasWidth - plotLeft - 32);
3917
+ plotRight = Math.round(canvasWidth - plotWidth - plotLeft);
3918
+ }
3919
+ // save axisMax, axisMin
3920
+ if (isXaxis) {
3921
+ xaxisMax = axisMax;
3922
+ xaxisMin = axisMin;
3923
+ } else {
3924
+ yaxisMax = axisMax;
3925
+ yaxisMin = axisMin;
3926
+ }
3927
+ });
3928
+ //
3929
+ // calculate axisMax, axisMin - end
3930
+ //
3931
+ //
3932
+ // calculate xtransOffset, xtransWidth, ytransWidth
3933
+ //
3934
+ xtransWidth = plotWidth / (xaxisMax - xaxisMin + xdataDxx);
3935
+ xtransOffset = xtransWidth * 0.5 * xdataDxx;
3936
+ ytransWidth = plotHeight / (yaxisMax - yaxisMin);
3937
+ //
3938
+ // calculate plotbarMargin, plotbarWidth, plotbarYaxis
3939
+ plotbarMargin = Math.max(2, 0.24 * xtransWidth * xdataDxx);
3940
+ plotbarYaxis = yaxisTranslate(0);
3941
+ //
3942
+ // update crop-area for
3943
+ // elemCrosshairList, elemMousetrackerList, elemSeriesList
3944
+ //
3945
+ svgAttrSet(elemClip.firstElementChild, {
3946
+ height: plotHeight,
3947
+ width: plotWidth
3948
+ });
3949
+ [
3950
+ elemCrosshairList, elemMousetrackerList, elemSeriesList
3951
+ ].forEach(function (elem) {
3952
+ svgAttrSet(elem, {
3953
+ "clip-path": `url(#${elemClip.id})`,
3954
+ transform: `translate(${plotLeft},${plotTop})`
3955
+ });
3956
+ });
3957
+ //
3958
+ // redraw elemGridline, elemTick, elemTicklabel - start
3959
+ //
3960
+ [
3961
+ Object.entries(xaxistickDict),
3962
+ Object.entries(yaxistickDict)
3963
+ ].flat().forEach(function ([
3964
+ tickx, elemTick
3965
+ ]) {
3966
+ let {
3967
+ elemGridline,
3968
+ elemTicklabel,
3969
+ isXaxis
3970
+ } = elemTick;
3971
+ let xx;
3972
+ let yy;
3973
+ // init xx, yy for first-draw
3974
+ if (elemTick.isFirstDraw) {
3975
+ xx = plotLeft + 0.5 * plotWidth;
3976
+ yy = canvasHeight - plotBottom - 0.5 * plotHeight;
3977
+ if (isXaxis) {
3978
+ yy = canvasHeight + 0.125 * plotHeight;
3979
+ } else {
3980
+ xx = -0.125 * plotWidth;
3981
+ }
3982
+ delete elemTick.isFirstDraw;
3983
+ svgAttrSet(elemTick, {
3984
+ d: `M ${xx} ${yy} L ${xx} ${yy + 5}`
3985
+ });
3986
+ if (elemGridline) {
3987
+ svgAttrSet(elemGridline, {
3988
+ d: (
3989
+ `M ${plotLeft} ${yy}`
3990
+ + ` L ${canvasWidth - plotRight} ${yy}`
3991
+ )
3992
+ });
3993
+ }
3994
+ if (elemTicklabel) {
3995
+ svgAttrSet(elemTicklabel, {
3996
+ x: xx,
3997
+ y: yy
3998
+ });
3999
+ }
4000
+ }
4001
+ // init xx, yy for animation
4002
+ if (isXaxis) {
4003
+ xx = plotLeft + xaxisTranslate(tickx);
4004
+ yy = canvasHeight - plotBottom;
4005
+ } else {
4006
+ xx = plotLeft;
4007
+ yy = plotTop + yaxisTranslate(tickx);
4008
+ }
4009
+ // redraw elemTick
4010
+ if (isXaxis) {
4011
+ svgAnimate(elemTick, {
4012
+ d: `M ${xx - 0.5} ${yy + 0.5} L ${xx - 0.5} ${yy + 5.5}`
4013
+ });
4014
+ }
4015
+ // redraw elemTicklabel
4016
+ if (elemTicklabel) {
4017
+ svgAnimate(elemTicklabel, (
4018
+ isXaxis
4019
+ ? {
4020
+ x: xx,
4021
+ y: yy + 18
4022
+ }
4023
+ : {
4024
+ x: xx - 8,
4025
+ y: yy + 14 * 0.9 - 0.5 * elemTicklabel.getBBox().height
4026
+ }
4027
+ ));
4028
+ }
4029
+ // redraw elemGridline
4030
+ if (elemGridline) {
4031
+ svgAnimate(elemGridline, {
4032
+ d: (
4033
+ `M ${plotLeft} ${yy + CRISPY}`
4034
+ + ` L ${canvasWidth - plotRight} ${yy + CRISPY}`
4035
+ )
4036
+ });
4037
+ }
4038
+ });
4039
+ //
4040
+ // redraw elemGridline, elemTick, elemTicklabel - end
4041
+ //
4042
+ //
4043
+ // redraw seriesList - start
4044
+ //
4045
+ seriesList.forEach(function (series) {
4046
+ let {
4047
+ elemGraph,
4048
+ elemGraphtracker,
4049
+ pointListSeries
4050
+ } = series;
4051
+ let elemGraphD = "";
4052
+ let pointXPrv = 0;
4053
+ let pointYPrv = 0;
4054
+ if (series.isHidden) {
4055
+ return;
4056
+ }
4057
+ // redraw pointListSeries
4058
+ pointListSeries.forEach(function (pointObj, ii) {
4059
+ let barY;
4060
+ let {
4061
+ elemPoint,
4062
+ xval,
4063
+ yval
4064
+ } = pointObj;
4065
+ let pointX;
4066
+ let pointY;
4067
+ // pixelate xval, yval to pointX, pointY
4068
+ pointX = xaxisTranslate(xval);
4069
+ pointY = (
4070
+ yval === undefined
4071
+ ? undefined
4072
+ : yaxisTranslate(yval)
4073
+ );
4074
+ // save pointX, pointY
4075
+ pointObj.pointX = pointX;
4076
+ pointObj.pointY = pointY;
4077
+ if (pointY === undefined) {
4078
+ return;
4079
+ }
4080
+ if (isBarchart) {
4081
+ // if isBarchart, then redraw point as bar
4082
+ barY = Math.min(pointY, plotbarYaxis);
4083
+ svgAnimate(elemPoint, {
4084
+ height: Math.max(
4085
+ 4,
4086
+ Math.max(pointY, plotbarYaxis) - barY
4087
+ ),
4088
+ width: 2 * plotbarMargin,
4089
+ x: pointX - plotbarMargin,
4090
+ y: barY
4091
+ });
4092
+ return;
4093
+ }
4094
+ if (
4095
+ 128 <= (
4096
+ Math.pow(pointX - pointXPrv, 2)
4097
+ + Math.pow(pointY - pointYPrv, 2)
4098
+ )
4099
+ || ii + 1 === pointListSeries.length
4100
+ ) {
4101
+ pointXPrv = pointX;
4102
+ pointYPrv = pointY;
4103
+ }
4104
+ // if not isBarchart, then redraw point as shape
4105
+ svgAnimate(elemPoint, {
4106
+ height: 2 * ELEM_POINT_RADIUS,
4107
+ // visibility: (
4108
+ // pointX === pointXPrv
4109
+ // ? "inherit"
4110
+ // : "hidden"
4111
+ // ),
4112
+ width: 2 * ELEM_POINT_RADIUS,
4113
+ x: pointXPrv - ELEM_POINT_RADIUS,
4114
+ y: pointYPrv - ELEM_POINT_RADIUS
4115
+ });
4116
+ // if not isBarchart, then calculate elemGraphD
4117
+ elemGraphD += (
4118
+ (
4119
+ elemGraphD === ""
4120
+ // moveto
4121
+ ? "M"
4122
+ // lineto
4123
+ : "L"
4124
+ )
4125
+ // + ` ${pointX} ${pointY} `
4126
+ + ` ${pointXPrv} ${pointYPrv} `
4127
+ );
4128
+ });
4129
+ // if not isBarchart, then redraw elemGraph
4130
+ if (!isBarchart) {
4131
+ elemGraphD = elemGraphD.trim();
4132
+ svgAnimate(elemGraph, {
4133
+ d: elemGraphD
4134
+ });
4135
+ elemGraphtracker.setAttribute("d", elemGraphD);
4136
+ }
4137
+ // calculate pixelToPointDict
4138
+ series.xpixelToPointDict = xpixelToPointDictCreate(
4139
+ pointListSeries
4140
+ );
4141
+ });
4142
+ // calculate xpixelToPointDictHovered
4143
+ if (isBarchart) {
4144
+ xpixelToPointDictHovered = xpixelToPointDictCreate(
4145
+ pointListBarchart
4146
+ );
4147
+ }
4148
+ // reset seriesHovered
4149
+ seriesList.forEach(function (series) {
4150
+ if (!seriesHovered || seriesHovered.isHidden) {
4151
+ onSeriesHover(series);
4152
+ onSeriesUnhover();
4153
+ }
4154
+ });
4155
+ //
4156
+ // redraw seriesList - end
4157
+ //
4158
+ }
4159
+ // uichartRedraw - end
4160
+ function uichartResize() {
4161
+ // this function will resize <uichart>
4162
+ // temporarily remove elemCanvasFixed
4163
+ // to calculate canvasHeight, canvasWidth
4164
+ elemCanvasFixed.remove();
4165
+ canvasHeight = Math.round(elemCanvasFlex.clientHeight);
4166
+ canvasWidth = Math.round(elemCanvasFlex.clientWidth);
4167
+ // restore elemCanvasFixed
4168
+ elemCanvasFlex.appendChild(elemCanvasFixed);
4169
+ // increment / decrement isResizing
4170
+ isResizing += 1;
4171
+ setTimeout(function () {
4172
+ isResizing -= 1;
4173
+ }, UI_ANIMATE_DURATION);
4174
+ svgAttrSet(elemCanvasFixed, {
4175
+ height: canvasHeight,
4176
+ width: canvasWidth
4177
+ });
4178
+ // redraw from uichartResize
4179
+ uichartRedraw();
4180
+ }
4181
+ function uichartSeriesHideOrShow(series, modeHide) {
4182
+ // this function will hide-or-show <series>
4183
+ if (!series || series.isDummy) {
4184
+ return;
4185
+ }
4186
+ // reset previously hidden points to yaxis
4187
+ if (isBarchart && !modeHide && series.isHidden) {
4188
+ series.pointListSeries.forEach(function ({
4189
+ elemPoint,
4190
+ pointY
4191
+ }) {
4192
+ if (pointY !== undefined) {
4193
+ svgAttrSet(elemPoint, {
4194
+ height: 0,
4195
+ y: plotbarYaxis
4196
+ });
4197
+ }
4198
+ });
4199
+ }
4200
+ series.isHidden = modeHide;
4201
+ if (!modeHide) {
4202
+ onSeriesHover(series);
4203
+ }
4204
+ [
4205
+ series.elemSeries, series.elemGraphtracker
4206
+ ].forEach(function (elem) {
4207
+ if (elem) {
4208
+ elem.setAttribute("visibility", (
4209
+ modeHide
4210
+ ? "hidden"
4211
+ : "visible"
4212
+ ));
4213
+ }
4214
+ });
4215
+ // hide or show legend
4216
+ elemLegend.children[series.seriesIi].dataset.hidden = Number(modeHide);
4217
+ }
4218
+ function xaxisTranslate(xval) {
4219
+ // this function will translate <xval> to xpixel position on chart
4220
+ return xtransOffset + xtransWidth * (xval - xaxisMin);
4221
+ }
4222
+ function xpixelToPointDictCreate(pointList) {
4223
+ // this function will create dict mapping <xpixel> to nearest point in
4224
+ // <pointList> along xaxis
4225
+ let dict = [];
4226
+ let ii = 0;
4227
+ let nn = pointList.length;
4228
+ let pointObj;
4229
+ let pointObjPrv;
4230
+ let xpixel = 0;
4231
+ let xpixelEnd;
4232
+ while (ii < nn) {
4233
+ pointObj = pointList[ii];
4234
+ if (pointObj.pointY !== undefined && !pointObj.series.isHidden) {
4235
+ if (pointObjPrv) {
4236
+ xpixelEnd = 0.5 * (pointObjPrv.pointX + pointObj.pointX);
4237
+ while (xpixel < xpixelEnd) {
4238
+ dict[xpixel] = pointObjPrv;
4239
+ xpixel += 1;
4240
+ }
4241
+ }
4242
+ pointObjPrv = pointObj;
4243
+ }
4244
+ ii += 1;
4245
+ }
4246
+ while (pointObjPrv && xpixel < plotWidth) {
4247
+ dict[xpixel] = pointObjPrv;
4248
+ xpixel += 1;
4249
+ }
4250
+ return dict;
4251
+ }
4252
+ function yaxisTranslate(yval) {
4253
+ // this function will translate <yval> to ypixel position on chart
4254
+ return plotHeight - ytransWidth * (yval - yaxisMin);
4255
+ }
4256
+ //
4257
+ // uichartCreate - start
4258
+ //
4259
+ // Resize the box and re-align all aligned elements
4260
+ svgAttrSet(elemCanvasFixed, {
4261
+ height: canvasHeight,
4262
+ width: canvasWidth
4263
+ });
4264
+ // init event-handling
4265
+ elemCanvasFlex.onclick = onSeriesHoveredHide;
4266
+ elemCanvasFlex.onmouseenter = onPointUnhover;
4267
+ elemCanvasFlex.onmouseleave = onPointUnhover;
4268
+ elemCanvasFlex.onmousemove = onPointHover;
4269
+ elemCanvasFlex.onwheel = onCanvasZoom;
4270
+ elemLegend.onmouseleave = onPointUnhover;
4271
+ elemLegend.onmouseover = onSeriesHover;
4272
+ elemUichart.querySelector(".uichartNav").onclick = onUichartAction;
4273
+ // init seriesList
4274
+ seriesList.forEach(function (series, seriesIi) {
4275
+ let elemGraph;
4276
+ let elemGraphtracker;
4277
+ let elemSeries = svgAttrSet("g");
4278
+ let {
4279
+ isDummy,
4280
+ seriesColor,
4281
+ xdata,
4282
+ ydata
4283
+ } = series;
4284
+ let pointListSeries;
4285
+ let seriesShape;
4286
+ elemSeries.classList.add(`elemSeries_${seriesIi}`);
4287
+ // init seriesColor
4288
+ if (isDummy) {
4289
+ seriesColor = "rgba(192,192,192,0)";
4290
+ } else if (!(seriesColor && typeof seriesColor === "string")) {
4291
+ seriesColor = seriesColorList[
4292
+ ((seriesColor ?? counterColor) - 1) % seriesColorList.length
4293
+ ];
4294
+ counterColor += 1;
4295
+ }
4296
+ seriesColor = seriesColor.replace((
4297
+ /^#(.)(.)(.)$/
4298
+ ), "#$1$1$2$2$3$3");
4299
+ // init seriesShape
4300
+ if (isBarchart || isDummy) {
4301
+ seriesShape = "square";
4302
+ } else {
4303
+ seriesShape = seriesShapeList[
4304
+ ((seriesShape ?? counterShape) - 1) % seriesShapeList.length
4305
+ ];
4306
+ counterShape += 1;
4307
+ }
4308
+ // init xdata, ydata
4309
+ xdata = new Float64Array(xdata);
4310
+ ydata = new Float64Array(ydata.map(function (yval) {
4311
+ return yval ?? NaN;
4312
+ }));
4313
+ if (!isBarchart) {
4314
+ // init elemGraph
4315
+ elemGraph = svgAttrSet("path", {
4316
+ fill: "none",
4317
+ stroke: seriesColor,
4318
+ "stroke-linecap": "round",
4319
+ "stroke-linejoin": "round",
4320
+ "stroke-width": ELEM_GRAPH_LINE_WIDTH
4321
+ });
4322
+ elemSeries.appendChild(elemGraph);
4323
+ // init elemGraphtracker
4324
+ elemGraphtracker = svgAttrSet("path", {
4325
+ fill: "none",
4326
+ stroke: "rgba(192,192,192,0.0001)",
4327
+ "stroke-linecap": "round",
4328
+ "stroke-linejoin": "round",
4329
+ "stroke-width": ELEM_GRAPH_LINE_WIDTH + 20,
4330
+ visibility: "inherit"
4331
+ });
4332
+ elemGraphtracker.classList.add(`series_${seriesIi}`);
4333
+ // init event-handling
4334
+ elemGraphtracker.dataset.seriesIi = seriesIi;
4335
+ elemGraphtracker.onmouseover = function (evt) {
4336
+ onSeriesHover(evt, true);
4337
+ };
4338
+ }
4339
+ // init pointListSeries
4340
+ pointListSeries = Array.from(ydata).map(function (yval, ii) {
4341
+ let elemPoint;
4342
+ let pointObj;
4343
+ let xval = xdata[ii];
4344
+ elemPoint = (
4345
+ isBarchart
4346
+ ? svgAttrSet("rect", {
4347
+ fill: seriesColor,
4348
+ stroke: "#333",
4349
+ "stroke-width": ELEM_POINT_BORDER_WIDTH,
4350
+ visibility: "inherit",
4351
+ y: 0.5 * canvasHeight
4352
+ })
4353
+ : svgAttrSet("path", {
4354
+ fill: seriesColor,
4355
+ stroke: "#333",
4356
+ "stroke-width": ELEM_POINT_BORDER_WIDTH,
4357
+ visibility: "inherit",
4358
+ y: 0.5 * canvasHeight
4359
+ })
4360
+ );
4361
+ // init fx_seriesShape
4362
+ if (!isBarchart) {
4363
+ elemPoint.fx_seriesShape = seriesShape;
4364
+ }
4365
+ pointObj = {
4366
+ elemPoint,
4367
+ series,
4368
+ xval,
4369
+ yval: (
4370
+ Number.isNaN(yval)
4371
+ ? undefined
4372
+ : yval
4373
+ )
4374
+ };
4375
+ if (isBarchart) {
4376
+ pointListBarchart.push(pointObj);
4377
+ }
4378
+ return pointObj;
4379
+ });
4380
+ // save series-properties
4381
+ Object.assign(series, {
4382
+ elemGraph,
4383
+ elemGraphtracker,
4384
+ elemSeries,
4385
+ isHidden: series.isDummy || series.isHidden,
4386
+ pointListSeries,
4387
+ seriesColor,
4388
+ seriesIi,
4389
+ seriesShape,
4390
+ xdata,
4391
+ xpixelToPointDict: [],
4392
+ ydata
4393
+ });
4394
+ });
4395
+ if (isBarchart) {
4396
+ pointListBarchart.sort(function (aa, bb) {
4397
+ return aa.xval - bb.xval;
4398
+ });
4399
+ }
4400
+ // draw seriesList in reverse, so first series has highest z-index
4401
+ Array.from(seriesList).reverse().forEach(function ({
4402
+ elemGraphtracker,
4403
+ elemSeries,
4404
+ pointListSeries
4405
+ }) {
4406
+ elemSeriesList.appendChild(elemSeries);
4407
+ Array.from(pointListSeries).reverse().forEach(function ({
4408
+ elemPoint
4409
+ }) {
4410
+ elemSeries.appendChild(elemPoint);
4411
+ });
4412
+ if (!isBarchart) {
4413
+ elemMousetrackerList.appendChild(elemGraphtracker);
4414
+ }
4415
+ });
4416
+ // draw elemLegend
4417
+ elemLegend.innerHTML = uichart.seriesList.map(function (series, ii) {
4418
+ return (`
4419
+ <a
4420
+ class="uichartAction uichartLegendElem"
4421
+ data-action="uichartSeriesHideOrShow"
4422
+ data-dummy="${series.isDummy | 0}"
4423
+ data-hidden="${series.isHidden | 0}"
4424
+ data-series-ii="${ii}"
4425
+ title="${stringHtmlSafe(series.seriesName)}"
4426
+ >
4427
+ <svg class="uichartLegendElemSvg" xmlns="http://www.w3.org/2000/svg">
4428
+ <g>
4429
+ <path
4430
+ d="${svgShapeDraw(series.seriesShape, 0, 2, 10, 10)}"
4431
+ fill="${series.seriesColor}"
4432
+ stroke-width="0"
4433
+ >
4434
+ </path>
4435
+ </g>
4436
+ </svg>
4437
+ <span style="margin-left: 12px; position: absolute;">${series.seriesName}</span>
4438
+ </a>
4439
+ `);
4440
+ }).join("");
4441
+ // first-draw
4442
+ uichartRedraw(true);
4443
+ // after first-draw, init startup-animation
4444
+ svgAttrSet(elemClip.firstElementChild, {
4445
+ width: 0
4446
+ });
4447
+ svgAnimate(elemClip.firstElementChild, {
4448
+ width: plotWidth
4449
+ });
4450
+ return Object.assign(uichart, {
4451
+ uichartRedraw,
4452
+ uichartResize
4453
+ });
4454
+ //
4455
+ // uichartCreate - end
4456
+ //
4457
+ }
4458
+
4459
+ async function uitableAjax(baton, {
4460
+ rowList,
4461
+ type
4462
+ }) {
4463
+ let {
4464
+ colList,
4465
+ contentElem,
4466
+ db,
4467
+ dbtableName,
4468
+ elemInfo,
4469
+ elemLoading,
4470
+ elemScroller,
4471
+ elemTable,
4472
+ hashtag,
4473
+ isDbchart,
4474
+ rowCount,
4475
+ sortCol,
4476
+ sortDir
4477
+ } = baton;
4478
+ let html = "";
4479
+ let viewRowBeg;
4480
+ let viewRowEnd;
4481
+ if (baton.rowCount === 0) {
4482
+ // uitableLoading - hide
4483
+ uiFadeOut(elemLoading);
4484
+ return;
4485
+ }
4486
+ switch (type) {
4487
+ // uitableScroll
4488
+ case "scroll":
4489
+ case "uitableInit":
4490
+ if (type === "uitableInit" && isDbchart) {
4491
+ uiFadeIn(elemLoading);
4492
+ await uiTryCatch(async function () {
4493
+ // resize uichart
4494
+ if (baton.uichart) {
4495
+ baton.uichart.uichartResize();
4496
+ return;
4497
+ }
4498
+ // create uichart
4499
+ baton.uichart = await uichartCreate(baton);
4500
+ });
4501
+ await waitAsync(500);
4502
+ uiFadeOut(elemLoading);
4503
+ return;
4504
+ }
4505
+ viewRowBeg = Math.max(0, Math.round(
4506
+ rowCount
4507
+ * elemScroller.scrollTop
4508
+ / (elemScroller.scrollHeight - 1 * UI_ROW_HEIGHT)
4509
+ ));
4510
+ viewRowEnd = Math.min(rowCount, Math.round(viewRowBeg + UI_VIEW_SIZE));
4511
+ // update table-view info
4512
+ elemInfo.textContent = (
4513
+ "showing "
4514
+ + new Intl.NumberFormat().format(viewRowBeg + 1)
4515
+ + " to "
4516
+ + new Intl.NumberFormat().format(viewRowEnd)
4517
+ + " of "
4518
+ + new Intl.NumberFormat().format(rowCount)
4519
+ + " rows"
4520
+ );
4521
+ // skip expensive table-redraw, if scroll-point is within boundaries
4522
+ if (
4523
+ contentElem.dataset.init !== "0"
4524
+ && baton.rowOffset <= Math.max(0, viewRowBeg - 1 * UI_VIEW_SIZE)
4525
+ && (
4526
+ Math.min(rowCount, viewRowEnd + 1 * UI_VIEW_SIZE)
4527
+ <= baton.rowOffset + UI_PAGE_SIZE
4528
+ )
4529
+ ) {
4530
+ return;
4531
+ }
4532
+ // Do the uitable redraw based on the calculated start point
4533
+ baton.rowOffset = Math.max(0, Math.round(
4534
+ viewRowBeg + 0.5 * UI_VIEW_SIZE - 0.5 * UI_PAGE_SIZE
4535
+ ));
4536
+ break;
4537
+ }
4538
+ switch (type !== "uitableDraw" && baton.modeAjax) {
4539
+ // uitableAjax
4540
+ case 0:
4541
+ // uitableLoading - show
4542
+ uiFadeIn(elemLoading);
4543
+ baton.modeAjax = 1;
4544
+ // uitable - paginate
4545
+ rowList = await dbExecAsync({
4546
+ db,
4547
+ responseType: "list",
4548
+ sql: (`
4549
+ SELECT
4550
+ rowid,
4551
+ --
4552
+ ${dbtableName}.*
4553
+ FROM ${dbtableName}
4554
+ ORDER BY [${colList[sortCol]}] ${sortDir}
4555
+ LIMIT ${Number(UI_PAGE_SIZE)}
4556
+ OFFSET ${Number(baton.rowOffset)};
4557
+ `)
4558
+ });
4559
+ rowList = (
4560
+ rowList[0]
4561
+ ? rowList[0].slice(1)
4562
+ : []
4563
+ ).map(function (row) {
4564
+ return row.map(function (val) {
4565
+ return (
4566
+ // bugfix - truncate large text to avoid freezing browser
4567
+ (typeof val === "string" && val.length > 65536)
4568
+ ? val.slice(0, 65536)
4569
+ : val
4570
+ );
4571
+ });
4572
+ });
4573
+ // recurse - draw
4574
+ await uitableAjax(baton, {
4575
+ rowList,
4576
+ type: "uitableDraw"
4577
+ });
4578
+ return;
4579
+ // debounce
4580
+ case 1:
4581
+ baton.modeAjax = 2;
4582
+ return;
4583
+ // debounce
4584
+ case 2:
4585
+ return;
4586
+ }
4587
+ // uitableDraw
4588
+ // Position the table in the virtual scroller
4589
+ elemTable.style.top = Math.max(0, Math.round(
4590
+ elemScroller.scrollHeight * baton.rowOffset / (baton.rowCount + 1)
4591
+ )) + "px";
4592
+ // Insert the required TR nodes into the table for display
4593
+ jsonHtmlSafe(rowList).forEach(function (row) {
4594
+ html += (`
4595
+ <tr data-dbtype="row" data-hashtag="${hashtag}" data-rowid="${row[0]}">
4596
+ `);
4597
+ row.forEach(function (val) {
4598
+ html += "<td>" + (val ?? "") + "</td>";
4599
+ });
4600
+ html += "</tr>";
4601
+ });
4602
+ elemTable.children[1].innerHTML = html;
4603
+ // debounce - throttle
4604
+ await waitAsync(500);
4605
+ // debounce - next
4606
+ if (baton.modeAjax === 2) {
4607
+ baton.modeAjax = 0;
4608
+ // keep focus on current scroller when debouncing
4609
+ if (type === "scroll") {
4610
+ elemScroller.focus();
4611
+ }
4612
+ await uitableAjax(baton, {});
4613
+ return;
4614
+ }
4615
+ // cleanup
4616
+ baton.modeAjax = 0;
4617
+ // uitableLoading - hide
4618
+ uiFadeOut(elemLoading);
4619
+ }
4620
+
4621
+ function uitableCreate(baton) {
4622
+ // this function will create a dom-datatable-view of sqlite queries and tables
4623
+ let contentElem;
4624
+ // All uitables are wrapped in a div
4625
+ // Generate the node required for the processing node
4626
+ // The HTML structure that we want to generate in this function is:
4627
+ // div - scroller
4628
+ // div - scroll head
4629
+ // div - scroll head inner
4630
+ // table - scroll head table
4631
+ // thead - thead
4632
+ // div - scroll body
4633
+ // table - table (master table)
4634
+ // thead - thead clone for sizing
4635
+ // tbody - tbody
4636
+ contentElem = domDivCreate(
4637
+ (`
4638
+ <div class="contentElem" data-init="0" id="${baton.hashtag}">
4639
+ <div class="contentElemTitle title">${stringHtmlSafe(baton.title)}</div>
4640
+ <div class="uitableLoading">loading</div>
4641
+ <div class="uichart" style="display: none;"></div>
4642
+ <div class="uitable">
4643
+ <div class="uitableInfo">showing 0 to 0 of 0 entries</div>
4644
+ <div
4645
+ class="uitableScroller"
4646
+ style="height: ${(UI_VIEW_SIZE + 2) * UI_ROW_HEIGHT}px;"
4647
+ tabindex="-1"
4648
+ >
4649
+ <div
4650
+ class="uitableScrollerDummy"
4651
+ style="height: ${baton.rowCount * UI_ROW_HEIGHT}px;"
4652
+ ></div>
4653
+ <table class="uitableTable">
4654
+ <thead>
4655
+ <tr>
4656
+ `)
4657
+ + jsonHtmlSafe(baton.colList).map(function (col, ii) {
4658
+ return (
4659
+ ii === 0
4660
+ ? `<th title="${col}" data-sort="asc">${col}</th>`
4661
+ : `<th title="${col}">${col}</th>`
4662
+ );
4663
+ }).join("")
4664
+ + (`
4665
+ </tr>
4666
+ </thead>
4667
+ <tbody>
4668
+ <tr data-dbtype="row" data-hashtag="${baton.hashtag}">
4669
+ <td colspan="${baton.colList.length}">
4670
+ No data available in table
4671
+ </td>
4672
+ </tr>
4673
+ </tbody>
4674
+ </table>
4675
+ </div>
4676
+ </div>
4677
+ </div>
4678
+ `)
4679
+ ).firstElementChild;
4680
+ // init event-handling - crud
4681
+ contentElem.querySelector(
4682
+ "tbody"
4683
+ ).oncontextmenu = onContextmenu;
4684
+ // init event-handling - sorting
4685
+ contentElem.querySelector(
4686
+ "thead tr"
4687
+ ).onclick = uitableSort.bind(undefined, baton);
4688
+ // init event-handling - scrolling
4689
+ contentElem.querySelector(
4690
+ ".uitableScroller"
4691
+ ).onscroll = uitableAjax.bind(undefined, baton);
4692
+ contentElem.addEventListener("uitableInit", function (evt) {
4693
+ uitableAjax(baton, evt);
4694
+ });
4695
+ Object.assign(baton, {
4696
+ contentElem,
4697
+ elemInfo: contentElem.querySelector(".uitableInfo"),
4698
+ elemLoading: contentElem.querySelector(".uitableLoading"),
4699
+ elemScroller: contentElem.querySelector(".uitableScroller"),
4700
+ elemTable: contentElem.querySelector(".uitableTable"),
4701
+ modeAjax: 0,
4702
+ rowOffset: 0
4703
+ });
4704
+ return contentElem;
4705
+ }
4706
+
4707
+ function uitableInitWithinView({
4708
+ modeDebounce
4709
+ }) {
4710
+ // this function will defer-init uitables when visible in viewport
4711
+ if (!modeDebounce) {
4712
+ debounce("uitableInitWithinView", uitableInitWithinView, {
4713
+ modeDebounce: true
4714
+ });
4715
+ return;
4716
+ }
4717
+ document.querySelectorAll(
4718
+ `#contentPanel1 .contentElem[data-init="0"]`
4719
+ ).forEach(function (elem) {
4720
+ let rect = elem.getBoundingClientRect();
4721
+ if (0 <= rect.bottom && rect.top < window.innerHeight) {
4722
+ elem.dispatchEvent(new window.CustomEvent("uitableInit"));
4723
+ elem.dataset.init = "1";
4724
+ }
4725
+ });
4726
+ }
4727
+
4728
+ function uitableSort(baton, {
4729
+ currentTarget,
4730
+ target
4731
+ }) {
4732
+ // Function to run on user sort request
4733
+ let colIi;
4734
+ let direction;
4735
+ let elem = target.closest("th");
4736
+ if (!elem) {
4737
+ return;
4738
+ }
4739
+ direction = elem.dataset.sort;
4740
+ direction = (
4741
+ direction === "asc"
4742
+ ? "desc"
4743
+ : "asc"
4744
+ );
4745
+ Array.from(currentTarget.children).forEach(function (elemTh, ii) {
4746
+ if (elemTh !== elem) {
4747
+ elemTh.dataset.sort = "";
4748
+ return;
4749
+ }
4750
+ colIi = ii;
4751
+ });
4752
+ elem.dataset.sort = direction;
4753
+ baton.sortCol = colIi;
4754
+ baton.sortDir = direction;
4755
+ // Reset scroll to top in redraw.
4756
+ baton.elemScroller.scrollTop = 0;
4757
+ baton.rowOffset = 0;
4758
+ uitableAjax(baton, {});
4759
+ }
4760
+
4761
+ function waitAsync(timeout) {
4762
+ // this function will wait <timeout> milliseconds
4763
+ return new Promise(function (resolve) {
4764
+ setTimeout(resolve, timeout);
4765
+ });
4766
+ }
4767
+
4768
+ // init
4769
+ window.addEventListener("load", init);