root commited on
Commit
5b065c1
1 Parent(s): 68e997e
Files changed (3) hide show
  1. main.py +33 -0
  2. public/play.html +1079 -0
  3. requirements.txt +2 -0
main.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, request
2
+ import chdb
3
+ import os
4
+
5
+ app = Flask(__name__, static_folder="public", static_url_path="")
6
+
7
+ @app.route('/', methods=["GET"])
8
+ def clickhouse():
9
+ query = request.args.get('query', default="", type=str)
10
+ format = request.args.get('default_format', default="CSV", type=str)
11
+ if not query:
12
+ return app.send_static_file('play.html')
13
+
14
+ res = chdb.query(query, format)
15
+ return res.get_memview().tobytes()
16
+
17
+ @app.route('/', methods=["POST"])
18
+ def play():
19
+ query = request.data
20
+ format = request.args.get('default_format', default="CSV", type=str)
21
+ if not query:
22
+ return app.send_static_file('play.html')
23
+
24
+ res = chdb.query(query, format)
25
+ return res.get_memview().tobytes()
26
+
27
+ @app.errorhandler(404)
28
+ def handle_404(e):
29
+ return app.send_static_file('play.html')
30
+
31
+ host = os.getenv('HOST', '0.0.0.0')
32
+ port = os.getenv('PORT', 7860)
33
+ app.run(host=host, port=port)
public/play.html ADDED
@@ -0,0 +1,1079 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <link rel="icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAKdUExURf/dV//TI//NCP/MAv/MAf/MA//PEv/XOP/kfP/TJv/MBP7cVP/XOv/NBv3lgv/PD/3bUf/NBP3XPv3WPfzWPf/MAPzWPv7QFf7PF/7ODf/NAf7MB/7PEf7ODvjTQPXbcv7OEP/SJP/MB/7OD/XYZP7PE//VMv7QF//MCPXXZf7OEv3SKP7PFv7NDvXXYf7NEP3TLf7PFf/NCf3UMv7OFf/NCv7NDfzXPv3UM/7OFP/NC/Xbb//OAf+3APy2TPXabv7ND//KAf/BAf+3Af+YAP+ZAPy3Tv7OEf+4Af+7Afy5Vv/RHf/ODP+cB/zKff+qAPrHef/eXf3VMfTWXf+bBf+ZAfvJfv/EAf+hAP+9Af/QAf+1Af+aA/vJff/IAf+lAP+bAP+uAP+aAPuvPv/dVfzjgv+WAP+aAv/PEfm5W/XXZP65D/yiGv/ib/6eD//bTf/NAvXXXf6eDv+aAf/WNvzPGfTGgP/WNPrRLfTDev/XPP7MBv2gFfTEff/dWP3RI/2gFv+hFf/mhP3SJP/NA/ygFv/SI/+wOfe1U/7lf/6jHf/QGPqrNf+fDv6kHv3bVf2hGf6jG/+ZAv6kH/S9av3VN/6lH/6lIPixRv+lIP6lIviuP/3TL/++Xv6mIv7PI/y1SfisO/3PI//KfPy2S/3SI/zDa/+2AP2pJ/zCavy2Tv6lIfiwQ/+2Af2oKf+hFPzBafy3T/i6Xf/GAf6hF/7BZf++Xf+dDPy/Zfy4UP/CAf/LAf+8Afy9Xv+sAP/PAfy8W/y6V/isOv+XAP+iAP+xAP+5Af+0AfitPP3AI/2lI/2nJfiuQPyvO/e0UPrDcf6aA/6dDfLGhPm1T/6cCP2eD/W5XvbJhfmxRPyjH/2dDf6bB/ulI/ezTQAAALQaDucAAADfdFJOU////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wA8JjrVAAAACXBIWXMAAA7CAAAOwgEVKEqAAAAEHklEQVRYR7WWh3sURRiHkdxRoqiAi7JiQUWwgQiKwgFGbjEBFcGcYEHDUYIFQaMC0u1KEwQBEVTAbgR7A6OEKEpRQUSK87fwfTO/nZ2ZnctDHh/f58nM3jfze3dutlxaif/I/ypodUrrskwm26ZtOxR8lBa0p3BM+akopiklOM2IMx1Ox4BLCcEZyCWUnYkhB7+gI1IWnTBo4xVkEXHojGELn6BEPpM5CxNMPII2Aean6YIpBr4VtG6JwbsHLTG4guzZ3LbA4ArKg3O464rpmTDEgcYxpL7CuUE37s6Tk8PzxQUXuquxDZagOzcXBRdzx4bwEj7q0azBFFwa9OSuV2wIL+NeiMubM1gruCK4krtewVXcde3NLdMHQY1xR9l7cHXQl7trlCGhGYMWZMu47Zfpz921wXXcJaQMAzCgBXT/l3N/feYG7gYGg7hLMA10ZXODh2AAAn5+giwfwTCUG5MbVZiouCnMDctHw1VdCTrIfYYhuFnWXGJDrjJfNSyfj6IRsiwFI+PrBMMt3CluvQ0HMIQVYcWoSHE7V1kwWoYl0jDmDm4l1VHhThxKQzh23F1330OnZ+7lIgvMF4g0aKppWmE8PrAhvO/+mglFlY+iiVQjwSRkFWWT1WSG866hYtwUWWVqqUSCqYgq1P0vUXnbkKvU5ycekALrFyB8EFOTvGV4KMIGSB5mwTREJd68ZXgEJUmBBdORZWY8iml23jI8hhJTx4LHjYf1CUxy82RIrmYBJaL4JAlmIszMwhwhMCOBTgZqUSGKs0nwlLGCOZhDGOeRzEXdXsE8EsxHWLIAkwjbsBBVIRahwsg9eBpZxTOYRpgGf14JxLPIKrxrKJGPnpOC5+13pseQ5F+IrG/2ohS8hGRMypDkhXhZVgCeBbHYXoJrMPO2YQl9ZoH9NBCWIckv5TOKZUirBSjBcgQ1huEV9MSKaCV3ryJft4o/SYFYjaDGMBi8xk+PEGuUYK2sKQG9ix1ShnXcwPA6xYvr+UgLxBsIahzDBvUGhGGjzmuBeBNBjWN4S+3ZGnyLt7lltEBsQlBjGDbT35bYIJeiSQRpwzsYoPv3XWqHeA2GwDXk3ntf1fn+/4D6D9Vl/Cj6mDtgChxDfT7/CVe3you2jY4+VRvwGTcxlsA05D6voffvF19+9bXMR9E3NP5tQRpMbEFiCL/7fnsNooq6HTQ+Qq3BwBHEhrAq+qGh3vwJIH6k8Z92qnkaV6AMYUNlvnHULkcgDS4pARtyVU0NP0eNbj7+l8AiLRCbctuLjU2/IGOxG1MMPALxK+0//wviou9/E59A/IaEjTfvF4g9yJgU92LQxi8Q+5BKqNuPIYcSAvH7HwiCPw9gwKWUQIiDfyFLZz/0N4ppSguEOPzPkaPHjh06/i8+e2lOcBIIcQLcIg1ac6/BWAAAAABJRU5ErkJggg==">
6
+ <title>chDB</title>
7
+
8
+ <!-- Code Style:
9
+
10
+ Do not use any JavaScript or CSS frameworks or preprocessors.
11
+ This HTML page should not require any build systems (node.js, npm, gulp, etc.)
12
+ This HTML page should not be minified, instead it should be reasonably minimalistic by itself.
13
+ This HTML page should not load any external resources on load.
14
+ (CSS and JavaScript must be embedded directly to the page. No external fonts or images should be loaded).
15
+ This UI should look as lightweight, clean and fast as possible.
16
+ All UI elements must be aligned in pixel-perfect way.
17
+ There should not be any animations.
18
+ No unexpected changes in positions of elements while the page is loading.
19
+ Navigation by keyboard should work.
20
+ 64-bit numbers must display correctly.
21
+
22
+ -->
23
+
24
+ <!-- Development Roadmap:
25
+
26
+ 1. Support readonly servers.
27
+ Check if readonly = 1 (with SELECT FROM system.settings) to avoid sending settings. It can be done once on address/credentials change.
28
+ It can be done in background, e.g. wait 100 ms after address/credentials change and do the check.
29
+ Also it can provide visual indication that credentials are correct.
30
+
31
+ -->
32
+
33
+ <style>
34
+ :root {
35
+ --background-color: #DDF8FF; /* Or #FFFBEF; actually many pastel colors look great for light theme. */
36
+ --element-background-color: #FFF;
37
+ --bar-color: #F8F4F0; /* Light bar in background of table cells. */
38
+ --border-color: #EEE;
39
+ --shadow-color: rgba(0, 0, 0, 0.1);
40
+ --button-color: #FFAA00; /* Orange on light-cyan is especially good. */
41
+ --text-color: #000;
42
+ --button-active-color: #F00;
43
+ --button-active-text-color: #FFF;
44
+ --misc-text-color: #888;
45
+ --error-color: #FEE; /* Light-pink on light-cyan is so neat, I even want to trigger errors to see this cool combination of colors. */
46
+ --table-header-color: #F8F8F8;
47
+ --table-hover-color: #FFF8EF;
48
+ --null-color: #A88;
49
+ --link-color: #06D;
50
+ --logo-color: #CEE;
51
+ --logo-color-active: #BDD;
52
+ }
53
+
54
+ [data-theme="dark"] {
55
+ --background-color: #000;
56
+ --element-background-color: #102030;
57
+ --bar-color: #182838;
58
+ --border-color: #111;
59
+ --shadow-color: rgba(255, 255, 255, 0.1);
60
+ --text-color: #CCC;
61
+ --button-color: #FFAA00;
62
+ --button-text-color: #000;
63
+ --button-active-color: #F00;
64
+ --button-active-text-color: #FFF;
65
+ --misc-text-color: #888;
66
+ --error-color: #400;
67
+ --table-header-color: #102020;
68
+ --table-hover-color: #003333;
69
+ --null-color: #A88;
70
+ --link-color: #4BDAF7;
71
+ --logo-color: #222;
72
+ --logo-color-active: #333;
73
+ }
74
+
75
+ *
76
+ {
77
+ box-sizing: border-box;
78
+ /* For iPad */
79
+ margin: 0;
80
+ border-radius: 0;
81
+ tab-size: 4;
82
+ }
83
+
84
+ html, body
85
+ {
86
+ height: 100%;
87
+ margin: 0;
88
+ /* This enables position: sticky on controls */
89
+ overflow: auto;
90
+ }
91
+
92
+ html
93
+ {
94
+ /* The fonts that have full support for hinting. */
95
+ font-family: Liberation Sans, DejaVu Sans, sans-serif, Noto Color Emoji, Apple Color Emoji, Segoe UI Emoji;
96
+ background: var(--background-color);
97
+ color: var(--text-color);
98
+ }
99
+
100
+ body
101
+ {
102
+ /* This element will show scroll-bar on overflow, and the scroll-bar will be outside of the padding. */
103
+ padding: 0.5rem;
104
+ }
105
+
106
+ #controls
107
+ {
108
+ /* When a page will be scrolled horizontally due to large table size, keep controls in place. */
109
+ position: sticky;
110
+ left: 0;
111
+ }
112
+
113
+ /* Otherwise Webkit based browsers will display ugly border on focus. */
114
+ textarea, input, button
115
+ {
116
+ outline: none;
117
+ border: none;
118
+ color: var(--text-color);
119
+ }
120
+
121
+ .monospace
122
+ {
123
+ /* Prefer fonts that have full hinting info. This is important for non-retina displays.
124
+ Also I personally dislike "Ubuntu" font due to the similarity of 'r' and 'г' (it looks very ignorant). */
125
+ font-family: Liberation Mono, DejaVu Sans Mono, MonoLisa, Consolas, monospace;
126
+ }
127
+
128
+ .monospace-table
129
+ {
130
+ /* Liberation is worse than DejaVu for block drawing characters. */
131
+ font-family: DejaVu Sans Mono, Liberation Mono, MonoLisa, Consolas, monospace;
132
+ }
133
+
134
+ .shadow
135
+ {
136
+ box-shadow: 0 0 1rem var(--shadow-color);
137
+ }
138
+
139
+ input, textarea
140
+ {
141
+ border: 1px solid var(--border-color);
142
+ /* The font must be not too small (to be inclusive) and not too large (it's less practical and make general feel of insecurity) */
143
+ font-size: 11pt;
144
+ padding: 0.25rem;
145
+ background-color: var(--element-background-color);
146
+ }
147
+
148
+ #query
149
+ {
150
+ /* Make enough space for even big queries. */
151
+ height: 20vh;
152
+ /* Keeps query text-area's width full screen even when user adjusting the width of the query box. */
153
+ min-width: 100%;
154
+ }
155
+
156
+ #inputs
157
+ {
158
+ white-space: nowrap;
159
+ }
160
+
161
+ #url
162
+ {
163
+ width: 70%;
164
+ }
165
+
166
+ #user
167
+ {
168
+ width: 15%;
169
+ }
170
+
171
+ #password
172
+ {
173
+ width: 15%;
174
+ }
175
+
176
+ #run_div
177
+ {
178
+ margin-top: 1rem;
179
+ }
180
+
181
+ #run
182
+ {
183
+ color: var(--button-text-color);
184
+ background-color: var(--button-color);
185
+ padding: 0.25rem 1rem;
186
+ cursor: pointer;
187
+ font-weight: bold;
188
+ font-size: 100%; /* Otherwise button element will have lower font size. */
189
+ }
190
+
191
+ #run:hover, #run:focus
192
+ {
193
+ color: var(--button-active-text-color);
194
+ background-color: var(--button-active-color);
195
+ }
196
+
197
+ #stats
198
+ {
199
+ float: right;
200
+ color: var(--misc-text-color);
201
+ }
202
+
203
+ #toggle-light, #toggle-dark
204
+ {
205
+ float: right;
206
+ padding-right: 0.5rem;
207
+ cursor: pointer;
208
+ }
209
+
210
+ .hint
211
+ {
212
+ color: var(--misc-text-color);
213
+ }
214
+
215
+ #data_div
216
+ {
217
+ margin-top: 1rem;
218
+ }
219
+
220
+ #data-table
221
+ {
222
+ width: 100%;
223
+ border-collapse: collapse;
224
+ border-spacing: 0;
225
+ }
226
+
227
+ /* Will be displayed when user specified custom format. */
228
+ #data-unparsed
229
+ {
230
+ background-color: var(--element-background-color);
231
+ margin-top: 0rem;
232
+ padding: 0.25rem 0.5rem;
233
+ display: none;
234
+ }
235
+
236
+ td
237
+ {
238
+ background-color: var(--element-background-color);
239
+ /* For wide tables any individual column will be no more than 50% of page width. */
240
+ max-width: 50vw;
241
+ /* The content is cut unless you hover. */
242
+ overflow: hidden;
243
+ text-overflow: ellipsis;
244
+ padding: 0.25rem 0.5rem;
245
+ border: 1px solid var(--border-color);
246
+ white-space: pre;
247
+ vertical-align: top;
248
+ }
249
+
250
+ .right
251
+ {
252
+ text-align: right;
253
+ }
254
+
255
+ th
256
+ {
257
+ padding: 0.25rem 0.5rem;
258
+ text-align: center;
259
+ background-color: var(--table-header-color);
260
+ border: 1px solid var(--border-color);
261
+ }
262
+
263
+ /* The row under mouse pointer is highlight for better legibility. */
264
+ tr:hover, tr:hover td
265
+ {
266
+ background-color: var(--table-hover-color);
267
+ }
268
+
269
+ tr:hover
270
+ {
271
+ box-shadow: 0 0 1rem rgba(0, 0, 0, 0.1);
272
+ }
273
+
274
+ #error
275
+ {
276
+ background: var(--error-color);
277
+ white-space: pre-wrap;
278
+ padding: 0.5rem 1rem;
279
+ display: none;
280
+ }
281
+
282
+ /* When mouse pointer is over table cell, will display full text (with wrap) instead of cut.
283
+ * We also keep it for some time on mouseout for "hysteresis" effect.
284
+ */
285
+ td.left:hover, .td-hover-hysteresis
286
+ {
287
+ white-space: pre-wrap;
288
+ max-width: none;
289
+ }
290
+
291
+ .td-selected
292
+ {
293
+ white-space: pre-wrap;
294
+ max-width: none;
295
+ background-color: var(--table-hover-color);
296
+ border: 2px solid var(--border-color);
297
+ }
298
+
299
+ td.transposed
300
+ {
301
+ max-width: none;
302
+ overflow: auto;
303
+ white-space: pre-wrap;
304
+ }
305
+
306
+ td.empty-result
307
+ {
308
+ text-align: center;
309
+ vertical-align: middle;
310
+ }
311
+
312
+ .row-number
313
+ {
314
+ width: 1%;
315
+ text-align: right;
316
+ background-color: var(--table-header-color);
317
+ color: var(--misc-text-color);
318
+ }
319
+
320
+ div.empty-result
321
+ {
322
+ opacity: 10%;
323
+ font-size: 7vw;
324
+ font-family: Liberation Sans, DejaVu Sans, sans-serif;
325
+ }
326
+
327
+ /* The style for SQL NULL */
328
+ .null
329
+ {
330
+ color: var(--null-color);
331
+ }
332
+
333
+ @keyframes hourglass-animation {
334
+ 0% {
335
+ transform: rotate(-180deg);
336
+ }
337
+ 50% {
338
+ transform: rotate(-180deg);
339
+ }
340
+ 100% {
341
+ transform: none;
342
+ }
343
+ }
344
+
345
+ #hourglass
346
+ {
347
+ display: none;
348
+ margin-left: 1rem;
349
+ font-size: 110%;
350
+ color: #888;
351
+ animation: hourglass-animation 1s linear infinite;
352
+ }
353
+
354
+ #check-mark
355
+ {
356
+ display: none;
357
+ padding-left: 1rem;
358
+ font-size: 110%;
359
+ color: #080;
360
+ }
361
+
362
+ a, a:visited
363
+ {
364
+ color: var(--link-color);
365
+ text-decoration: none;
366
+ }
367
+
368
+ #graph
369
+ {
370
+ display: none;
371
+ }
372
+
373
+ /* This is for graph in svg */
374
+ text
375
+ {
376
+ font-size: 14px;
377
+ fill: var(--text-color);
378
+ }
379
+
380
+ .node rect
381
+ {
382
+ fill: var(--element-background-color);
383
+ filter: drop-shadow(.2rem .2rem .2rem var(--shadow-color));
384
+ }
385
+
386
+ .edgePath path
387
+ {
388
+ stroke: var(--text-color);
389
+ }
390
+
391
+ marker
392
+ {
393
+ fill: var(--text-color);
394
+ }
395
+
396
+ #logo
397
+ {
398
+ fill: var(--logo-color);
399
+ }
400
+
401
+ #logo:hover
402
+ {
403
+ fill: var(--logo-color-active);
404
+ }
405
+
406
+ #logo-container
407
+ {
408
+ text-align: center;
409
+ margin-top: 5em;
410
+ }
411
+
412
+ #chart
413
+ {
414
+ background-color: var(--element-background-color);
415
+ filter: drop-shadow(.2rem .2rem .2rem var(--shadow-color));
416
+ display: none;
417
+ height: 70vh;
418
+ }
419
+
420
+ /* This is for charts (uPlot), Copyright (c) 2022 Leon Sorokin, MIT License, https://github.com/leeoniya/uPlot/ */
421
+ .u-wrap {position: relative;user-select: none;}
422
+ .u-over, .u-under, .u-axis {position: absolute;}
423
+ .u-under {overflow: hidden;}
424
+ .uplot canvas {display: block;position: relative;width: 100%;height: 100%;}
425
+ .u-legend {margin: auto;text-align: center; margin-top: 1em; font-family: Liberation Mono, DejaVu Sans Mono, MonoLisa, Consolas, monospace;}
426
+ .u-inline {display: block;}
427
+ .u-inline * {display: inline-block;}
428
+ .u-inline tr {margin-right: 16px;}
429
+ .u-legend th {font-weight: 600;}
430
+ .u-legend th > * {vertical-align: middle;display: inline-block;}
431
+ .u-legend td { min-width: 13em; }
432
+ .u-legend .u-marker {width: 1em;height: 1em;margin-right: 4px;background-clip: padding-box !important;}
433
+ .u-inline.u-live th::after {content: ":";vertical-align: middle;}
434
+ .u-inline:not(.u-live) .u-value {display: none;}
435
+ .u-series > * {padding: 4px;}
436
+ .u-series th {cursor: pointer;}
437
+ .u-legend .u-off > * {opacity: 0.3;}
438
+ .u-select {background: rgba(0,0,0,0.07);position: absolute;pointer-events: none;}
439
+ .u-cursor-x, .u-cursor-y {position: absolute;left: 0;top: 0;pointer-events: none;will-change: transform;z-index: 100;}
440
+ .u-hz .u-cursor-x, .u-vt .u-cursor-y {height: 100%;border-right: 1px dashed #607D8B;}
441
+ .u-hz .u-cursor-y, .u-vt .u-cursor-x {width: 100%;border-bottom: 1px dashed #607D8B;}
442
+ .u-cursor-pt {position: absolute;top: 0;left: 0;border-radius: 50%;border: 0 solid;pointer-events: none;will-change: transform;z-index: 100;/*this has to be !important since we set inline "background" shorthand */background-clip: padding-box !important;}
443
+ .u-axis.u-off, .u-select.u-off, .u-cursor-x.u-off, .u-cursor-y.u-off, .u-cursor-pt.u-off {display: none;}
444
+ </style>
445
+ </head>
446
+
447
+ <body>
448
+ <div id="controls">
449
+ <div id="inputs">
450
+ <input class="monospace shadow" id="url" type="text" value="http://localhost:8123/" placeholder="url" /><input class="monospace shadow" id="user" type="text" value="default" placeholder="user" /><input class="monospace shadow" id="password" type="password" placeholder="password" />
451
+ </div>
452
+ <div id="query_div">
453
+ <textarea autofocus spellcheck="false" class="monospace shadow" id="query"></textarea>
454
+ </div>
455
+ <div id="run_div">
456
+ <button class="shadow" id="run">Run</button>
457
+ <span class="hint">&nbsp;(Ctrl/Cmd+Enter)</span>
458
+ <span id="hourglass">⧗</span>
459
+ <span id="check-mark">✔</span>
460
+ <span id="stats"></span>
461
+ <span id="toggle-dark">🌑</span><span id="toggle-light">🌞</span>
462
+ </div>
463
+ </div>
464
+ <div id="data_div">
465
+ <table class="monospace-table shadow" id="data-table"></table>
466
+ <pre class="monospace-table shadow" id="data-unparsed"></pre>
467
+ </div>
468
+ <div id="chart"></div>
469
+ <svg id="graph" fill="none"></svg>
470
+ <p id="error" class="monospace shadow">
471
+ </p>
472
+ <p id="logo-container"></p>
473
+ </body>
474
+
475
+ <script type="text/javascript">
476
+
477
+ /// Incremental request number. When response is received,
478
+ /// if its request number does not equal to the current request number, response will be ignored.
479
+ /// This is to avoid race conditions.
480
+ let request_num = 0;
481
+
482
+ /// Save query in history only if it is different.
483
+ let previous_query = '';
484
+
485
+ const current_url = new URL(window.location);
486
+
487
+ const server_address = current_url.searchParams.get('url');
488
+ if (server_address) {
489
+ document.getElementById('url').value = server_address;
490
+ } else if (location.protocol != 'file:') {
491
+ /// Substitute the address of the server where the page is served.
492
+ document.getElementById('url').value = location.origin;
493
+ }
494
+
495
+ /// Substitute user name if it's specified in the query string
496
+ const user_from_url = current_url.searchParams.get('user');
497
+ if (user_from_url) {
498
+ document.getElementById('user').value = user_from_url;
499
+ }
500
+
501
+ function postImpl(posted_request_num, query)
502
+ {
503
+ const user = document.getElementById('user').value;
504
+ const password = document.getElementById('password').value;
505
+
506
+ const server_address = document.getElementById('url').value;
507
+
508
+ var url = server_address +
509
+ (server_address.indexOf('?') >= 0 ? '&' : '?') +
510
+ /// Ask server to allow cross-domain requests.
511
+ 'add_http_cors_header=1' +
512
+ '&default_format=JSONCompact' +
513
+ /// Safety settings to prevent results that browser cannot display.
514
+ '&max_result_rows=1000&max_result_bytes=10000000&result_overflow_mode=break';
515
+
516
+ // If play.html is opened locally, append username and password to the URL parameter to avoid CORS issue.
517
+ if (document.location.href.startsWith("file://")) {
518
+ url += '&user=' + encodeURIComponent(user) +
519
+ '&password=' + encodeURIComponent(password)
520
+ }
521
+
522
+ const xhr = new XMLHttpRequest;
523
+
524
+ xhr.open('POST', url, true);
525
+ // If play.html is open normally, use Basic auth to prevent username and password being exposed in URL parameters
526
+ if (!document.location.href.startsWith("file://")) {
527
+ xhr.setRequestHeader("Authorization", "Basic " + btoa(user+":"+password));
528
+ }
529
+ xhr.onreadystatechange = function()
530
+ {
531
+ if (posted_request_num != request_num) {
532
+ return;
533
+ } else if (this.readyState === XMLHttpRequest.DONE) {
534
+ renderResponse(this.status, this.response);
535
+
536
+ /// The query is saved in browser history (in state JSON object)
537
+ /// as well as in URL fragment identifier.
538
+ if (query != previous_query) {
539
+ const state = {
540
+ query: query,
541
+ status: this.status,
542
+ response: this.response.length > 100000 ? null : this.response /// Lower than the browser's limit.
543
+ };
544
+ const title = "Query: " + query;
545
+
546
+ let history_url = window.location.pathname + '?user=' + encodeURIComponent(user);
547
+ if (server_address != location.origin) {
548
+ /// Save server's address in URL if it's not identical to the address of the play UI.
549
+ history_url += '&url=' + encodeURIComponent(server_address);
550
+ }
551
+ history_url += '#' + window.btoa(query);
552
+
553
+ if (previous_query == '') {
554
+ history.replaceState(state, title, history_url);
555
+ } else {
556
+ history.pushState(state, title, history_url);
557
+ }
558
+ document.title = title;
559
+ previous_query = query;
560
+ }
561
+ } else {
562
+ //console.log(this);
563
+ }
564
+ }
565
+
566
+ document.getElementById('check-mark').style.display = 'none';
567
+ document.getElementById('hourglass').style.display = 'inline-block';
568
+
569
+ xhr.send(query);
570
+ }
571
+
572
+ function renderResponse(status, response) {
573
+ document.getElementById('hourglass').style.display = 'none';
574
+
575
+ if (status === 200) {
576
+ let json;
577
+ try { json = JSON.parse(response); } catch (e) {}
578
+
579
+ if (json !== undefined && json.statistics !== undefined) {
580
+ renderResult(json);
581
+ } else if (Array.isArray(json) && json.length == 2 &&
582
+ Array.isArray(json[0]) && Array.isArray(json[1]) && json[0].length > 1 && json[0].length == json[1].length) {
583
+ /// If user requested FORMAT JSONCompactColumns, we will render it as a chart.
584
+ renderChart(json);
585
+ } else {
586
+ renderUnparsedResult(response);
587
+ }
588
+ document.getElementById('check-mark').style.display = 'inline';
589
+ } else {
590
+ /// TODO: Proper rendering of network errors.
591
+ renderError(response);
592
+ }
593
+ }
594
+
595
+ let query_area = document.getElementById('query');
596
+
597
+ window.onpopstate = function(event) {
598
+ if (!event.state) {
599
+ return;
600
+ }
601
+ query_area.value = event.state.query;
602
+ if (!event.state.response) {
603
+ clear();
604
+ return;
605
+ }
606
+ renderResponse(event.state.status, event.state.response);
607
+ };
608
+
609
+ if (window.location.hash) {
610
+ query_area.value = window.atob(window.location.hash.substr(1));
611
+ }
612
+
613
+ function post()
614
+ {
615
+ ++request_num;
616
+ let query = query_area.value;
617
+ postImpl(request_num, query);
618
+ }
619
+
620
+ document.getElementById('run').onclick = function()
621
+ {
622
+ post();
623
+ }
624
+
625
+ document.onkeydown = function(event)
626
+ {
627
+ /// Firefox has code 13 for Enter and Chromium has code 10.
628
+ if ((event.metaKey || event.ctrlKey) && (event.keyCode == 13 || event.keyCode == 10)) {
629
+ post();
630
+ }
631
+ }
632
+
633
+ /// Pressing Tab in textarea will increase indentation.
634
+ /// But for accessibility reasons, we will fall back to tab navigation if the user already used Tab for that.
635
+
636
+ let user_prefers_tab_navigation = false;
637
+
638
+ [...document.querySelectorAll('input')].map(elem => {
639
+ elem.onkeydown = (e) => {
640
+ if (e.key == 'Tab') { user_prefers_tab_navigation = true; }
641
+ };
642
+ });
643
+
644
+ query_area.onkeydown = (e) => {
645
+ if (e.key == 'Tab' && !event.shiftKey && !user_prefers_tab_navigation) {
646
+ let elem = e.target;
647
+ let selection_start = elem.selectionStart;
648
+ let selection_end = elem.selectionEnd;
649
+
650
+ elem.value = elem.value.substring(0, elem.selectionStart) + ' ' + elem.value.substring(elem.selectionEnd);
651
+ elem.selectionStart = selection_start + 4;
652
+ elem.selectionEnd = selection_start + 4;
653
+
654
+ e.preventDefault();
655
+ return false;
656
+ }
657
+ };
658
+
659
+ function clearElement(id)
660
+ {
661
+ let elem = document.getElementById(id);
662
+ while (elem.firstChild) {
663
+ elem.removeChild(elem.lastChild);
664
+ }
665
+ elem.style.display = 'none';
666
+ }
667
+
668
+ function clear()
669
+ {
670
+ clearElement('data-table');
671
+ clearElement('graph');
672
+ clearElement('chart');
673
+ clearElement('data-unparsed');
674
+ clearElement('error');
675
+
676
+ document.getElementById('check-mark').display = 'none';
677
+ document.getElementById('hourglass').display = 'none';
678
+ document.getElementById('stats').innerText = '';
679
+ document.getElementById('logo-container').style.display = 'block';
680
+ }
681
+
682
+ function formatReadable(number = 0, decimals = 2, units = []) {
683
+ const k = 1000;
684
+ const i = number ? Math.floor(Math.log(number) / Math.log(k)) : 0;
685
+ const unit = units[i];
686
+ const dm = unit ? decimals : 0;
687
+ return Number(number / Math.pow(k, i)).toFixed(dm) + unit;
688
+ }
689
+
690
+ function formatReadableBytes(bytes) {
691
+ const units = [' B', ' KB', ' MB', ' GB', ' TB', ' PB', ' EB', ' ZB', ' YB'];
692
+
693
+ return formatReadable(bytes, 2, units);
694
+ }
695
+
696
+ function formatReadableRows(rows) {
697
+ const units = ['', ' thousand', ' million', ' billion', ' trillion', ' quadrillion'];
698
+
699
+ return formatReadable(rows, 2, units);
700
+ }
701
+
702
+ function renderResult(response)
703
+ {
704
+ clear();
705
+
706
+ let stats = document.getElementById('stats');
707
+ const seconds = response.statistics.elapsed.toFixed(3);
708
+ const rows = response.statistics.rows_read;
709
+ const bytes = response.statistics.bytes_read;
710
+ const formatted_bytes = formatReadableBytes(bytes);
711
+ const formatted_rows = formatReadableRows(rows);
712
+ stats.innerText = `Elapsed: ${seconds} sec, read ${formatted_rows} rows, ${formatted_bytes}.`;
713
+
714
+ /// We can also render graphs if user performed EXPLAIN PIPELINE graph=1 or EXPLAIN AST graph = 1
715
+ if (response.data.length > 3 && query_area.value.match(/^\s*EXPLAIN/i) && typeof(response.data[0][0]) === "string" && response.data[0][0].startsWith("digraph")) {
716
+ renderGraph(response);
717
+ } else {
718
+ renderTable(response);
719
+ }
720
+ }
721
+
722
+ function renderCell(cell, col_idx, settings)
723
+ {
724
+ let td = document.createElement('td');
725
+
726
+ let is_null = (cell === null);
727
+ let is_link = false;
728
+
729
+ /// Test: SELECT number, toString(number) AS str, number % 2 ? number : NULL AS nullable, range(number) AS arr, CAST((['hello', 'world'], [number, number % 2]) AS Map(String, UInt64)) AS map FROM numbers(10)
730
+ let text;
731
+ if (is_null) {
732
+ text = 'ᴺᵁᴸᴸ';
733
+ } else if (typeof(cell) === 'object') {
734
+ text = JSON.stringify(cell);
735
+ } else {
736
+ text = cell;
737
+
738
+ /// If it looks like URL, create a link. This is for convenience.
739
+ if (typeof(cell) == 'string' && cell.match(/^https?:\/\/\S+$/)) {
740
+ is_link = true;
741
+ }
742
+ }
743
+
744
+ let node = document.createTextNode(text);
745
+ if (is_link) {
746
+ let link = document.createElement('a');
747
+ link.appendChild(node);
748
+ link.href = text;
749
+ link.setAttribute('target', '_blank');
750
+ node = link;
751
+ }
752
+
753
+ if (settings.is_transposed) {
754
+ td.className = 'left transposed';
755
+ } else {
756
+ td.className = settings.column_is_number[col_idx] ? 'right' : 'left';
757
+ }
758
+ if (is_null) {
759
+ td.className += ' null';
760
+ }
761
+
762
+ /// If it's a number, render bar in background.
763
+ if (!settings.is_transposed && settings.column_need_render_bars[col_idx] && text > 0) {
764
+ const ratio = 100 * text / settings.column_maximums[col_idx];
765
+
766
+ let div = document.createElement('div');
767
+
768
+ div.style.width = '100%';
769
+ div.style.background = `linear-gradient(to right,
770
+ var(--bar-color) 0%, var(--bar-color) ${ratio}%,
771
+ transparent ${ratio}%, transparent 100%)`;
772
+
773
+ div.appendChild(node);
774
+ node = div;
775
+ }
776
+
777
+ td.appendChild(node);
778
+ return td;
779
+ }
780
+
781
+ function renderTableTransposed(response)
782
+ {
783
+ let tbody = document.createElement('tbody');
784
+ for (let col_idx in response.meta) {
785
+ let tr = document.createElement('tr');
786
+ {
787
+ let th = document.createElement('th');
788
+ th.className = 'right';
789
+ th.style.width = '0';
790
+ th.appendChild(document.createTextNode(response.meta[col_idx].name));
791
+ tr.appendChild(th);
792
+ }
793
+ for (let row_idx in response.data)
794
+ {
795
+ let cell = response.data[row_idx][col_idx];
796
+ const td = renderCell(cell, col_idx, {is_transposed: true});
797
+ tr.appendChild(td);
798
+ }
799
+ if (response.data.length == 0 && col_idx == 0)
800
+ {
801
+ /// If result is empty, show this fact with a style.
802
+ let td = document.createElement('td');
803
+ td.rowSpan = response.meta.length;
804
+ td.className = 'empty-result';
805
+ let div = document.createElement('div');
806
+ div.appendChild(document.createTextNode("empty result"));
807
+ div.className = 'empty-result';
808
+ td.appendChild(div);
809
+ tr.appendChild(td);
810
+ }
811
+ tbody.appendChild(tr);
812
+ }
813
+ let table = document.getElementById('data-table');
814
+ table.appendChild(tbody);
815
+ table.style.display = 'table';
816
+ }
817
+
818
+ function renderTable(response)
819
+ {
820
+ if (response.data.length <= 1 && response.meta.length >= 5) {
821
+ renderTableTransposed(response)
822
+ return;
823
+ }
824
+
825
+ const should_display_row_numbers = response.data.length > 3;
826
+
827
+ let thead = document.createElement('thead');
828
+
829
+ if (should_display_row_numbers) {
830
+ let th = document.createElement('th');
831
+ th.className = 'row-number';
832
+ th.appendChild(document.createTextNode('№'));
833
+ thead.appendChild(th);
834
+ }
835
+
836
+ for (let idx in response.meta) {
837
+ let th = document.createElement('th');
838
+ const name = document.createTextNode(response.meta[idx].name);
839
+ th.appendChild(name);
840
+ thead.appendChild(th);
841
+ }
842
+
843
+ /// To prevent hanging the browser, limit the number of cells in a table.
844
+ /// It's important to have the limit on number of cells, not just rows, because tables may be wide or narrow.
845
+ /// Also we permit rendering of more records but only if elapsed time is not large.
846
+ const max_rows = 10000 / response.meta.length;
847
+ const max_render_ms = 200;
848
+ let row_num = 0;
849
+
850
+ const column_is_number = response.meta.map(elem => !!elem.type.match(/^(Nullable\()?(U?Int|Decimal|Float)/));
851
+ const column_maximums = column_is_number.map((elem, idx) => elem ? Math.max(...response.data.map(row => row[idx])) : 0);
852
+ const column_minimums = column_is_number.map((elem, idx) => elem ? Math.min(...response.data.map(row => Math.max(0, row[idx]))) : 0);
853
+ const column_need_render_bars = column_is_number.map((elem, idx) => column_maximums[idx] > 0 && column_maximums[idx] > column_minimums[idx]);
854
+
855
+ const settings = {
856
+ is_transposed: false,
857
+ column_is_number: column_is_number,
858
+ column_maximums: column_maximums,
859
+ column_minimums: column_minimums,
860
+ column_need_render_bars: column_need_render_bars,
861
+ };
862
+
863
+ const start_time = performance.now();
864
+
865
+ let tbody = document.createElement('tbody');
866
+ for (let row_idx in response.data) {
867
+ let tr = document.createElement('tr');
868
+ if (should_display_row_numbers) {
869
+ let td = document.createElement('td');
870
+ td.className = 'row-number';
871
+ td.appendChild(document.createTextNode(1 + +row_idx));
872
+ tr.appendChild(td);
873
+ }
874
+ for (let col_idx in response.data[row_idx]) {
875
+ let cell = response.data[row_idx][col_idx];
876
+ const td = renderCell(cell, col_idx, settings);
877
+
878
+ td.onclick = () => { td.classList.add('td-selected') };
879
+ td.onmouseenter = () => {
880
+ td.classList.add('td-hover-hysteresis');
881
+ td.onmouseleave = () => {
882
+ setTimeout(() => { td && td.classList.remove('td-hover-hysteresis') }, 1000);
883
+ };
884
+ };
885
+
886
+ tr.appendChild(td);
887
+ }
888
+ tbody.appendChild(tr);
889
+
890
+ ++row_num;
891
+ if (row_num >= max_rows && performance.now() - start_time >= max_render_ms) {
892
+ break;
893
+ }
894
+ }
895
+
896
+ let table = document.getElementById('data-table');
897
+ table.appendChild(thead);
898
+ table.appendChild(tbody);
899
+ table.style.display = 'table';
900
+ }
901
+
902
+ /// A function to render raw data when non-default format is specified.
903
+ function renderUnparsedResult(response)
904
+ {
905
+ clear();
906
+ let data = document.getElementById('data-unparsed')
907
+
908
+ if (response === '') {
909
+ /// TODO: Fade or remove previous result when new request will be performed.
910
+ response = 'Ok.';
911
+ }
912
+
913
+ data.innerText = response;
914
+ /// inline-block make width adjust to the size of content.
915
+ data.style.display = 'inline-block';
916
+ }
917
+
918
+ function renderError(response)
919
+ {
920
+ clear();
921
+ document.getElementById('error').innerText = response ? response : "No response.";
922
+ document.getElementById('error').style.display = 'block';
923
+ document.getElementById('logo-container').style.display = 'none';
924
+ }
925
+
926
+ /// Huge JS libraries should be loaded only if needed.
927
+ function loadJS(src, integrity) {
928
+ return new Promise((resolve, reject) => {
929
+ const script = document.createElement('script');
930
+ script.src = src;
931
+ if (integrity) {
932
+ script.crossOrigin = 'anonymous';
933
+ script.integrity = integrity;
934
+ } else {
935
+ console.warn('no integrity for', src)
936
+ }
937
+ script.addEventListener('load', function() { resolve(true); });
938
+ document.head.appendChild(script);
939
+ });
940
+ }
941
+
942
+ let load_dagre_promise;
943
+ function loadDagre() {
944
+ if (load_dagre_promise) { return load_dagre_promise; }
945
+
946
+ load_dagre_promise = Promise.all([
947
+ loadJS('https://dagrejs.github.io/project/dagre/v0.8.5/dagre.min.js',
948
+ 'sha384-2IH3T69EIKYC4c+RXZifZRvaH5SRUdacJW7j6HtE5rQbvLhKKdawxq6vpIzJ7j9M'),
949
+ loadJS('https://dagrejs.github.io/project/graphlib-dot/v0.6.4/graphlib-dot.min.js',
950
+ 'sha384-Q7oatU+b+y0oTkSoiRH9wTLH6sROySROCILZso/AbMMm9uKeq++r8ujD4l4f+CWj'),
951
+ loadJS('https://dagrejs.github.io/project/dagre-d3/v0.6.4/dagre-d3.min.js',
952
+ 'sha384-9N1ty7Yz7VKL3aJbOk+8ParYNW8G5W+MvxEfFL9G7CRYPmkHI9gJqyAfSI/8190W'),
953
+ loadJS('https://cdn.jsdelivr.net/npm/d3@7.0.0',
954
+ 'sha384-S+Kf0r6YzKIhKA8d1k2/xtYv+j0xYUU3E7+5YLrcPVab6hBh/r1J6cq90OXhw80u'),
955
+ ]);
956
+
957
+ return load_dagre_promise;
958
+ }
959
+
960
+ async function renderGraph(response)
961
+ {
962
+ await loadDagre();
963
+
964
+ /// https://github.com/dagrejs/dagre-d3/issues/131
965
+ const dot = response.data.reduce((acc, row) => acc + '\n' + row[0].replace(/shape\s*=\s*box/g, 'shape=rect'));
966
+
967
+ let graph = graphlibDot.read(dot);
968
+ graph.graph().rankdir = 'TB';
969
+
970
+ let render = new dagreD3.render();
971
+
972
+ let svg = document.getElementById('graph');
973
+ svg.style.display = 'block';
974
+
975
+ render(d3.select("#graph"), graph);
976
+
977
+ svg.style.width = graph.graph().width;
978
+ svg.style.height = graph.graph().height;
979
+ }
980
+
981
+ let load_uplot_promise;
982
+ function loadUplot() {
983
+ if (load_uplot_promise) { return load_uplot_promise; }
984
+ load_uplot_promise = loadJS('https://cdn.jsdelivr.net/npm/uplot@1.6.21/dist/uPlot.iife.min.js',
985
+ 'sha384-TwdJPnTsKP6pnvFZZKda0WJCXpjcHCa7MYHmjrYDu6rsEsb/UnFdoL0phS5ODqTA');
986
+ return load_uplot_promise;
987
+ }
988
+
989
+ let uplot;
990
+ async function renderChart(json)
991
+ {
992
+ await loadUplot();
993
+ clear();
994
+
995
+ let chart = document.getElementById('chart');
996
+ chart.style.display = 'block';
997
+
998
+ let paths = uPlot.paths.stepped({align: 1});
999
+
1000
+ const [line_color, fill_color, grid_color, axes_color] = theme == 'light'
1001
+ ? ["#F80", "#FED", "#c7d0d9", "#2c3235"]
1002
+ : ["#888", "#045", "#2c3235", "#c7d0d9"];
1003
+
1004
+ const opts = {
1005
+ width: chart.clientWidth,
1006
+ height: chart.clientHeight,
1007
+ scales: { x: { time: json[0][0] > 1000000000 && json[0][0] < 2000000000 } },
1008
+ axes: [ { stroke: axes_color,
1009
+ grid: { width: 1 / devicePixelRatio, stroke: grid_color },
1010
+ ticks: { width: 1 / devicePixelRatio, stroke: grid_color } },
1011
+ { stroke: axes_color,
1012
+ grid: { width: 1 / devicePixelRatio, stroke: grid_color },
1013
+ ticks: { width: 1 / devicePixelRatio, stroke: grid_color } } ],
1014
+ series: [ { label: "x" },
1015
+ { label: "y", stroke: line_color, fill: fill_color,
1016
+ drawStyle: 0, lineInterpolation: 1, paths } ],
1017
+ padding: [ null, null, null, (Math.ceil(Math.log10(Math.max(...json[1]))) + Math.floor(Math.log10(Math.max(...json[1])) / 3)) * 6 ],
1018
+ };
1019
+
1020
+ uplot = new uPlot(opts, json, chart);
1021
+ }
1022
+
1023
+ function resizeChart() {
1024
+ if (uplot) {
1025
+ let chart = document.getElementById('chart');
1026
+ uplot.setSize({ width: chart.clientWidth, height: chart.clientHeight });
1027
+ }
1028
+ }
1029
+
1030
+ function redrawChart() {
1031
+ if (uplot && document.getElementById('chart').style.display == 'block') {
1032
+ renderChart(uplot.data);
1033
+ }
1034
+ }
1035
+
1036
+ new ResizeObserver(resizeChart).observe(document.getElementById('chart'));
1037
+
1038
+ /// First we check if theme is set via the 'theme' GET parameter, if not, we check localStorage, otherwise we check OS preference.
1039
+ let theme = current_url.searchParams.get('theme');
1040
+ if (['dark', 'light'].indexOf(theme) === -1) {
1041
+ theme = window.localStorage.getItem('theme');
1042
+ }
1043
+ if (!theme) {
1044
+ theme = 'light';
1045
+ }
1046
+
1047
+ function setColorTheme(new_theme, update_preference) {
1048
+ theme = new_theme;
1049
+ if (update_preference) {
1050
+ window.localStorage.setItem('theme', theme);
1051
+ }
1052
+ document.documentElement.setAttribute('data-theme', theme);
1053
+ redrawChart();
1054
+ }
1055
+
1056
+ if (theme) {
1057
+ document.documentElement.setAttribute('data-theme', theme);
1058
+ } else {
1059
+ /// Obtain system-level user preference
1060
+ const media_query_list = window.matchMedia('(prefers-color-scheme: dark)');
1061
+ if (media_query_list.matches) {
1062
+ setColorTheme('dark');
1063
+ }
1064
+
1065
+ /// There is a rumor that on some computers, the theme is changing automatically on day/night.
1066
+ media_query_list.addEventListener('change', function(e) {
1067
+ setColorTheme(e.matches ? 'dark' : 'light');
1068
+ });
1069
+ }
1070
+
1071
+ document.getElementById('toggle-light').onclick = function() {
1072
+ setColorTheme('light', true);
1073
+ }
1074
+
1075
+ document.getElementById('toggle-dark').onclick = function() {
1076
+ setColorTheme('dark', true);
1077
+ }
1078
+ </script>
1079
+ </html>
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ Flask
2
+ chdb