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