mascot-vis 4.4.0 → 4.4.1
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/dist/mascot-cjs.cjs +2 -2
- package/dist/mascot-esm.js +2 -2
- package/dist/mascot-umd.js +2 -2
- package/index-new.html +1278 -0
- package/package.json +1 -1
package/index-new.html
ADDED
|
@@ -0,0 +1,1278 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<title>Mascot</title>
|
|
6
|
+
<script src="lib/d3.v7.min.js"></script>
|
|
7
|
+
<script src="lib/dagre.min.js"></script>
|
|
8
|
+
<script src="lib/three.js"></script>
|
|
9
|
+
<script src="lib/pixi.min.js"></script>
|
|
10
|
+
<script src="dist/mascot-umd.js"></script>
|
|
11
|
+
<script src="lib/codemirror.js"></script>
|
|
12
|
+
<script src="lib/acorn.js"></script>
|
|
13
|
+
<link rel="stylesheet" href="lib/codemirror.css" />
|
|
14
|
+
<script src="lib/javascript.js"></script>
|
|
15
|
+
<script src="lib/jquery-1.12.4.js"></script>
|
|
16
|
+
<script src="lib/jquery-ui.js"></script>
|
|
17
|
+
<link rel="stylesheet" href="lib/jquery-ui.css" />
|
|
18
|
+
<script src="js/depGraphVis.js"></script>
|
|
19
|
+
<link
|
|
20
|
+
rel="stylesheet"
|
|
21
|
+
type="text/css"
|
|
22
|
+
href="https://cdn.datatables.net/2.3.2/css/dataTables.dataTables.min.css"
|
|
23
|
+
/>
|
|
24
|
+
<script
|
|
25
|
+
type="text/javascript"
|
|
26
|
+
charset="utf8"
|
|
27
|
+
src="https://cdn.datatables.net/2.3.2/js/dataTables.min.js"
|
|
28
|
+
></script>
|
|
29
|
+
|
|
30
|
+
<style>
|
|
31
|
+
html {
|
|
32
|
+
margin: 0;
|
|
33
|
+
padding: 0;
|
|
34
|
+
height: 100%;
|
|
35
|
+
font-family: Arial;
|
|
36
|
+
}
|
|
37
|
+
body {
|
|
38
|
+
height: 100vh;
|
|
39
|
+
margin: 0;
|
|
40
|
+
padding: 0;
|
|
41
|
+
background: #f2f2f2;
|
|
42
|
+
overflow: hidden;
|
|
43
|
+
}
|
|
44
|
+
#topBar {
|
|
45
|
+
position: relative;
|
|
46
|
+
height: 42px;
|
|
47
|
+
border-bottom: 1px solid #bbb;
|
|
48
|
+
background: #e8e8e8;
|
|
49
|
+
}
|
|
50
|
+
#topActions {
|
|
51
|
+
position: absolute;
|
|
52
|
+
left: 12px;
|
|
53
|
+
top: 7px;
|
|
54
|
+
display: flex;
|
|
55
|
+
align-items: center;
|
|
56
|
+
gap: 8px;
|
|
57
|
+
}
|
|
58
|
+
/* Centered: previous button, demo name + position, next button. */
|
|
59
|
+
#demoNav {
|
|
60
|
+
position: absolute;
|
|
61
|
+
left: 50%;
|
|
62
|
+
top: 7px;
|
|
63
|
+
transform: translateX(-50%);
|
|
64
|
+
display: flex;
|
|
65
|
+
align-items: center;
|
|
66
|
+
gap: 14px;
|
|
67
|
+
max-width: calc(100% - 300px);
|
|
68
|
+
}
|
|
69
|
+
#demoNavTitle {
|
|
70
|
+
display: flex;
|
|
71
|
+
align-items: baseline;
|
|
72
|
+
gap: 8px;
|
|
73
|
+
min-width: 0;
|
|
74
|
+
}
|
|
75
|
+
#demoNavName {
|
|
76
|
+
min-width: 0;
|
|
77
|
+
font-size: 15px;
|
|
78
|
+
font-weight: bold;
|
|
79
|
+
color: #243447;
|
|
80
|
+
}
|
|
81
|
+
#demoNavName .demoLabel {
|
|
82
|
+
align-items: center;
|
|
83
|
+
}
|
|
84
|
+
#demoNavPos {
|
|
85
|
+
flex-shrink: 0;
|
|
86
|
+
font-size: 12px;
|
|
87
|
+
color: #777;
|
|
88
|
+
}
|
|
89
|
+
#showGalleryBtn,
|
|
90
|
+
.navBtn {
|
|
91
|
+
border: 1px solid #9aa3ad;
|
|
92
|
+
border-radius: 999px;
|
|
93
|
+
background: linear-gradient(#ffffff, #edf1f5);
|
|
94
|
+
color: #243447;
|
|
95
|
+
padding: 6px 14px;
|
|
96
|
+
font-size: 12px;
|
|
97
|
+
font-weight: 600;
|
|
98
|
+
cursor: pointer;
|
|
99
|
+
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.08);
|
|
100
|
+
}
|
|
101
|
+
#showGalleryBtn:hover,
|
|
102
|
+
.navBtn:hover {
|
|
103
|
+
background: linear-gradient(#ffffff, #e3e9ef);
|
|
104
|
+
border-color: #7d8792;
|
|
105
|
+
}
|
|
106
|
+
#showGalleryBtn:active,
|
|
107
|
+
.navBtn:active {
|
|
108
|
+
transform: translateY(1px);
|
|
109
|
+
}
|
|
110
|
+
#showGalleryBtn:disabled,
|
|
111
|
+
.navBtn:disabled {
|
|
112
|
+
opacity: 0.5;
|
|
113
|
+
cursor: not-allowed;
|
|
114
|
+
pointer-events: none;
|
|
115
|
+
background: linear-gradient(#f0f0f0, #e0e0e0);
|
|
116
|
+
border-color: #ccc;
|
|
117
|
+
}
|
|
118
|
+
#demoSidebar {
|
|
119
|
+
width: 213px;
|
|
120
|
+
height: calc(100% - 43px);
|
|
121
|
+
border-right: 1px solid #ccc;
|
|
122
|
+
background: #f2f2f2;
|
|
123
|
+
float: left;
|
|
124
|
+
}
|
|
125
|
+
#tagFilters {
|
|
126
|
+
width: 182px;
|
|
127
|
+
max-width: 182px;
|
|
128
|
+
height: calc(100% - 48px);
|
|
129
|
+
padding: 0 5px 0 25px;
|
|
130
|
+
font-size: 12px;
|
|
131
|
+
overflow-y: auto;
|
|
132
|
+
}
|
|
133
|
+
#demoSearchWrap {
|
|
134
|
+
width: 182px;
|
|
135
|
+
max-width: 182px;
|
|
136
|
+
background: transparent;
|
|
137
|
+
padding: 10px 5px 8px 25px;
|
|
138
|
+
box-sizing: content-box;
|
|
139
|
+
}
|
|
140
|
+
#demoSearch {
|
|
141
|
+
width: calc(100% - 20px);
|
|
142
|
+
border: 1px solid #bbb;
|
|
143
|
+
border-radius: 4px;
|
|
144
|
+
padding: 6px 8px;
|
|
145
|
+
font-size: 12px;
|
|
146
|
+
outline: none;
|
|
147
|
+
}
|
|
148
|
+
#demoSearch:focus {
|
|
149
|
+
border-color: #7d8792;
|
|
150
|
+
box-shadow: 0 0 0 2px rgba(125, 135, 146, 0.15);
|
|
151
|
+
}
|
|
152
|
+
.tagGroupHeader {
|
|
153
|
+
display: flex;
|
|
154
|
+
justify-content: space-between;
|
|
155
|
+
align-items: baseline;
|
|
156
|
+
margin: 12px 0 6px;
|
|
157
|
+
font-size: 11px;
|
|
158
|
+
font-weight: bold;
|
|
159
|
+
text-transform: uppercase;
|
|
160
|
+
letter-spacing: 0.04em;
|
|
161
|
+
color: #666;
|
|
162
|
+
}
|
|
163
|
+
.tagGroup + .tagGroupHeader {
|
|
164
|
+
margin-top: 26px;
|
|
165
|
+
}
|
|
166
|
+
.tagGroupClear {
|
|
167
|
+
font-weight: normal;
|
|
168
|
+
text-transform: none;
|
|
169
|
+
letter-spacing: 0;
|
|
170
|
+
color: #4a6a8a;
|
|
171
|
+
cursor: pointer;
|
|
172
|
+
}
|
|
173
|
+
.tagGroupClear:hover {
|
|
174
|
+
text-decoration: underline;
|
|
175
|
+
}
|
|
176
|
+
.tagGroup {
|
|
177
|
+
list-style: none;
|
|
178
|
+
margin: 0 0 6px;
|
|
179
|
+
padding: 0;
|
|
180
|
+
}
|
|
181
|
+
.tagItem {
|
|
182
|
+
position: relative;
|
|
183
|
+
display: flex;
|
|
184
|
+
align-items: center;
|
|
185
|
+
gap: 6px;
|
|
186
|
+
margin: 0 0 2px -6px;
|
|
187
|
+
padding: 4px 6px;
|
|
188
|
+
border-radius: 4px;
|
|
189
|
+
cursor: pointer;
|
|
190
|
+
user-select: none;
|
|
191
|
+
}
|
|
192
|
+
.tagItem:hover {
|
|
193
|
+
background: #e4e7eb;
|
|
194
|
+
}
|
|
195
|
+
.tagItem.selected {
|
|
196
|
+
background: #243447;
|
|
197
|
+
color: #fff;
|
|
198
|
+
}
|
|
199
|
+
.tagItem.selected .demoIcon {
|
|
200
|
+
background: #fff;
|
|
201
|
+
color: #5b7083;
|
|
202
|
+
}
|
|
203
|
+
.tagItemLabel {
|
|
204
|
+
flex: 1;
|
|
205
|
+
min-width: 0;
|
|
206
|
+
}
|
|
207
|
+
.tagItemCount {
|
|
208
|
+
font-size: 11px;
|
|
209
|
+
color: #888;
|
|
210
|
+
}
|
|
211
|
+
.tagItem.selected .tagItemCount {
|
|
212
|
+
color: #c9d3de;
|
|
213
|
+
}
|
|
214
|
+
.tagItem.disabled {
|
|
215
|
+
color: #aaa;
|
|
216
|
+
cursor: default;
|
|
217
|
+
}
|
|
218
|
+
.tagItem.disabled:hover {
|
|
219
|
+
background: transparent;
|
|
220
|
+
}
|
|
221
|
+
.tagItem.disabled .demoIcon {
|
|
222
|
+
opacity: 0.4;
|
|
223
|
+
}
|
|
224
|
+
/* Checkbox for adding to the selection; shown on hover and on selected items. */
|
|
225
|
+
/* First item in the row, so its left edge lines up with the group heading. */
|
|
226
|
+
.tagCheck {
|
|
227
|
+
position: relative;
|
|
228
|
+
flex-shrink: 0;
|
|
229
|
+
width: 10px;
|
|
230
|
+
height: 10px;
|
|
231
|
+
border: 1px solid #8a96a3;
|
|
232
|
+
border-radius: 2px;
|
|
233
|
+
background: #fff;
|
|
234
|
+
visibility: hidden;
|
|
235
|
+
cursor: pointer;
|
|
236
|
+
}
|
|
237
|
+
.tagItem:not(.disabled):hover .tagCheck,
|
|
238
|
+
.tagItem.selected .tagCheck {
|
|
239
|
+
visibility: visible;
|
|
240
|
+
}
|
|
241
|
+
.tagItem.selected .tagCheck {
|
|
242
|
+
border-color: #fff;
|
|
243
|
+
background: #fff;
|
|
244
|
+
}
|
|
245
|
+
.tagItem.selected .tagCheck::after {
|
|
246
|
+
content: "";
|
|
247
|
+
position: absolute;
|
|
248
|
+
left: 3px;
|
|
249
|
+
top: 0px;
|
|
250
|
+
width: 3px;
|
|
251
|
+
height: 6px;
|
|
252
|
+
border: solid #243447;
|
|
253
|
+
border-width: 0 1.5px 1.5px 0;
|
|
254
|
+
transform: rotate(45deg);
|
|
255
|
+
}
|
|
256
|
+
#galleryEmpty {
|
|
257
|
+
margin: 24px 16px;
|
|
258
|
+
color: #666;
|
|
259
|
+
font-size: 13px;
|
|
260
|
+
}
|
|
261
|
+
#canvasElement {
|
|
262
|
+
width: 1600;
|
|
263
|
+
height: 1000;
|
|
264
|
+
margin: 0;
|
|
265
|
+
padding: 0;
|
|
266
|
+
position: absolute;
|
|
267
|
+
top: 43px;
|
|
268
|
+
left: 213px;
|
|
269
|
+
}
|
|
270
|
+
#svgElement {
|
|
271
|
+
width: calc(100% - 213px);
|
|
272
|
+
height: calc(100% - 43px);
|
|
273
|
+
margin: 0;
|
|
274
|
+
padding: 0;
|
|
275
|
+
position: absolute;
|
|
276
|
+
top: 43px;
|
|
277
|
+
left: 213px;
|
|
278
|
+
background: white;
|
|
279
|
+
user-select: none;
|
|
280
|
+
}
|
|
281
|
+
#gallery,
|
|
282
|
+
#depGraphVis {
|
|
283
|
+
width: calc(100% - 213px);
|
|
284
|
+
height: calc(100% - 43px);
|
|
285
|
+
overflow-y: auto;
|
|
286
|
+
background: #fff;
|
|
287
|
+
position: absolute;
|
|
288
|
+
top: 43px;
|
|
289
|
+
right: 0;
|
|
290
|
+
z-index: 1000;
|
|
291
|
+
}
|
|
292
|
+
.galleryItem {
|
|
293
|
+
max-width: 220px;
|
|
294
|
+
min-width: 220px;
|
|
295
|
+
height: 170px;
|
|
296
|
+
vertical-align: text-bottom;
|
|
297
|
+
text-align: center;
|
|
298
|
+
border: 2px solid #eee;
|
|
299
|
+
display: inline-block;
|
|
300
|
+
margin: 10px 15px;
|
|
301
|
+
transition: box-shadow 0.15s ease, transform 0.15s ease;
|
|
302
|
+
}
|
|
303
|
+
.galleryItemName {
|
|
304
|
+
font-family: Verdana, Geneva, sans-serif;
|
|
305
|
+
font-size: 11.5px;
|
|
306
|
+
font-weight: 400;
|
|
307
|
+
}
|
|
308
|
+
.gallerySection {
|
|
309
|
+
display: flex;
|
|
310
|
+
align-items: baseline;
|
|
311
|
+
gap: 8px;
|
|
312
|
+
margin: 22px 15px 2px;
|
|
313
|
+
padding-bottom: 6px;
|
|
314
|
+
border-bottom: 1px solid #e3e3e3;
|
|
315
|
+
font-size: 14px;
|
|
316
|
+
font-weight: bold;
|
|
317
|
+
color: #333;
|
|
318
|
+
}
|
|
319
|
+
.gallerySectionCount {
|
|
320
|
+
font-size: 12px;
|
|
321
|
+
font-weight: normal;
|
|
322
|
+
color: #888;
|
|
323
|
+
}
|
|
324
|
+
/* Lift the card slightly with a soft shadow below it. */
|
|
325
|
+
.galleryItem:hover {
|
|
326
|
+
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.16), 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
327
|
+
transform: translateY(-2px);
|
|
328
|
+
cursor: pointer;
|
|
329
|
+
}
|
|
330
|
+
.demoLabel {
|
|
331
|
+
display: flex;
|
|
332
|
+
align-items: center;
|
|
333
|
+
gap: 4px;
|
|
334
|
+
min-width: 0;
|
|
335
|
+
}
|
|
336
|
+
.demoLabelName {
|
|
337
|
+
overflow: hidden;
|
|
338
|
+
text-overflow: ellipsis;
|
|
339
|
+
white-space: nowrap;
|
|
340
|
+
}
|
|
341
|
+
.demoIcon {
|
|
342
|
+
display: inline-flex;
|
|
343
|
+
align-items: center;
|
|
344
|
+
justify-content: center;
|
|
345
|
+
flex-shrink: 0;
|
|
346
|
+
width: 14px;
|
|
347
|
+
height: 14px;
|
|
348
|
+
border-radius: 3px;
|
|
349
|
+
background: #5b7083;
|
|
350
|
+
color: #fff;
|
|
351
|
+
}
|
|
352
|
+
.demoIcon svg {
|
|
353
|
+
width: 10px;
|
|
354
|
+
height: 10px;
|
|
355
|
+
}
|
|
356
|
+
#console {
|
|
357
|
+
width: calc(100% - 228px);
|
|
358
|
+
position: absolute;
|
|
359
|
+
left: 221px;
|
|
360
|
+
height: 70%;
|
|
361
|
+
resize: none;
|
|
362
|
+
border: none;
|
|
363
|
+
display: none;
|
|
364
|
+
padding: 60px;
|
|
365
|
+
}
|
|
366
|
+
#btmPanel {
|
|
367
|
+
width: calc(100% - 213px);
|
|
368
|
+
height: 260px;
|
|
369
|
+
position: absolute;
|
|
370
|
+
bottom: 0;
|
|
371
|
+
right: 0;
|
|
372
|
+
z-index: 950;
|
|
373
|
+
}
|
|
374
|
+
#ctrlPanel {
|
|
375
|
+
width: 250px;
|
|
376
|
+
height: calc(100% - 42px);
|
|
377
|
+
position: absolute;
|
|
378
|
+
right: 0;
|
|
379
|
+
top: 42px;
|
|
380
|
+
background-color: #cddde9;
|
|
381
|
+
padding: 40px 20px;
|
|
382
|
+
display: none;
|
|
383
|
+
z-index: 900;
|
|
384
|
+
}
|
|
385
|
+
.ui-tabs {
|
|
386
|
+
padding: 0;
|
|
387
|
+
}
|
|
388
|
+
.ui-tabs .ui-tabs-panel {
|
|
389
|
+
padding: 0;
|
|
390
|
+
}
|
|
391
|
+
.ui-tabs .ui-tabs-nav li a {
|
|
392
|
+
font-size: 8.5pt !important;
|
|
393
|
+
padding: 0;
|
|
394
|
+
}
|
|
395
|
+
#codeTab,
|
|
396
|
+
#dataTab,
|
|
397
|
+
#jsonTab {
|
|
398
|
+
width: 100%;
|
|
399
|
+
height: calc(100% - 40px);
|
|
400
|
+
}
|
|
401
|
+
#codeControls {
|
|
402
|
+
width: auto;
|
|
403
|
+
height: 30px;
|
|
404
|
+
position: absolute;
|
|
405
|
+
top: 0;
|
|
406
|
+
right: 0;
|
|
407
|
+
margin: 8px 10px 0 0;
|
|
408
|
+
font-size: 8.5pt;
|
|
409
|
+
vertical-align: top;
|
|
410
|
+
display: flex;
|
|
411
|
+
align-items: flex-start;
|
|
412
|
+
justify-content: flex-end;
|
|
413
|
+
gap: 4px;
|
|
414
|
+
}
|
|
415
|
+
.rendererControls {
|
|
416
|
+
display: none;
|
|
417
|
+
}
|
|
418
|
+
.hiddenControl {
|
|
419
|
+
display: none;
|
|
420
|
+
}
|
|
421
|
+
.CodeMirror {
|
|
422
|
+
width: 100%;
|
|
423
|
+
height: 100%;
|
|
424
|
+
resize: none;
|
|
425
|
+
border: none;
|
|
426
|
+
padding: 0;
|
|
427
|
+
font-family: Courier;
|
|
428
|
+
font-size: 12.5px;
|
|
429
|
+
overflow-x: hidden;
|
|
430
|
+
}
|
|
431
|
+
</style>
|
|
432
|
+
</head>
|
|
433
|
+
<body>
|
|
434
|
+
<div id="topBar">
|
|
435
|
+
<div id="topActions">
|
|
436
|
+
<button id="showGalleryBtn" onclick="window.showGalleryView()">
|
|
437
|
+
Show Gallery
|
|
438
|
+
</button>
|
|
439
|
+
</div>
|
|
440
|
+
<div id="demoNav" style="visibility: hidden">
|
|
441
|
+
<button id="prevDemoBtn" class="navBtn" onclick="window.stepDemo(-1)">‹ Previous</button>
|
|
442
|
+
<div id="demoNavTitle">
|
|
443
|
+
<span id="demoNavName"></span>
|
|
444
|
+
<span id="demoNavPos"></span>
|
|
445
|
+
</div>
|
|
446
|
+
<button id="nextDemoBtn" class="navBtn" onclick="window.stepDemo(1)">Next ›</button>
|
|
447
|
+
</div>
|
|
448
|
+
</div>
|
|
449
|
+
|
|
450
|
+
<div id="demoSidebar">
|
|
451
|
+
<div id="demoSearchWrap">
|
|
452
|
+
<input id="demoSearch" type="text" placeholder="Search ..." aria-label="Search demos" />
|
|
453
|
+
</div>
|
|
454
|
+
|
|
455
|
+
<div id="tagFilters"></div>
|
|
456
|
+
</div>
|
|
457
|
+
|
|
458
|
+
<canvas id="canvasElement"></canvas>
|
|
459
|
+
<svg id="svgElement" xmlns="http://www.w3.org/2000/svg"></svg>
|
|
460
|
+
<div id="console"></div>
|
|
461
|
+
<div id="ctrlPanel"></div>
|
|
462
|
+
|
|
463
|
+
<div id="btmPanel">
|
|
464
|
+
<ul>
|
|
465
|
+
<li><a href="#codeTab">Code</a></li>
|
|
466
|
+
<li><a id="dataTabLi" href="#dataTab">Data</a></li>
|
|
467
|
+
<li><a id="jsonTabLi" href="#jsonTab">Data</a></li>
|
|
468
|
+
<li><a href="#infoTab">About</a></li>
|
|
469
|
+
</ul>
|
|
470
|
+
<div id="codeTab">
|
|
471
|
+
<textarea id="scriptEditor"></textarea>
|
|
472
|
+
</div>
|
|
473
|
+
<div id="dataTab" style="overflow-y: auto">
|
|
474
|
+
<table
|
|
475
|
+
id="dataTable"
|
|
476
|
+
class="display"
|
|
477
|
+
style="font-size: 0.75rem; width: 100%"
|
|
478
|
+
></table>
|
|
479
|
+
</div>
|
|
480
|
+
<div id="jsonTab" style="overflow-y: auto"></div>
|
|
481
|
+
<div id="infoTab" style="padding: 15px"></div>
|
|
482
|
+
<div id="codeControls">
|
|
483
|
+
<button onclick="window.loadScript()">run</button>
|
|
484
|
+
<button onclick="window.save()">save</button>
|
|
485
|
+
<button class="hiddenControl" onclick="window.showDepGraph()">show graph</button>
|
|
486
|
+
<span class="rendererControls">
|
|
487
|
+
<span style="margin-left: 10px;">Renderer:</span>
|
|
488
|
+
<input type="radio" id="SVG" name="renderer" value="svg" checked />
|
|
489
|
+
<label for="SVG">SVG</label>
|
|
490
|
+
<input type="radio" id="WebGL" name="renderer" value="webgl" disabled />
|
|
491
|
+
<label for="WebGL">WebGL</label>
|
|
492
|
+
</span>
|
|
493
|
+
</div>
|
|
494
|
+
</div>
|
|
495
|
+
|
|
496
|
+
<div id="gallery"></div>
|
|
497
|
+
<svg id="depGraphVis"></svg>
|
|
498
|
+
<a id="download"></a>
|
|
499
|
+
|
|
500
|
+
<script type="module">
|
|
501
|
+
import { JSONEditor } from "./lib/jsoneditor.js";
|
|
502
|
+
|
|
503
|
+
let demos = {};
|
|
504
|
+
let dataTable = null;
|
|
505
|
+
|
|
506
|
+
const editor = CodeMirror.fromTextArea(document.getElementById("scriptEditor"), {
|
|
507
|
+
lineNumbers: true,
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
let jsonEditor;
|
|
511
|
+
|
|
512
|
+
function showError(message, source, lineno, colno, error) {
|
|
513
|
+
const csl = document.getElementById("console");
|
|
514
|
+
removeAllChildren(csl);
|
|
515
|
+
const msgNode = document.createElement("div");
|
|
516
|
+
msgNode.textContent = message ? message : error ? JSON.stringify(error) : "";
|
|
517
|
+
msgNode.style.color = "red";
|
|
518
|
+
csl.appendChild(msgNode);
|
|
519
|
+
if (error && error.stack) {
|
|
520
|
+
const stackNode = document.createElement("div");
|
|
521
|
+
stackNode.textContent = error.stack.replace(/\n/g, "\r\n").replace(/@/g, " at ");
|
|
522
|
+
stackNode.style.whiteSpace = "pre-line";
|
|
523
|
+
stackNode.style.color = "#777";
|
|
524
|
+
csl.appendChild(stackNode);
|
|
525
|
+
}
|
|
526
|
+
csl.style.display = "block";
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
window.onerror = showError;
|
|
530
|
+
|
|
531
|
+
window.updateShowGalleryBtn = function () {
|
|
532
|
+
const btn = document.getElementById("showGalleryBtn");
|
|
533
|
+
btn.disabled = document.getElementById("gallery").style.visibility !== "hidden";
|
|
534
|
+
updateDemoNav();
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
function removeAllChildren(e) {
|
|
538
|
+
while (e.firstChild) e.firstChild.remove();
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function makeAsync(s) {
|
|
542
|
+
const c =
|
|
543
|
+
"try {\n" +
|
|
544
|
+
s +
|
|
545
|
+
"\n} catch (err) {showError(err.name ? err.name + ': ' + err.message : undefined, 0, 0, 0, err);}";
|
|
546
|
+
return "(async () => {" + c + "})();";
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// "static" or "interactive", taken from the first value of the demo's behavior.
|
|
550
|
+
function getDemoKind(demo) {
|
|
551
|
+
const behavior = demos[demo] && demos[demo].behavior;
|
|
552
|
+
return behavior && behavior[0] === "interactive" ? "interactive" : "static";
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// URL state, e.g. ?demo=BarChartHorz&mark=bars-and-rectangles,lines-and-paths&behavior=interactive&q=brush
|
|
556
|
+
// `demo` present => that demo is shown; absent => the gallery is shown.
|
|
557
|
+
function getCurrentDemo() {
|
|
558
|
+
return new URLSearchParams(window.location.search).get("demo") || "";
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function slug(v) {
|
|
562
|
+
return String(v).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Tags that get an icon before the demo name (gallery + sidebar) and an entry in the legend.
|
|
566
|
+
// To add one (e.g. "animated"), add the value to the demos' "behavior" array in demos.json and add an entry here.
|
|
567
|
+
const TAG_ICONS = {
|
|
568
|
+
interactive: {
|
|
569
|
+
label: "Interactive",
|
|
570
|
+
svg: '<svg viewBox="0 0 16 16" aria-hidden="true"><g fill="currentColor"><rect x="5" y="0.8" width="2.5" height="9" rx="1.25"/><rect x="7.8" y="5" width="2.3" height="5" rx="1.15"/><rect x="10.4" y="5.6" width="2.2" height="4.6" rx="1.1"/><rect x="12.9" y="6.5" width="2" height="4" rx="1"/><rect x="5" y="8" width="9.9" height="7" rx="2.6"/><rect x="2.2" y="7.2" width="2.4" height="5.4" rx="1.2" transform="rotate(-30 3.4 9.9)"/><rect x="4.2" y="9" width="2" height="2.4"/></g></svg>',
|
|
571
|
+
},
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
function appendTagIcon(sel, tag) {
|
|
575
|
+
sel
|
|
576
|
+
.append("span")
|
|
577
|
+
.attr("class", "demoIcon")
|
|
578
|
+
.attr("title", TAG_ICONS[tag].label)
|
|
579
|
+
.html(TAG_ICONS[tag].svg);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Fills `sel` with the demo's tag icons followed by its name.
|
|
583
|
+
function appendDemoLabel(sel, demo) {
|
|
584
|
+
sel.classed("demoLabel", true);
|
|
585
|
+
for (const tag of demos[demo].behavior || []) {
|
|
586
|
+
if (TAG_ICONS[tag]) appendTagIcon(sel, tag);
|
|
587
|
+
}
|
|
588
|
+
sel.append("span").attr("class", "demoLabelName").text(demos[demo].name || demo);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
// Sidebar filter groups. Within a group, selected tags are OR'ed; across groups, AND'ed.
|
|
593
|
+
// `order` (optional) fixes the display order of values; unlisted values follow in data order.
|
|
594
|
+
const capitalize = (v) => v.charAt(0).toUpperCase() + v.slice(1);
|
|
595
|
+
// Set `hidden: true` to take a group out of the sidebar, filtering and URL without deleting it.
|
|
596
|
+
const ALL_TAG_GROUPS = [
|
|
597
|
+
{ key: "mark", label: "Mark", valueOf: (d) => d.mark },
|
|
598
|
+
{
|
|
599
|
+
key: "composition",
|
|
600
|
+
label: "Composition",
|
|
601
|
+
valueOf: (d) => d.composition,
|
|
602
|
+
order: ["single chart", "small multiples", "multiple views"],
|
|
603
|
+
format: capitalize,
|
|
604
|
+
},
|
|
605
|
+
{
|
|
606
|
+
key: "structure",
|
|
607
|
+
label: "Data Structure",
|
|
608
|
+
hidden: true,
|
|
609
|
+
valueOf: (d) => d["data structure"],
|
|
610
|
+
order: ["list", "key-value", "tabular", "hierarchical", "network"],
|
|
611
|
+
format: capitalize,
|
|
612
|
+
},
|
|
613
|
+
{ key: "behavior", label: "Behavior", valueOf: (d) => (d.behavior || [])[0], format: capitalize },
|
|
614
|
+
];
|
|
615
|
+
const TAG_GROUPS = ALL_TAG_GROUPS.filter((g) => !g.hidden);
|
|
616
|
+
const selectedTags = Object.fromEntries(TAG_GROUPS.map((g) => [g.key, new Set()]));
|
|
617
|
+
let galleryOrder = [];
|
|
618
|
+
|
|
619
|
+
// All values of a group in display order: first appearance (static demos first, so marks follow
|
|
620
|
+
// the original static.json order), then re-sorted by the group's explicit `order` if it has one.
|
|
621
|
+
function getGroupValues(g) {
|
|
622
|
+
const keys = getVisibleDemoKeys();
|
|
623
|
+
const ordered = [
|
|
624
|
+
...keys.filter((k) => getDemoKind(k) === "static"),
|
|
625
|
+
...keys.filter((k) => getDemoKind(k) !== "static"),
|
|
626
|
+
];
|
|
627
|
+
const values = [];
|
|
628
|
+
for (const k of ordered) {
|
|
629
|
+
const v = g.valueOf(demos[k]);
|
|
630
|
+
if (v && !values.includes(v)) values.push(v);
|
|
631
|
+
}
|
|
632
|
+
if (g.order) values.sort((a, b) => rank(g.order, a) - rank(g.order, b));
|
|
633
|
+
return values;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Sorted by mark (sidebar order), then composition (sidebar order), then name.
|
|
637
|
+
function getGalleryOrder() {
|
|
638
|
+
if (!galleryOrder.length) {
|
|
639
|
+
const markOrder = getGroupValues(getGroup("mark"));
|
|
640
|
+
const compGroup = getGroup("composition");
|
|
641
|
+
const compOrder = getGroupValues(compGroup);
|
|
642
|
+
galleryOrder = getVisibleDemoKeys().sort(
|
|
643
|
+
(a, b) =>
|
|
644
|
+
rank(markOrder, demos[a].mark) - rank(markOrder, demos[b].mark) ||
|
|
645
|
+
rank(compOrder, compGroup.valueOf(demos[a])) - rank(compOrder, compGroup.valueOf(demos[b])) ||
|
|
646
|
+
(demos[a].name || a).localeCompare(demos[b].name || b)
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
return galleryOrder;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Previous/next follow the gallery order, limited to the demos that match the current filters.
|
|
653
|
+
// If the open demo itself doesn't match (e.g. opened by link), step through the full gallery instead.
|
|
654
|
+
function getNavSequence(demo) {
|
|
655
|
+
const filtered = getGalleryOrder().filter((k) => demoMatchesFilters(k));
|
|
656
|
+
return filtered.includes(demo) ? filtered : getGalleryOrder();
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function updateDemoNav() {
|
|
660
|
+
const nav = document.getElementById("demoNav");
|
|
661
|
+
const demo = getCurrentDemo();
|
|
662
|
+
const showingDemo = document.getElementById("gallery").style.visibility === "hidden" && demos[demo];
|
|
663
|
+
nav.style.visibility = showingDemo ? "visible" : "hidden";
|
|
664
|
+
if (!showingDemo) return;
|
|
665
|
+
|
|
666
|
+
const seq = getNavSequence(demo);
|
|
667
|
+
const i = seq.indexOf(demo);
|
|
668
|
+
const prev = document.getElementById("prevDemoBtn");
|
|
669
|
+
const next = document.getElementById("nextDemoBtn");
|
|
670
|
+
prev.disabled = i <= 0;
|
|
671
|
+
next.disabled = i < 0 || i >= seq.length - 1;
|
|
672
|
+
prev.title = prev.disabled ? "" : "Previous: " + (demos[seq[i - 1]].name || seq[i - 1]) + " (\u2190)";
|
|
673
|
+
next.title = next.disabled ? "" : "Next: " + (demos[seq[i + 1]].name || seq[i + 1]) + " (\u2192)";
|
|
674
|
+
document.getElementById("demoNavPos").textContent = i < 0 ? "" : i + 1 + " / " + seq.length;
|
|
675
|
+
d3.select("#demoNavName").html("").call(appendDemoLabel, demo);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
window.stepDemo = function (delta) {
|
|
679
|
+
const demo = getCurrentDemo();
|
|
680
|
+
if (!demos[demo] || document.getElementById("gallery").style.visibility !== "hidden") return;
|
|
681
|
+
const seq = getNavSequence(demo);
|
|
682
|
+
const target = seq[seq.indexOf(demo) + delta];
|
|
683
|
+
if (target) openDemo(target);
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
// Left/right arrow keys step through demos, unless the user is typing somewhere.
|
|
687
|
+
document.addEventListener("keydown", (event) => {
|
|
688
|
+
if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
|
|
689
|
+
if (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) return;
|
|
690
|
+
const el = document.activeElement;
|
|
691
|
+
if (el && (el.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) || el.closest(".CodeMirror"))) return;
|
|
692
|
+
window.stepDemo(event.key === "ArrowLeft" ? -1 : 1);
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
function getGroup(key) {
|
|
696
|
+
return ALL_TAG_GROUPS.find((g) => g.key === key);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function getVisibleDemoKeys() {
|
|
700
|
+
return Object.keys(demos).filter((k) => !demos[k].draft);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// `exceptGroup` ignores that group's selection (used for the per-tag counts).
|
|
704
|
+
function demoMatchesFilters(k, exceptGroup) {
|
|
705
|
+
const d = demos[k];
|
|
706
|
+
for (const g of TAG_GROUPS) {
|
|
707
|
+
if (g.key === exceptGroup) continue;
|
|
708
|
+
const sel = selectedTags[g.key];
|
|
709
|
+
if (sel.size && !sel.has(g.valueOf(d))) return false;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const searchValue = (document.getElementById("demoSearch")?.value || "").trim().toLowerCase();
|
|
713
|
+
if (!searchValue) return true;
|
|
714
|
+
const haystack = [k, d.name, d.title, d.mark, d.type, d.composition, d["data structure"], ...(d.behavior || [])]
|
|
715
|
+
.filter(Boolean)
|
|
716
|
+
.join("\n")
|
|
717
|
+
.toLowerCase();
|
|
718
|
+
return haystack.includes(searchValue);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function getSearchValue() {
|
|
722
|
+
return (document.getElementById("demoSearch")?.value || "").trim();
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Writes the current demo, selected tags and search text into the URL.
|
|
726
|
+
function updateUrl({ demo = getCurrentDemo(), push = false } = {}) {
|
|
727
|
+
const params = new URLSearchParams();
|
|
728
|
+
if (demo) params.set("demo", demo);
|
|
729
|
+
for (const g of TAG_GROUPS) {
|
|
730
|
+
const sel = selectedTags[g.key];
|
|
731
|
+
if (sel.size) params.set(g.key, [...sel].map(slug).join(","));
|
|
732
|
+
}
|
|
733
|
+
const q = getSearchValue();
|
|
734
|
+
if (q) params.set("q", q);
|
|
735
|
+
|
|
736
|
+
const query = params.toString().replace(/%2C/g, ",");
|
|
737
|
+
const url = window.location.pathname + (query ? "?" + query : "");
|
|
738
|
+
if (url === window.location.pathname + window.location.search + window.location.hash) return;
|
|
739
|
+
window.history[push ? "pushState" : "replaceState"](null, "", url);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// Reads selected tags and search text from the URL (slugs are matched against the actual values).
|
|
743
|
+
function readFiltersFromUrl() {
|
|
744
|
+
const params = new URLSearchParams(window.location.search);
|
|
745
|
+
for (const g of TAG_GROUPS) {
|
|
746
|
+
const sel = selectedTags[g.key];
|
|
747
|
+
sel.clear();
|
|
748
|
+
const raw = params.get(g.key);
|
|
749
|
+
if (!raw) continue;
|
|
750
|
+
const wanted = new Set(raw.split(",").map(slug));
|
|
751
|
+
for (const k of getVisibleDemoKeys()) {
|
|
752
|
+
const v = g.valueOf(demos[k]);
|
|
753
|
+
if (v && wanted.has(slug(v))) sel.add(v);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
document.getElementById("demoSearch").value = params.get("q") || "";
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Called whenever tags or search change: re-filter, show the gallery, and sync the URL.
|
|
760
|
+
// If a demo was open, push a history entry so Back returns to it.
|
|
761
|
+
function filtersChanged() {
|
|
762
|
+
buildTagFilters();
|
|
763
|
+
rebuildGallery();
|
|
764
|
+
updateUrl({ demo: "", push: !!getCurrentDemo() });
|
|
765
|
+
showView();
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function openDemo(demo) {
|
|
769
|
+
updateUrl({ demo, push: true });
|
|
770
|
+
showView();
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
window.showGalleryView = function () {
|
|
774
|
+
updateUrl({ demo: "", push: true });
|
|
775
|
+
showView();
|
|
776
|
+
};
|
|
777
|
+
|
|
778
|
+
function rank(order, value) {
|
|
779
|
+
const i = order.indexOf(value);
|
|
780
|
+
return i < 0 ? order.length : i;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const ADD_KEY_LABEL = /Mac|iPhone|iPad/.test(navigator.platform) ? "\u2318-click" : "Ctrl-click";
|
|
784
|
+
|
|
785
|
+
// Plain click: show only this value in its group (click the sole selection again to clear).
|
|
786
|
+
// Cmd/Ctrl-click or the checkbox: add/remove this value, keeping the rest of the group's selection.
|
|
787
|
+
function selectTag(sel, value, additive) {
|
|
788
|
+
if (additive) {
|
|
789
|
+
if (sel.has(value)) sel.delete(value);
|
|
790
|
+
else sel.add(value);
|
|
791
|
+
} else if (sel.size === 1 && sel.has(value)) {
|
|
792
|
+
sel.clear();
|
|
793
|
+
} else {
|
|
794
|
+
sel.clear();
|
|
795
|
+
sel.add(value);
|
|
796
|
+
}
|
|
797
|
+
filtersChanged();
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function buildTagFilters() {
|
|
801
|
+
const root = d3.select("#tagFilters");
|
|
802
|
+
root.html("");
|
|
803
|
+
const keys = getVisibleDemoKeys();
|
|
804
|
+
|
|
805
|
+
for (const g of TAG_GROUPS) {
|
|
806
|
+
// Every value is always listed (so the list doesn't jump around); its count is the number of
|
|
807
|
+
// demos it would show given the other groups' selections and the search text.
|
|
808
|
+
const values = getGroupValues(g);
|
|
809
|
+
const counts = new Map(values.map((v) => [v, 0]));
|
|
810
|
+
for (const k of keys) {
|
|
811
|
+
if (!demoMatchesFilters(k, g.key)) continue;
|
|
812
|
+
const v = g.valueOf(demos[k]);
|
|
813
|
+
if (v) counts.set(v, counts.get(v) + 1);
|
|
814
|
+
}
|
|
815
|
+
const sel = selectedTags[g.key];
|
|
816
|
+
|
|
817
|
+
const header = root.append("div").attr("class", "tagGroupHeader");
|
|
818
|
+
header.append("span").text(g.label);
|
|
819
|
+
if (sel.size) {
|
|
820
|
+
header
|
|
821
|
+
.append("span")
|
|
822
|
+
.attr("class", "tagGroupClear")
|
|
823
|
+
.text("Clear")
|
|
824
|
+
.on("click", () => {
|
|
825
|
+
sel.clear();
|
|
826
|
+
filtersChanged();
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const list = root.append("ul").attr("class", "tagGroup");
|
|
831
|
+
|
|
832
|
+
for (const value of values) {
|
|
833
|
+
const count = counts.get(value);
|
|
834
|
+
const selected = sel.has(value);
|
|
835
|
+
const disabled = !count && !selected;
|
|
836
|
+
const item = list
|
|
837
|
+
.append("li")
|
|
838
|
+
.attr("class", "tagItem")
|
|
839
|
+
.classed("selected", selected)
|
|
840
|
+
.classed("disabled", disabled)
|
|
841
|
+
.attr("title", disabled ? "No demos match the other filters" : "Click to show only this \u00b7 " + ADD_KEY_LABEL + " to add")
|
|
842
|
+
.on("click", (event) => {
|
|
843
|
+
if (disabled) return;
|
|
844
|
+
selectTag(sel, value, event.metaKey || event.ctrlKey);
|
|
845
|
+
});
|
|
846
|
+
item
|
|
847
|
+
.append("span")
|
|
848
|
+
.attr("class", "tagCheck")
|
|
849
|
+
.on("click", (event) => {
|
|
850
|
+
event.stopPropagation();
|
|
851
|
+
if (!disabled) selectTag(sel, value, true);
|
|
852
|
+
});
|
|
853
|
+
if (TAG_ICONS[value]) appendTagIcon(item, value);
|
|
854
|
+
item
|
|
855
|
+
.append("span")
|
|
856
|
+
.attr("class", "tagItemLabel")
|
|
857
|
+
.text(g.format ? g.format(value) : value);
|
|
858
|
+
item.append("span").attr("class", "tagItemCount").text(count);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function anyFilterActive() {
|
|
864
|
+
return !!getSearchValue() || TAG_GROUPS.some((g) => selectedTags[g.key].size);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// Hand-picked demos, ordered by their "featured" number in demos.json.
|
|
868
|
+
function getFeaturedKeys() {
|
|
869
|
+
return getVisibleDemoKeys()
|
|
870
|
+
.filter((k) => demos[k].featured)
|
|
871
|
+
.sort((a, b) => demos[a].featured - demos[b].featured);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function appendGallerySection(container, label, count) {
|
|
875
|
+
const heading = container.append("div").attr("class", "gallerySection");
|
|
876
|
+
heading.append("span").text(label);
|
|
877
|
+
if (count != null) heading.append("span").attr("class", "gallerySectionCount").text(count);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function rebuildGallery() {
|
|
881
|
+
const container = d3.select("#gallery");
|
|
882
|
+
container.html("");
|
|
883
|
+
|
|
884
|
+
const keys = getGalleryOrder().filter((k) => demoMatchesFilters(k));
|
|
885
|
+
|
|
886
|
+
if (!keys.length) {
|
|
887
|
+
container.append("div").attr("id", "galleryEmpty").text("No demos match the selected filters.");
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// Featured section first, only when nothing is filtered or searched. Featured demos also
|
|
891
|
+
// appear in their mark sections below, so each section stays complete.
|
|
892
|
+
const featured = anyFilterActive() ? [] : getFeaturedKeys();
|
|
893
|
+
if (featured.length) {
|
|
894
|
+
appendGallerySection(container, "Featured");
|
|
895
|
+
for (const k of featured) appendGalleryItem(container, k);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// One section heading per mark, with the number of matching demos in it.
|
|
899
|
+
const sectionCounts = new Map();
|
|
900
|
+
for (const k of keys) sectionCounts.set(demos[k].mark, (sectionCounts.get(demos[k].mark) || 0) + 1);
|
|
901
|
+
let currentSection = null;
|
|
902
|
+
|
|
903
|
+
for (const k of keys) {
|
|
904
|
+
if (demos[k].mark !== currentSection) {
|
|
905
|
+
currentSection = demos[k].mark;
|
|
906
|
+
appendGallerySection(container, currentSection || "Other", sectionCounts.get(currentSection));
|
|
907
|
+
}
|
|
908
|
+
appendGalleryItem(container, k);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function appendGalleryItem(container, k) {
|
|
913
|
+
const item = container.append("div").attr("class", "galleryItem");
|
|
914
|
+
const stillPath = 'demos/thumbnails/' + getDemoBaseName(k) + '.png';
|
|
915
|
+
const cacheStamp = Date.now();
|
|
916
|
+
const stillPreviewPath = stillPath + "?t=" + cacheStamp;
|
|
917
|
+
const hoverPreviewPath =
|
|
918
|
+
getDemoKind(k) === "interactive"
|
|
919
|
+
? stillPath.replace(/\.png$/i, ".gif")
|
|
920
|
+
: null;
|
|
921
|
+
const preview = item
|
|
922
|
+
.append("div")
|
|
923
|
+
.style("display", "block")
|
|
924
|
+
.style("height", "150px")
|
|
925
|
+
.style("background", "white")
|
|
926
|
+
.style("overflow", "hidden")
|
|
927
|
+
.style("position", "relative")
|
|
928
|
+
.on("mouseenter", function () {
|
|
929
|
+
if (hoverPreviewPath) {
|
|
930
|
+
img.attr("src", hoverPreviewPath + "?t=" + Date.now());
|
|
931
|
+
}
|
|
932
|
+
})
|
|
933
|
+
.on("mouseleave", function () {
|
|
934
|
+
img.attr("src", stillPreviewPath);
|
|
935
|
+
})
|
|
936
|
+
.on("click", () => openDemo(k));
|
|
937
|
+
|
|
938
|
+
const img = preview
|
|
939
|
+
.append("img")
|
|
940
|
+
.attr("src", stillPreviewPath)
|
|
941
|
+
.style("display", "block")
|
|
942
|
+
.style("position", "absolute")
|
|
943
|
+
.style("left", "0")
|
|
944
|
+
.style("top", "0")
|
|
945
|
+
.style("height", "100%")
|
|
946
|
+
.style("width", "100%")
|
|
947
|
+
.style("object-fit", "contain")
|
|
948
|
+
.style("object-position", "center center")
|
|
949
|
+
.style("pointer-events", "none");
|
|
950
|
+
|
|
951
|
+
item
|
|
952
|
+
.append("div")
|
|
953
|
+
.attr("class", "galleryItemName")
|
|
954
|
+
.style("background", "#FAFAFA")
|
|
955
|
+
.style("text-align", "left")
|
|
956
|
+
.style("color", "#222")
|
|
957
|
+
.style("height", "16px")
|
|
958
|
+
.style("padding", "5px 4px 0 4px")
|
|
959
|
+
.call(appendDemoLabel, k);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function loadDataTable(responseText) {
|
|
963
|
+
const csvRegex = /msc\.csv\("(.+)"\);/;
|
|
964
|
+
const csvMatch = csvRegex.exec(responseText);
|
|
965
|
+
if (!csvMatch) return;
|
|
966
|
+
|
|
967
|
+
const csvPath = csvMatch[1];
|
|
968
|
+
fetch(csvPath)
|
|
969
|
+
.then((resp) => resp.text())
|
|
970
|
+
.then((text) => {
|
|
971
|
+
const rows = text.trim().split("\n");
|
|
972
|
+
if (dataTable !== null) dataTable.destroy();
|
|
973
|
+
|
|
974
|
+
const table = document.getElementById("dataTable");
|
|
975
|
+
const newHead = `<thead><tr>${rows[0]
|
|
976
|
+
.split(",")
|
|
977
|
+
.reduce((acc, col) => (acc += `<th>${col}</th>`), "")}</tr></thead>`;
|
|
978
|
+
table.innerHTML = newHead;
|
|
979
|
+
|
|
980
|
+
dataTable = $("#dataTable").DataTable({
|
|
981
|
+
data: rows.slice(1).map((row) => row.split(",")),
|
|
982
|
+
searching: false,
|
|
983
|
+
paging: false,
|
|
984
|
+
scrollCollapse: true,
|
|
985
|
+
fixedColumns: true,
|
|
986
|
+
info: false,
|
|
987
|
+
});
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function getJsonDataPath(responseText) {
|
|
992
|
+
const jsonRegex = /msc\.(tree|graph)json\(\s*["']([^"']+)["']\s*\)/i;
|
|
993
|
+
const jsonMatch = jsonRegex.exec(responseText);
|
|
994
|
+
return jsonMatch ? jsonMatch[2] : undefined;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function loadJson(responseText) {
|
|
998
|
+
const jsonPath = getJsonDataPath(responseText);
|
|
999
|
+
if (!jsonPath) return;
|
|
1000
|
+
|
|
1001
|
+
fetch(jsonPath)
|
|
1002
|
+
.then((resp) => resp.text())
|
|
1003
|
+
.then((text) => {
|
|
1004
|
+
const json = JSON.parse(text);
|
|
1005
|
+
jsonEditor.set({ text: undefined, json });
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function loadCtrls(demo) {
|
|
1010
|
+
const ctrlPanel = document.getElementById("ctrlPanel");
|
|
1011
|
+
ctrlPanel.innerHTML = "";
|
|
1012
|
+
const hasUi = getDemoKind(demo) === "interactive" && demos[demo].ui;
|
|
1013
|
+
if (!hasUi) {
|
|
1014
|
+
ctrlPanel.style.display = "none";
|
|
1015
|
+
loadDemo(demo);
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
ctrlPanel.style.display = "block";
|
|
1020
|
+
fetch("demos/interactive/" + demo + "_UI.js")
|
|
1021
|
+
.then((response) => (response.ok ? response.text() : undefined))
|
|
1022
|
+
.then((data) => {
|
|
1023
|
+
if (data) eval(data);
|
|
1024
|
+
loadDemo(demo);
|
|
1025
|
+
})
|
|
1026
|
+
.catch((error) => {
|
|
1027
|
+
console.error("Error fetching controls:", error);
|
|
1028
|
+
loadDemo(demo);
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
function getDemoBaseName(demo) {
|
|
1033
|
+
if (getDemoKind(demo) === "interactive") return demo;
|
|
1034
|
+
return demo[0].toLowerCase() + demo.substring(1);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function getDemoPath(demo) {
|
|
1038
|
+
if (getDemoKind(demo) === "interactive") return "demos/interactive/" + demo + ".js";
|
|
1039
|
+
return "demos/static/" + getDemoBaseName(demo) + ".js";
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
function loadDemo(demo) {
|
|
1043
|
+
const oReq = new XMLHttpRequest();
|
|
1044
|
+
oReq.addEventListener("load", function () {
|
|
1045
|
+
loadScript(this.responseText);
|
|
1046
|
+
|
|
1047
|
+
if (this.responseText.indexOf(".csv") > 0) {
|
|
1048
|
+
$("#jsonTabLi").parent().hide();
|
|
1049
|
+
$("#dataTabLi").parent().show();
|
|
1050
|
+
loadDataTable(this.responseText);
|
|
1051
|
+
} else if (getJsonDataPath(this.responseText)) {
|
|
1052
|
+
$("#jsonTabLi").parent().show();
|
|
1053
|
+
$("#dataTabLi").parent().hide();
|
|
1054
|
+
loadJson(this.responseText);
|
|
1055
|
+
} else {
|
|
1056
|
+
$("#jsonTabLi").parent().hide();
|
|
1057
|
+
$("#dataTabLi").parent().hide();
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
$("#btmPanel").tabs("option", "active", 0);
|
|
1061
|
+
|
|
1062
|
+
if (demos[demo]) {
|
|
1063
|
+
d3.select("#infoTab").html(demos[demo].about || "");
|
|
1064
|
+
document.querySelectorAll("#infoTab a").forEach((a) => {
|
|
1065
|
+
a.setAttribute("target", "_blank");
|
|
1066
|
+
a.setAttribute("rel", "noopener noreferrer");
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
oReq.open("GET", getDemoPath(demo));
|
|
1072
|
+
oReq.send();
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
window.loadScript = function (s) {
|
|
1076
|
+
const script = s ? s : editor.getValue();
|
|
1077
|
+
const parsed = acorn.parse(script, { ecmaVersion: "latest" });
|
|
1078
|
+
let blocks = parsed.body.map((d) => script.substring(d.start, d.end));
|
|
1079
|
+
const isStatic = getDemoKind(getCurrentDemo()) === "static";
|
|
1080
|
+
|
|
1081
|
+
if (isStatic) {
|
|
1082
|
+
blocks = blocks.filter((l) => l.indexOf("render") < 0);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
const sceneLine = blocks.find((l) => l.indexOf("msc.scene(") > 0);
|
|
1086
|
+
if (!sceneLine) {
|
|
1087
|
+
// Nothing to render (e.g. a scratch file that's all comments): still show the code and clear the view.
|
|
1088
|
+
if (s !== undefined) editor.setValue(script);
|
|
1089
|
+
removeAllChildren(document.getElementById("svgElement"));
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
const sceneName = sceneLine.split("=")[0].replace("let", " ").trim();
|
|
1094
|
+
const r = $("input[type='radio'][name='renderer']:checked").val();
|
|
1095
|
+
const ele = r === "svg" ? "svgElement" : "canvasElement";
|
|
1096
|
+
|
|
1097
|
+
let newScript = blocks.join("\n");
|
|
1098
|
+
if (isStatic) {
|
|
1099
|
+
newScript += "\nmsc.renderer('" + r + "','" + ele + "').render(" + sceneName + ");";
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
const csl = document.getElementById("console");
|
|
1103
|
+
removeAllChildren(csl);
|
|
1104
|
+
csl.style.display = "none";
|
|
1105
|
+
document.getElementById("gallery").style.visibility = "hidden";
|
|
1106
|
+
window.updateShowGalleryBtn();
|
|
1107
|
+
document.getElementById("depGraphVis").style.visibility = "hidden";
|
|
1108
|
+
|
|
1109
|
+
if (r === "svg") {
|
|
1110
|
+
const svg = document.getElementById("svgElement");
|
|
1111
|
+
removeAllChildren(svg);
|
|
1112
|
+
svg.style.display = "block";
|
|
1113
|
+
document.getElementById("canvasElement").style.display = "none";
|
|
1114
|
+
} else {
|
|
1115
|
+
const canvas = document.getElementById("canvasElement");
|
|
1116
|
+
canvas.style.display = "block";
|
|
1117
|
+
document.getElementById("svgElement").style.display = "none";
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
editor.setValue(newScript);
|
|
1121
|
+
eval(makeAsync(newScript));
|
|
1122
|
+
};
|
|
1123
|
+
|
|
1124
|
+
window.benchmark = function () {
|
|
1125
|
+
const demo = getCurrentDemo();
|
|
1126
|
+
if (getDemoKind(demo) !== "interactive") return;
|
|
1127
|
+
let script = editor.getValue();
|
|
1128
|
+
fetch("demos/interactive/" + demo + "_benchmark.js")
|
|
1129
|
+
.then((response) => (response.ok ? response.text() : undefined))
|
|
1130
|
+
.then((data) => {
|
|
1131
|
+
if (!data) return;
|
|
1132
|
+
script += data;
|
|
1133
|
+
const svg = document.getElementById("svgElement");
|
|
1134
|
+
removeAllChildren(svg);
|
|
1135
|
+
eval(makeAsync(script));
|
|
1136
|
+
})
|
|
1137
|
+
.catch((error) => {
|
|
1138
|
+
console.error("Error fetching benchmark:", error);
|
|
1139
|
+
});
|
|
1140
|
+
};
|
|
1141
|
+
|
|
1142
|
+
window.save = function () {
|
|
1143
|
+
const code = editor.getValue();
|
|
1144
|
+
const demo = getCurrentDemo();
|
|
1145
|
+
const scn = code.split("=")[0].replace("let", "").trim();
|
|
1146
|
+
let out = code;
|
|
1147
|
+
out += "const a = document.getElementById('download');";
|
|
1148
|
+
// Both modes save the same .msc format. The interactive branch used to call
|
|
1149
|
+
// scene.toJSON() and write .alscn, but toJSON() was removed when serialization
|
|
1150
|
+
// moved to utils/serialize.js, and nothing reads .alscn. Note that interactions
|
|
1151
|
+
// themselves are not serialized -- an interactive demo saves as a static scene.
|
|
1152
|
+
out += "let spec = msc.serialize(" + scn + ");";
|
|
1153
|
+
out +=
|
|
1154
|
+
"a.href = URL.createObjectURL(new Blob([JSON.stringify(spec, null, 2)], {type: 'application/json'}));";
|
|
1155
|
+
out += "a.setAttribute('download', '" + demo + ".msc');";
|
|
1156
|
+
out += "a.click();";
|
|
1157
|
+
eval(makeAsync(out));
|
|
1158
|
+
};
|
|
1159
|
+
|
|
1160
|
+
window.showDepGraph = function () {
|
|
1161
|
+
let code = editor.getValue();
|
|
1162
|
+
const scn = code.split("=")[0].replace("let", "").trim();
|
|
1163
|
+
code += "return " + scn + "._depGraph;";
|
|
1164
|
+
const dg = eval(makeAsync(code));
|
|
1165
|
+
dg.then((r) => {
|
|
1166
|
+
visualizeDepGraph(r);
|
|
1167
|
+
});
|
|
1168
|
+
};
|
|
1169
|
+
|
|
1170
|
+
document.addEventListener("keydown", function (event) {
|
|
1171
|
+
if (event.metaKey && !event.ctrlKey && !event.altKey && event.key.toLowerCase() === "g") {
|
|
1172
|
+
event.preventDefault();
|
|
1173
|
+
window.showDepGraph();
|
|
1174
|
+
}
|
|
1175
|
+
}, true);
|
|
1176
|
+
|
|
1177
|
+
let loadedDemo = null;
|
|
1178
|
+
|
|
1179
|
+
// Shows the demo named in the URL (loading it only if it isn't already loaded), or the gallery.
|
|
1180
|
+
function showView() {
|
|
1181
|
+
document.getElementById("depGraphVis").style.visibility = "hidden";
|
|
1182
|
+
const demo = getCurrentDemo();
|
|
1183
|
+
const gallery = document.getElementById("gallery");
|
|
1184
|
+
|
|
1185
|
+
if (!demo || !demos[demo]) {
|
|
1186
|
+
gallery.style.visibility = "visible";
|
|
1187
|
+
window.updateShowGalleryBtn();
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
gallery.style.visibility = "hidden";
|
|
1192
|
+
window.updateShowGalleryBtn();
|
|
1193
|
+
if (demo !== loadedDemo) {
|
|
1194
|
+
loadedDemo = demo;
|
|
1195
|
+
// loadCtrls shows the control panel for demos with a UI, otherwise hides it and loads the demo.
|
|
1196
|
+
loadCtrls(demo);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// Old links used #DemoKey; convert them to ?demo=DemoKey.
|
|
1201
|
+
function migrateHashLink() {
|
|
1202
|
+
const key = decodeURIComponent(window.location.hash.replace("#", ""));
|
|
1203
|
+
if (!key) return;
|
|
1204
|
+
const params = new URLSearchParams(window.location.search);
|
|
1205
|
+
params.set("demo", key);
|
|
1206
|
+
const query = params.toString().replace(/%2C/g, ",");
|
|
1207
|
+
window.history.replaceState(null, "", window.location.pathname + "?" + query);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
window.addEventListener("popstate", () => {
|
|
1211
|
+
readFiltersFromUrl();
|
|
1212
|
+
buildTagFilters();
|
|
1213
|
+
rebuildGallery();
|
|
1214
|
+
showView();
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
window.addEventListener("hashchange", () => {
|
|
1218
|
+
migrateHashLink();
|
|
1219
|
+
showView();
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
$("input[type=radio][name=renderer]").change(function () {
|
|
1223
|
+
window.loadScript();
|
|
1224
|
+
});
|
|
1225
|
+
|
|
1226
|
+
$(function () {
|
|
1227
|
+
$("#btmPanel").resizable({ minHeight: 100, handles: "n" });
|
|
1228
|
+
$("#btmPanel").tabs({
|
|
1229
|
+
activate: function (event, ui) {
|
|
1230
|
+
const t = ui.newTab.attr("li", "innerHTML")[0].getElementsByTagName("a")[0].innerHTML;
|
|
1231
|
+
if (t === "Code") $("#codeControls").css("visibility", "visible");
|
|
1232
|
+
else $("#codeControls").css("visibility", "hidden");
|
|
1233
|
+
},
|
|
1234
|
+
});
|
|
1235
|
+
|
|
1236
|
+
jsonEditor = new JSONEditor({
|
|
1237
|
+
target: document.getElementById("jsonTab"),
|
|
1238
|
+
props: {
|
|
1239
|
+
content: { text: undefined, json: {} },
|
|
1240
|
+
readOnly: true,
|
|
1241
|
+
onChange: (updatedContent, previousContent, patchResult) => {
|
|
1242
|
+
console.log("onChange", updatedContent, previousContent, patchResult);
|
|
1243
|
+
},
|
|
1244
|
+
},
|
|
1245
|
+
});
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
document.getElementById("demoSearch").addEventListener("input", () => {
|
|
1249
|
+
filtersChanged();
|
|
1250
|
+
});
|
|
1251
|
+
|
|
1252
|
+
$(document).ready(() => {
|
|
1253
|
+
fetch("demos/demos.json")
|
|
1254
|
+
.then((r) => r.json())
|
|
1255
|
+
.then((json) => {
|
|
1256
|
+
demos = { ...json };
|
|
1257
|
+
// Debug-only demos: not in demos.json, open only by URL (?demo=Test), and marked draft so they
|
|
1258
|
+
// never appear in the gallery, sidebar counts or previous/next. "Test" loads demos/static/test.js.
|
|
1259
|
+
const DEBUG_DEMOS = {
|
|
1260
|
+
Test: {
|
|
1261
|
+
name: "Test",
|
|
1262
|
+
behavior: ["static"],
|
|
1263
|
+
draft: true,
|
|
1264
|
+
about: "Scratch file for debugging visualizations (demos/static/test.js).",
|
|
1265
|
+
},
|
|
1266
|
+
};
|
|
1267
|
+
for (const [k, v] of Object.entries(DEBUG_DEMOS)) if (!demos[k]) demos[k] = v;
|
|
1268
|
+
migrateHashLink();
|
|
1269
|
+
readFiltersFromUrl();
|
|
1270
|
+
buildTagFilters();
|
|
1271
|
+
rebuildGallery();
|
|
1272
|
+
updateUrl(); // normalize the URL (e.g. drop unknown tag values)
|
|
1273
|
+
showView();
|
|
1274
|
+
});
|
|
1275
|
+
});
|
|
1276
|
+
</script>
|
|
1277
|
+
</body>
|
|
1278
|
+
</html>
|